commit 6c944c4ffd5dc336e638f910095a5e7c9cf118d0 Author: zzm <584337238@qq.com> Date: Mon Sep 21 19:21:35 2026 +0800 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0804e39 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Git +.git +.gitignore + +# IDE +.idea +*.iml +*.iws +*.ipr +.vscode + +# Build output +target/ +build/ +*.jar +!ruoyi-admin/target/ruoyi-admin.jar + +# Logs +*.log +nohup.out +logs/ + +# Docker +docker/compose/.env + +# Node +node_modules/ +dist/ + +# Misc +*.swp +__pycache__/ +*.pyc +.claude/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c186538 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +reviews +###################################################################### +# Build Tools + +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!.mvn/wrapper/maven-wrapper.jar + +###################################################################### +# IDE + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### JRebel ### +rebel.xml + +### NetBeans ### +nbproject/private/ +build/* +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +###################################################################### +# Others +*.log +*.xml.versionsBackup +*.swp + +!*/build/*.java +!*/build/*.html +!*/build/*.xml + +**/.vscode/ +**/__pycache__/ +**/uv.lock + +.claude/ + +###################################################################### +# Docker +docker/backup/ diff --git a/bin/checks/evidence.js b/bin/checks/evidence.js new file mode 100644 index 0000000..cc9547e --- /dev/null +++ b/bin/checks/evidence.js @@ -0,0 +1,113 @@ +// 只读证据收集:后台写接口 → 前端按钮文案(含出处),供人工核对后写进 set-known-audit-logs.js +// +// node bin/checks/evidence.js 输出到 stdout(很长,建议重定向) +// node bin/checks/evidence.js > ev.txt +// +// 链路:controller 端点 → api 函数(按 url+method 匹配)→ 调用该函数的 handler → 绑定该 handler 的按钮文案 +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..', '..'); +const UI = path.join(ROOT, 'ruoyi-ui', 'src'); + +function walk(dir, out, filter) { + if (!fs.existsSync(dir)) return out; + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) { if (e.name !== 'node_modules' && e.name !== 'dist') walk(p, out, filter); } + else if (!filter || filter(e.name)) out.push(p); + } + return out; +} +const norm = (u) => String(u).replace(/\$\{[^}]*\}/g, '{}').replace(/\{[^}]*\}/g, '{}') + .replace(/\/+/g, '/').replace(/\/$/, '').replace(/['"`;\s]/, ''); + +// ---------- 1. api 函数 → url/method ---------- +const apiEntries = []; // {fn, url, method, module} +for (const f of walk(path.join(UI, 'api'), [], (n) => n.endsWith('.js'))) { + const src = fs.readFileSync(f, 'utf8'); + const mod = '@/api/' + path.relative(path.join(UI, 'api'), f).replace(/\\/g, '/').replace(/\.js$/, ''); + for (const m of src.matchAll(/export function (\w+)\s*\(([\s\S]{0,600}?)\n\}/g)) { + const body = m[2]; + const url = (body.match(/url:\s*([`'"])([^`'"]+)\1/) || [])[2]; + const method = ((body.match(/method:\s*['"](\w+)['"]/) || [])[1] || 'get').toUpperCase(); + if (url) apiEntries.push({ fn: m[1], url, method, module: mod, file: path.relative(ROOT, f) }); + } +} + +// ---------- 2. .vue 里的按钮 + handler → api 调用 ---------- +const vueFiles = walk(path.join(UI, 'views'), [], (n) => n.endsWith('.vue')); +const fnEvidence = new Map(); // fn -> [{vue,line,label}] +for (const f of vueFiles) { + const src = fs.readFileSync(f, 'utf8'); + if (!/from ['"]@\/api\//.test(src)) continue; + const rel = path.relative(ROOT, f); + // 按钮:文字 / el-dropdown-item + for (const m of src.matchAll(/]*?@click(?:\.native)?="([\w.]+)[^"]*"[\s\S]{0,400}?>\s*([^<>{}]{1,20}?)\s*<\/el-(?:button|dropdown-item)>/g)) { + const handler = m[1].split('.')[0]; + const label = m[2].trim(); + const line = (src.slice(0, m.index).match(/\n/g) || []).length + 1; + // handler 函数体:从定义处到下一个顶层方法定义(截断保护),比按第一个 } 截更可靠 + const hm = src.match(new RegExp('(?:^|\\n)\\s*' + handler + '\\s*\\([^)]*\\)\\s*\\{')); + if (!hm) continue; + const rest = src.slice(hm.index + hm[0].length); + const nextDef = rest.search(/\n\s{0,4}[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/); + const body = rest.slice(0, nextDef < 0 ? 3000 : Math.min(nextDef, 3000)); + for (const e of apiEntries) { + if (new RegExp('\\b' + e.fn + '\\s*\\(').test(body)) { + if (!fnEvidence.has(e.fn)) fnEvidence.set(e.fn, []); + fnEvidence.get(e.fn).push({ vue: rel, line, label }); + } + } + } +} + +// ---------- 3. controller 端点 ---------- +const isBlockMember = (l) => /^\s*(\/\/|\*|\/\*|@)/.test(l) || (/^\s*[\w.]+\s*=/.test(l) && /[,)]\s*$/.test(l)); +const endpoints = []; +for (const f of walk(path.join(ROOT, 'skins-service'), [], (n) => n.endsWith('Controller.java')) + .concat(walk(path.join(ROOT, 'skins-promo'), [], (n) => n.endsWith('Controller.java')))) { + const src = fs.readFileSync(f, 'utf8'); + const base = (src.match(/@RequestMapping\(\s*(?:value\s*=\s*)?"([^"]*)"/) || [, null])[1]; + if (!base || !base.startsWith('/admin')) continue; + const lines = src.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + const mm = lines[i].match(/^\s*@(Get|Post|Put|Delete)Mapping\b(.*)$/); + if (!mm) continue; + if (mm[1].toUpperCase() === 'GET' && !/reset|clear|delete|remove|update|save|apply|generate|sync|send|publish|toggle|adjust|grant|cancel|recalc/i.test(lines[i])) { + // GET 默认跳过,除非名字像改数据 + } + let top = i; for (let j = i - 1; j >= 0; j--) { if (lines[j].trim() === '' || !isBlockMember(lines[j])) break; top = j; } + const block = lines.slice(top, i).join('\n'); + let name = null; + for (let j = i + 1; j < Math.min(i + 14, lines.length); j++) { + const s = lines[j].match(/^\s*(?:public|protected|private)\s+[\w<>\[\],\.\? ]+\s+(\w+)\s*\(/); if (s) { name = s[1]; break; } } + const sub = (mm[2].match(/"([^"]*)"/) || [, ''])[1]; + endpoints.push({ + controller: path.basename(f, '.java'), method: name, http: mm[1].toUpperCase(), + url: base + sub, human: /menu\s*=/.test(block) || /action\s*=/.test(block), hasLog: /@Log\(/.test(block), + }); + } +} + +// ---------- 4. 关联 ---------- +const out = []; +for (const ep of endpoints) { + if (ep.http === 'GET' && !/reset|clear|delete|remove|update|save|apply|generate|sync|send|publish|toggle|adjust|grant|cancel|recalc/i.test(ep.method || '')) continue; + if (ep.http === 'GET' && !/reset|clear|delete|remove|update|save|apply|generate|sync|send|publish|toggle|adjust|grant|cancel|recalc/i.test(ep.url)) continue; + const hit = apiEntries.find((e) => e.method === ep.http && norm(e.url) === norm(ep.url)) + || apiEntries.find((e) => e.method === ep.http && norm(e.url).startsWith(norm(ep.url) + '/')); + const ev = hit ? (fnEvidence.get(hit.fn) || []) : []; + out.push({ ep, hit, ev }); +} + +const state = (ep) => (!ep.hasLog ? '不记' : ep.human ? '已人话' : '旧JSON'); +const onlyTodo = process.argv.includes('--todo'); +console.log('controller.method | HTTP url | 现状 | 建议文案 | 证据'); +for (const { ep, hit, ev } of out) { + if (onlyTodo && ep.human) continue; + const labels = [...new Set(ev.map((x) => x.label))]; + const suggest = labels.length === 1 ? labels[0] : labels.length === 0 ? '—(无按钮证据)' : labels.map((l) => l + '?').join('/'); + const evidence = ev.slice(0, 2).map((x) => `${x.vue}:${x.line}`).join(' ') || (hit ? `api ${hit.fn} 无按钮绑定` : '无 api'); + console.log(`${(ep.controller + '.' + ep.method).padEnd(52)} ${ep.http.padEnd(6)} ${ep.url.padEnd(44)} ${state(ep).padEnd(6)} ${suggest.padEnd(22)} ${evidence}`); +} diff --git a/bin/checks/operation-audit.test.cjs b/bin/checks/operation-audit.test.cjs new file mode 100644 index 0000000..3ea8a0b --- /dev/null +++ b/bin/checks/operation-audit.test.cjs @@ -0,0 +1,32 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +;(async () => { + const source = fs.readFileSync(path.join(__dirname, '../../ruoyi-ui/src/utils/operationAudit.js'), 'utf8') + const { parseOperationAudit: parse } = await import('data:text/javascript;base64,' + Buffer.from(source).toString('base64')) + const payload = { auditVersion: 1, action: '修改', target: { id: 18, name: '测试用户' }, state: 'changed', changes: [ + { label: '账户余额', before: '100', after: '200' }, { label: '账户积分', before: '50', after: '80' } + ], truncated: false } + const result = parse(JSON.stringify(payload)) + assert.equal(result.changes.length, 2) + assert.equal(result.changes[0].before, '100') + assert.match(result.target, /18/) + for (const state of ['failed', 'unavailable', 'unchanged']) { + const p = parse(JSON.stringify({ ...payload, state })) + assert.equal(p.changes.length, 0) + assert.ok(p.notice) + } + assert.match(parse(JSON.stringify({ ...payload, truncated: true })).notice, /上限/) + assert.equal(parse('点击【修改】|余额\t100\t200').changes.length, 1) + assert.equal(parse('点击【修改】').changes.length, 0) + assert.equal(parse('{"password":"secret"}').action, '历史记录') + assert.equal(parse('{malformed').action, '历史记录') + const dangerous = '汉字|\t\n;→' + assert.equal(parse(JSON.stringify({ ...payload, changes: [{ label: '昵称', before: '', after: dangerous }] })).changes[0].after, dangerous) + const vue = fs.readFileSync(path.join(__dirname, '../../ruoyi-ui/src/views/monitor/operlog/index.vue'), 'utf8') + assert.ok(!vue.includes('v-html')) + const compiler = require('../../ruoyi-ui/node_modules/vue-template-compiler') + const template = compiler.parseComponent(vue).template.content + assert.deepEqual(compiler.compile(template).errors, []) + console.log('operation-audit parser and Vue template PASSED') +})().catch(e => { console.error(e); process.exit(1) }) diff --git a/bin/checks/run-user-audit.ps1 b/bin/checks/run-user-audit.ps1 new file mode 100644 index 0000000..ac54f51 --- /dev/null +++ b/bin/checks/run-user-audit.ps1 @@ -0,0 +1,17 @@ +$ErrorActionPreference = 'Stop' +$root = Resolve-Path (Join-Path $PSScriptRoot '../..') +Push-Location $root +try { + & mvn -o -pl ruoyi-admin -am test-compile -DskipTests -q + if ($LASTEXITCODE -ne 0) { throw 'test-compile failed' } + $cpFile = Join-Path $root 'ruoyi-admin/target/audit-classpath.txt' + & mvn -o -q -pl ruoyi-admin dependency:build-classpath "-Dmdep.outputFile=$cpFile" + if ($LASTEXITCODE -ne 0) { throw 'classpath generation failed' } + $classes = @('ruoyi-admin/target/test-classes','ruoyi-admin/target/classes','ruoyi-common/target/classes','ruoyi-framework/target/classes','ruoyi-system/target/classes','skins-model/target/classes','skins-service/service-admin/target/classes') + $cp = (($classes -join ';') + ';' + (Get-Content $cpFile -Raw).Trim()).Replace('\','/') + $argFile = Join-Path $root 'ruoyi-admin/target/audit-java.args' + $arguments = @('-Dfile.encoding=UTF-8', '-cp', ('"' + $cp + '"'), 'com.ruoyi.audit.UserOperationAuditTest') + [System.IO.File]::WriteAllLines($argFile, $arguments, (New-Object System.Text.UTF8Encoding($false))) + & java "@$argFile" + if ($LASTEXITCODE -ne 0) { throw 'audit regression failed' } +} finally { Pop-Location } diff --git a/bin/checks/set-known-audit-logs.js b/bin/checks/set-known-audit-logs.js new file mode 100644 index 0000000..a2e463c --- /dev/null +++ b/bin/checks/set-known-audit-logs.js @@ -0,0 +1,405 @@ +// 按「方法名 → 注解」显式白名单写入 @Log(不做 URL 猜配,避免错位) +// +// node bin/checks/set-known-audit-logs.js 干跑 +// node bin/checks/set-known-audit-logs.js --apply 落盘 +// +// 按钮文案全部取自页面源码原文(每条的来源写在本表注释里,格式:views/xxx.vue:行号): +// 工具条按钮(生成招商账号/一键上架/发放CDK)优先于弹窗里的通用【确 定】—— +// 运营的心智是"我点了那颗有名字的按钮",【确 定】记进日志等于没信息。 +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..', '..'); +const apply = process.argv.includes('--apply'); +const A = 'skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/'; +const W = 'skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/controller/'; +const O = 'skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/'; + +const TARGETS = [ + // ---- 用户列表(ttuser/index.vue:工具条 113/121/129,行内 86/96,下拉 212/218/224)---- + [A + 'TtUserController.java', 'generateAccount', { menu: '用户列表', action: '生成招商账号', type: 'INSERT' }], + [A + 'TtUserController.java', 'generateRobot', { menu: '用户列表', action: '生成机器人', type: 'INSERT' }], + [A + 'TtUserController.java', 'generateBlankAccount', { menu: '用户列表', action: '生成白号', type: 'INSERT' }], + [A + 'TtUserController.java', 'generateCardMerchant', { menu: '用户列表', action: '生成卡商账号', type: 'INSERT' }], + [A + 'TtUserController.java', 'edit', { menu: '用户列表', action: '修改', type: 'UPDATE', entity: 'TtUser', mapper: 'TtUserMapper' }], + [A + 'TtUserController.java', 'editInvitationCode', { menu: '用户列表', action: '修改邀请码', type: 'UPDATE', entity: 'TtUser', mapper: 'TtUserMapper' }], + [A + 'TtUserController.java', 'remove', { menu: '用户列表', action: '删除', type: 'DELETE' }], + [A + 'TtUserController.java', 'removeUserPackSackData', { menu: '背包列表', action: '删除', type: 'DELETE' }], + [A + 'TtUserController.java', 'resetUserBalance', { menu: '用户列表', action: '重置余额积分', type: 'CLEAN', entity: 'TtUser', mapper: 'TtUserMapper', idField: 'userId' }], + [A + 'TtUserController.java', 'forceDelete', { menu: '用户列表', action: '真删除', type: 'FORCE' }], + + // ---- 亏损返水(ttredPacket/lossRebate.vue:31 保存上限 / 112 发放CDK)---- + [A + 'LossRebateController.java', 'grant', { menu: '亏损返水', action: '发放CDK', type: 'GRANT', entity: 'TtLossRebate', mapper: 'TtLossRebateMapper', idField: 'id' }], + [A + 'LossRebateController.java', 'saveConfig', { menu: '亏损返水', action: '保存上限', type: 'UPDATE' }], + + // ---- 卡商充值(ttCardMerchantRecharge/index.vue:126 确认充值 / 160 确认修改)---- + [A + 'TtCardMerchantController.java', 'cardMerchantRecharge', { menu: '卡商充值', action: '确认充值', type: 'GRANT', entity: 'TtUser', mapper: 'TtUserMapper', idField: 'userId' }], + [A + 'TtCardMerchantController.java', 'cardMerchantUpdateBalance', { menu: '卡商充值', action: '确认修改', type: 'UPDATE', entity: 'TtUser', mapper: 'TtUserMapper', idField: 'userId' }], + + // ---- 生成空白卡密(blankCardKey/index.vue:5 生成空白卡密 开弹窗)---- + [A + 'AdminBlankCardKeyController.java', 'generate', { menu: '生成空白卡密', action: '生成空白卡密', type: 'INSERT' }], + + // ---- 疯狂转盘 游戏配置(game/wheel/config/index.vue:8 重置奖池 / 44 调整奖池 / 241 保存)---- + [W + 'AdminWheelController.java', 'resetPool', { menu: '游戏配置', action: '重置奖池', type: 'CLEAN' }], + [W + 'AdminWheelController.java', 'adjustPool', { menu: '游戏配置', action: '调整奖池', type: 'GRANT' }], + [W + 'AdminWheelController.java', 'updateGameConfig', { menu: '游戏配置', action: '保存', type: 'UPDATE' }], + // ---- 疯狂转盘 奖励矩阵(game/wheel/prize/index.vue:79 保存 / 89 全部保存 → saveMatrix)---- + [W + 'AdminWheelController.java', 'matrixSave', { menu: '奖励矩阵', action: '保存', type: 'UPDATE' }], + + // ---- 商城列表(ttShopping/index.vue:75 一键下架 / 82 一键上架)---- + [A + 'ShoppingController.java', 'putAwayAll', { menu: '商城列表', action: '一键上架', type: 'UPDATE' }], + [A + 'ShoppingController.java', 'soldOutAll', { menu: '商城列表', action: '一键下架', type: 'UPDATE' }], + + // ---- Roll房列表(ttRoll/index.vue:222 新 增 / 223 修 改 / 120 删除)---- + [A + 'TtRollController.java', 'add', { menu: 'Roll房列表', action: '新增', type: 'INSERT', entity: 'TtRoll', mapper: 'TtRollMapper' }], + [A + 'TtRollController.java', 'edit', { menu: 'Roll房列表', action: '修改', type: 'UPDATE', entity: 'TtRoll', mapper: 'TtRollMapper' }], + [A + 'TtRollController.java', 'remove', { menu: 'Roll房列表', action: '删除', type: 'DELETE' }], + + // ---- 抽奖活动(turnoverLottery/list.vue:43 新增抽奖活动 / 86 修改 / 92 删除)---- + [A + 'TurnoverLotteryAdminController.java', 'create', { menu: '抽奖活动', action: '新增抽奖活动', type: 'INSERT' }], + [A + 'TurnoverLotteryAdminController.java', 'update', { menu: '抽奖活动', action: '修改', type: 'UPDATE' }], + [A + 'TurnoverLotteryAdminController.java', 'delete', { menu: '抽奖活动', action: '删除', type: 'DELETE' }], + + // ---- 奥林匹斯 奖池水位(game/olympus/rewardPool/index.vue:101/106 批量注水|抽水、111 重置调整、172/175 行内图标→弹窗 213 确定注水|抽水)---- + [O + 'OlympusAdminController.java', 'rewardPoolAdjust', { menu: '奖池水位', action: '注水/抽水', type: 'GRANT' }], + [O + 'OlympusAdminController.java', 'rewardPoolAdjustBatch', { menu: '奖池水位', action: '批量注水/抽水', type: 'GRANT' }], + [O + 'OlympusAdminController.java', 'rewardPoolAdjustReset', { menu: '奖池水位', action: '重置调整', type: 'CLEAN' }], + + // ---- 宝箱列表(ttBox/index.vue:48 新增 / 59 修改 / 70 删除 / 282 重置宝箱)---- + [A + 'TtBoxController.java', 'add', { menu: '宝箱列表', action: '新增', type: 'INSERT', entity: 'TtBox', mapper: 'TtBoxMapper' }], + [A + 'TtBoxController.java', 'edit', { menu: '宝箱列表', action: '修改', type: 'UPDATE' }], + [A + 'TtBoxController.java', 'remove', { menu: '宝箱列表', action: '删除', type: 'DELETE' }], + // 注意:resetBox 是 @GetMapping,但它改数据(GET 不在常规扫描范围内,这里显式补上) + [A + 'TtBoxController.java', 'resetBox', { menu: '宝箱列表', action: '重置宝箱', type: 'CLEAN', entity: 'TtBox', mapper: 'TtBoxMapper', idField: 'boxId' }], + + // ---- Roll 奖品/房间(awarddetali.vue:22 指定获奖者 / 23 取消奖品;rollUser.vue:10 加入机器人 / 18 加入用户)---- + [A + 'TtRollController.java', 'namedWinner', { menu: '装备列表', action: '指定获奖者', type: 'INSERT' }], + [A + 'TtRollController.java', 'cancelNamedWinner', { menu: '装备列表', action: '取消奖品', type: 'DELETE' }], + [A + 'TtRollController.java', 'addRobot', { menu: '房间用户', action: '加入机器人', type: 'INSERT' }], + [A + 'TtRollController.java', 'addUser', { menu: '房间用户', action: '加入用户', type: 'INSERT' }], + + // ---- 奥林匹斯 箱子配置(game/olympus/box/index.vue:53 编辑 / 180 保存 / 15+141 一键重算分布 / 179 保存为模板 / 214 应用选中模板 / 207 删除模板)---- + [O + 'OlympusAdminController.java', 'boxSave', { menu: '箱子配置', action: '编辑', type: 'UPDATE' }], + [O + 'OlympusAdminController.java', 'boxOrnamentsSave', { menu: '箱子配置', action: '保存', type: 'UPDATE' }], + [O + 'OlympusAdminController.java', 'recalcBatch', { menu: '箱子配置', action: '一键重算分布', type: 'CLEAN' }], + [O + 'OlympusAdminController.java', 'templateSave', { menu: '箱子配置', action: '保存为模板', type: 'INSERT' }], + [O + 'OlympusAdminController.java', 'templateApply', { menu: '箱子配置', action: '应用选中模板', type: 'UPDATE' }], + [O + 'OlympusAdminController.java', 'templateDelete', { menu: '箱子配置', action: '删除', type: 'DELETE' }], + // ---- 奥林匹斯 奖品管理(game/olympus/prize/index.vue:44 编辑 → 87 确定 → prizeSave)---- + [O + 'OlympusAdminController.java', 'prizeSave', { menu: '奖品管理', action: '编辑', type: 'UPDATE' }], + + // ---- 消费排行榜奖励(rechargeRankingReward.vue:5 生成消费奖励 / 23 修改 / 8 重置→handleReset:165)---- + [A + 'TtRechargeRankingRewardController.java', 'generatePromotionLevel', { menu: '消费排行榜奖励', action: '生成消费奖励', type: 'UPDATE' }], + [A + 'TtRechargeRankingRewardController.java', 'edit', { menu: '消费排行榜奖励', action: '修改', type: 'UPDATE' }], + [A + 'TtRechargeRankingRewardController.java', 'truncateRankingReward', { menu: '消费排行榜奖励', action: '重置', type: 'CLEAN' }], + + // ---- 对战排行榜奖励(fightRankingReward.vue:5 生成排名奖励 / 23 修改 / 8 重置→handleReset:167)---- + [A + 'TtFightRankingRewardController.java', 'generatePromotionLevel', { menu: '对战排行榜奖励', action: '生成排名奖励', type: 'UPDATE' }], + [A + 'TtFightRankingRewardController.java', 'edit', { menu: '对战排行榜奖励', action: '修改', type: 'UPDATE' }], + [A + 'TtFightRankingRewardController.java', 'truncateRankingReward', { menu: '对战排行榜奖励', action: '重置', type: 'CLEAN' }], + + // ---- 饰品列表(ttOrnaments/index.vue:58 新增饰品 / 123 编辑 / 125=77 删除 / 117 发放饰品)---- + [A + 'TtOrnamentsController.java', 'add', { menu: '饰品列表', action: '新增饰品', type: 'INSERT', entity: 'TtOrnament', mapper: 'TtOrnamentMapper' }], + [A + 'TtOrnamentsController.java', 'edit', { menu: '饰品列表', action: '编辑', type: 'UPDATE', entity: 'TtOrnament', mapper: 'TtOrnamentMapper' }], + [A + 'TtOrnamentsController.java', 'delete', { menu: '饰品列表', action: '删除', type: 'DELETE' }], + [A + 'TtOrnamentsController.java', 'grantOrnaments', { menu: '饰品列表', action: '发放饰品', type: 'GRANT' }], + + // ================= 以下为"存量旧 JSON 格式"转人话(按钮文案已逐页核实:都是标准 新增/修改/删除)================= + // ---- 福利列表 / 福利开箱记录(ttWelfare/index.vue、welfareBoxRecords.vue)---- + [A + 'TtWelfareController.java', 'add', { menu: '福利列表', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtWelfareController.java', 'edit', { menu: '福利列表', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtWelfareController.java', 'remove', { menu: '福利列表', action: '删除', type: 'DELETE' }], + [A + 'TtWelfareController.java', 'removeBoxRecords', { menu: '福利开箱记录', action: '删除', type: 'DELETE' }], + [A + 'TtWelfareController.java', 'clearBoxRecords', { menu: '福利开箱记录', action: '清空全部记录', type: 'CLEAN' }], + + // ---- 每月充值福利(monthlyRecharges.vue)---- + [A + 'TtWelfareMonthlyRechargesController.java', 'add', { menu: '每月充值福利', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtWelfareMonthlyRechargesController.java', 'edit', { menu: '每月充值福利', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtWelfareMonthlyRechargesController.java', 'remove', { menu: '每月充值福利', action: '删除', type: 'DELETE' }], + + // ---- 首充赠送(ttBonus/firstRecharge.vue)---- + [A + 'TtFirstRechargeController.java', 'add', { menu: '首充赠送', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtFirstRechargeController.java', 'edit', { menu: '首充赠送', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtFirstRechargeController.java', 'remove', { menu: '首充赠送', action: '删除', type: 'DELETE' }], + + // ---- CDK管理(ttRedPack/index.vue)---- + [A + 'TtRedPackController.java', 'add', { menu: 'CDK管理', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRedPackController.java', 'edit', { menu: 'CDK管理', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRedPackController.java', 'remove', { menu: 'CDK管理', action: '删除', type: 'DELETE' }], + + // ---- 汰换记录(ttReplacementRecord/index.vue)---- + [A + 'TtReplacementRecordController.java', 'add', { menu: '汰换记录', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtReplacementRecordController.java', 'edit', { menu: '汰换记录', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtReplacementRecordController.java', 'remove', { menu: '汰换记录', action: '删除', type: 'DELETE' }], + + // ---- 机器人对战分组(ttFight/robotFightGroup.vue)---- + [A + 'TtRobotFightGroupController.java', 'add', { menu: '机器人对战分组', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRobotFightGroupController.java', 'edit', { menu: '机器人对战分组', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRobotFightGroupController.java', 'remove', { menu: '机器人对战分组', action: '删除', type: 'DELETE' }], + + // ---- 机器人对战分组宝箱(ttFight/robotFightGroupBox.vue)---- + [A + 'TtRobotFightGroupBoxController.java', 'add', { menu: '机器人对战分组宝箱', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRobotFightGroupBoxController.java', 'edit', { menu: '机器人对战分组宝箱', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRobotFightGroupBoxController.java', 'remove', { menu: '机器人对战分组宝箱', action: '删除', type: 'DELETE' }], + + // ---- 机器人对战配置(ttFight/robotFightConfig.vue,两个 controller 共用一页)---- + [A + 'TtRobotFightJoinConfigController.java', 'add', { menu: '机器人对战配置', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRobotFightJoinConfigController.java', 'edit', { menu: '机器人对战配置', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRobotFightJoinConfigController.java', 'remove', { menu: '机器人对战配置', action: '删除', type: 'DELETE' }], + [A + 'TtRobotFightPriceConfigController.java', 'add', { menu: '机器人对战配置', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRobotFightPriceConfigController.java', 'edit', { menu: '机器人对战配置', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRobotFightPriceConfigController.java', 'remove', { menu: '机器人对战配置', action: '删除', type: 'DELETE' }], + + // ---- 转赠申请(delivery/transferApply.vue:同意 / 驳回 / 确定同意)---- + [A + 'TtTransferApplyController.java', 'approve', { menu: '转赠申请', action: '同意', type: 'GRANT' }], + [A + 'TtTransferApplyController.java', 'reject', { menu: '转赠申请', action: '驳回', type: 'GRANT' }], + [A + 'TtTransferApplyController.java', 'approveBatch', { menu: '转赠申请', action: '确定同意', type: 'GRANT' }], + [A + 'TtTransferApplyController.java', 'rejectBatch', { menu: '转赠申请', action: '驳回', type: 'GRANT' }], + + // ---- 任务列表(ttTaskCenter/index.vue)---- + [A + 'TtTaskCenterController.java', 'add', { menu: '任务列表', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtTaskCenterController.java', 'edit', { menu: '任务列表', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtTaskCenterController.java', 'remove', { menu: '任务列表', action: '删除', type: 'DELETE' }], + + // ---- 开箱机会(ttOrnaments/boxOpenChance.vue;库里无同名菜单,用页面功能名)---- + [A + 'TtBoxOpenChanceController.java', 'add', { menu: '开箱机会', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtBoxOpenChanceController.java', 'edit', { menu: '开箱机会', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtBoxOpenChanceController.java', 'remove', { menu: '开箱机会', action: '删除', type: 'DELETE' }], + + // ================= 其余有按钮证据的页面(标签来自 evidence.js:直接调用方的按钮文案,CRUD 页按工具条 新增/修改/删除)================= + // ---- 公告(ttSetting/announcement.vue)---- + [A + 'TtAnnouncementController.java', 'addAnnouncement', { menu: '公告', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtAnnouncementController.java', 'editAnnouncement', { menu: '公告', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtAnnouncementController.java', 'removeAnnouncementByAnnouncementId', { menu: '公告', action: '删除', type: 'DELETE' }], + // ---- Banner设置 / 彩金 / 文章 / 文章类型 / 宝箱分类(ttSetting/banner.vue, ttBonus/index.vue, contentList, contentType, ttBox/boxType)---- + [A + 'TtBannerController.java', 'add', { menu: 'Banner设置', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtBannerController.java', 'edit', { menu: 'Banner设置', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtBannerController.java', 'remove', { menu: 'Banner设置', action: '删除', type: 'DELETE' }], + [A + 'TtBonusController.java', 'add', { menu: '彩金', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtBonusController.java', 'edit', { menu: '彩金', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtBonusController.java', 'remove', { menu: '彩金', action: '删除', type: 'DELETE' }], + [A + 'TtContentController.java', 'add', { menu: '文章', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtContentController.java', 'edit', { menu: '文章', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtContentController.java', 'remove', { menu: '文章', action: '删除', type: 'DELETE' }], + [A + 'TtContentTypeController.java', 'add', { menu: '文章类型', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtContentTypeController.java', 'edit', { menu: '文章类型', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtContentTypeController.java', 'remove', { menu: '文章类型', action: '删除', type: 'DELETE' }], + [A + 'TtBoxTypeController.java', 'add', { menu: '宝箱分类', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtBoxTypeController.java', 'edit', { menu: '宝箱分类', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtBoxTypeController.java', 'remove', { menu: '宝箱分类', action: '删除', type: 'DELETE' }], + // ---- 宝箱饰品(挂在宝箱列表下;批量填货按钮见 ttBox/index.vue:283)---- + [A + 'TtBoxOrnamentsController.java', 'add', { menu: '宝箱列表', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtBoxOrnamentsController.java', 'edit', { menu: '宝箱列表', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtBoxOrnamentsController.java', 'remove', { menu: '宝箱列表', action: '删除', type: 'DELETE' }], + [A + 'TtBoxOrnamentsController.java', 'batchAdd', { menu: '宝箱列表', action: '批量填货', type: 'INSERT' }], + // ---- 等级类(ttOrnamentsLevel/index.vue, ttSetting/promotionLevel.vue, ttSetting/vips.vue)---- + [A + 'TtOrnamentsLevelController.java', 'edit', { menu: '饰品等级', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtOrnamentsLevelController.java', 'generateOrnamentsLevel', { menu: '饰品等级', action: '新增', type: 'INSERT' }], + [A + 'TtPromotionLevelController.java', 'edit', { menu: '推广等级设置', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtPromotionLevelController.java', 'generatePromotionLevel', { menu: '推广等级设置', action: '生成推广列表', type: 'INSERT' }], + [A + 'TtPromotionLevelController.java', 'truncateVipLevel', { menu: '推广等级设置', action: '重置', type: 'CLEAN' }], + [A + 'TtVipLevelController.java', 'edit', { menu: 'VIP等级设置', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtVipLevelController.java', 'generateVipLevel', { menu: 'VIP等级设置', action: '生成VIP等级', type: 'INSERT' }], + [A + 'TtVipLevelController.java', 'truncateVipLevel', { menu: 'VIP等级设置', action: '重置', type: 'CLEAN' }], + // ---- 充值/卡密(ttRechargeList/index.vue, ttRechargeRecord? , ttGenerateCard/index.vue:135)---- + [A + 'TtRechargeListController.java', 'add', { menu: '货币列表', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRechargeListController.java', 'edit', { menu: '货币列表', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRechargeListController.java', 'remove', { menu: '货币列表', action: '删除', type: 'DELETE' }], + [A + 'TtRechargeConfigController.java', 'add', { menu: '网站货币管理', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRechargeConfigController.java', 'edit', { menu: '网站货币管理', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRechargeConfigController.java', 'remove', { menu: '网站货币管理', action: '删除', type: 'DELETE' }], + [A + 'TtRechargeCardController.java', 'generateCard', { menu: '卡密', action: '生成卡密', type: 'INSERT' }], + [A + 'TtRechargeCardController.java', 'generateBlankCard', { menu: '卡密', action: '生成空白卡密', type: 'INSERT' }], + // ---- CDK口令 / Roll奖池 / 限时Roll / 官方周期Roll配置 / 奖项配置 ---- + [A + 'TtRedPacketController.java', 'add', { menu: 'CDK口令', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRedPacketController.java', 'edit', { menu: 'CDK口令', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRedPacketController.java', 'remove', { menu: 'CDK口令', action: '删除', type: 'DELETE' }], + [A + 'TtRollJackpotController.java', 'add', { menu: '奖池列表', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRollJackpotController.java', 'edit', { menu: '奖池列表', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRollJackpotController.java', 'remove', { menu: '奖池列表', action: '删除', type: 'DELETE' }], + [A + 'TtRollJackpotOrnamentsController.java', 'add', { menu: '奖池列表', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtRollJackpotOrnamentsController.java', 'edit', { menu: '奖池列表', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtRollJackpotOrnamentsController.java', 'remove', { menu: '奖池列表', action: '删除', type: 'DELETE' }], + [A + 'TtTimeRollController.java', 'add', { menu: '时间roll房', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtTimeRollController.java', 'edit', { menu: '时间roll房', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtTimeRollController.java', 'remove', { menu: '时间roll房', action: '删除', type: 'DELETE' }], + [A + 'TtOfficialRollConfigController.java', 'add', { menu: '官方周期Roll配置', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtOfficialRollConfigController.java', 'edit', { menu: '官方周期Roll配置', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtOfficialRollConfigController.java', 'remove', { menu: '官方周期Roll配置', action: '删除', type: 'DELETE' }], + [A + 'TurnoverLotteryPrizeController.java', 'add', { menu: '奖项配置', action: '新增', type: 'INSERT', auto: true }], + [A + 'TurnoverLotteryPrizeController.java', 'edit', { menu: '奖项配置', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TurnoverLotteryPrizeController.java', 'remove', { menu: '奖项配置', action: '删除', type: 'DELETE' }], + // ---- 升级饰品 / 失败奖励(ttUpgrade/index.vue, upgradeFall.vue)---- + [A + 'TtUpgradeOrnamentsController.java', 'batchAdd', { menu: '饰品列表', action: '新增', type: 'INSERT' }], + [A + 'TtUpgradeOrnamentsController.java', 'edit', { menu: '饰品列表', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtUpgradeOrnamentsController.java', 'remove', { menu: '饰品列表', action: '删除', type: 'DELETE' }], + [A + 'TtUpgradeFailOrnamentsController.java', 'batchAdd', { menu: '失败奖励', action: '新增', type: 'INSERT' }], + [A + 'TtUpgradeFailOrnamentsController.java', 'edit', { menu: '失败奖励', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtUpgradeFailOrnamentsController.java', 'remove', { menu: '失败奖励', action: '删除', type: 'DELETE' }], + // ---- 头像设置 / 网站道具 / 发货记录 / 商城上下架 / 站点参数 ---- + [A + 'TtUserAvatarController.java', 'add', { menu: '头像设置', action: '新增', type: 'INSERT', auto: true }], + [A + 'TtUserAvatarController.java', 'edit', { menu: '头像设置', action: '修改', type: 'UPDATE', auto: true }], + [A + 'TtUserAvatarController.java', 'remove', { menu: '头像设置', action: '删除', type: 'DELETE' }], + [A + 'WebsitePropertyController.java', 'add', { menu: '网站道具', action: '新增', type: 'INSERT', auto: true }], + [A + 'WebsitePropertyController.java', 'edit', { menu: '网站道具', action: '修改', type: 'UPDATE', auto: true }], + [A + 'WebsitePropertyController.java', 'remove', { menu: '网站道具', action: '删除', type: 'DELETE' }], + [A + 'TtDeliveryRecordController.java', 'deliveryApprove', { menu: '发货记录', action: '确认发货', type: 'GRANT' }], + [A + 'ShoppingController.java', 'batchPutAwayOrSoldOut', { menu: '商城列表', action: '上架/下架', type: 'UPDATE' }], + [A + 'WebsiteSetupController.java', 'updateParameterSetting', { menu: '网站设置', action: '保存', type: 'UPDATE' }], + // ---- 疯狂转盘 奖励矩阵删除 ---- + [W + 'AdminWheelController.java', 'matrixDelete', { menu: '奖励矩阵', action: '删除', type: 'DELETE' }], +]; + +const IMPORTS = { + TtUser: 'com.ruoyi.domain.entity.sys.TtUser', + TtUserMapper: 'com.ruoyi.admin.mapper.TtUserMapper', + TtLossRebate: 'com.ruoyi.domain.entity.TtLossRebate', + TtLossRebateMapper: 'com.ruoyi.admin.mapper.TtLossRebateMapper', + TtRoll: 'com.ruoyi.domain.entity.roll.TtRoll', + TtRollMapper: 'com.ruoyi.admin.mapper.TtRollMapper', + TtBox: 'com.ruoyi.domain.other.TtBox', + TtBoxMapper: 'com.ruoyi.admin.mapper.TtBoxMapper', + TtOrnament: 'com.ruoyi.domain.entity.TtOrnament', + TtOrnamentMapper: 'com.ruoyi.admin.mapper.TtOrnamentMapper', +}; + +const isBlockMember = (l) => + /^\s*(\/\/|\*|\/\*|@)/.test(l) || (/^\s*[\w.]+\s*=/.test(l) && /[,)]\s*$/.test(l)); + +// ---- 自动解析实体/Mapper:attrs.auto = true 时,按方法入参里的实体类型去认 ---- +function collectFiles(dir, out, suffix) { + if (!fs.existsSync(dir)) return out; + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) collectFiles(p, out, suffix); + else if (e.isFile() && e.name.endsWith(suffix)) out.push(p); + } + return out; +} +const ENTITY = new Map(); // simpleName -> fqn(skins-model 全部类;真正用的时候还要求有 Mapper) +for (const f of collectFiles(path.join(ROOT, 'skins-model'), [], '.java')) { + const src = fs.readFileSync(f, 'utf8'); + const cls = (src.match(/public\s+class\s+(\w+)/) || [, null])[1]; + const pkg = (src.match(/^package\s+([\w.]+);/m) || [, ''])[1]; + if (cls && !ENTITY.has(cls)) ENTITY.set(cls, `${pkg}.${cls}`); +} +const MAPPER = new Map(); // entitySimple -> {simple, fqn}(BaseMapper 与普通 MyBatis 接口都要认) +for (const f of collectFiles(path.join(ROOT, 'skins-service'), [], 'Mapper.java')) { + const simple = path.basename(f, '.java'); + const entityName = simple.endsWith('Mapper') ? simple.slice(0, -6) : null; + if (!entityName || !ENTITY.has(entityName) || MAPPER.has(entityName)) continue; + const pkg = (fs.readFileSync(f, 'utf8').match(/^package\s+([\w.]+);/m) || [, ''])[1]; + MAPPER.set(entityName, { simple, fqn: `${pkg}.${simple}` }); +} + +/** 取方法参数里的类型名(优先 @RequestBody 那个) */ +function methodTypes(lines, sig) { + let text = ''; + for (let i = sig; i < Math.min(sig + 10, lines.length); i++) { + text += lines[i] + ' '; + if (/\)\s*\{/.test(lines[i]) || /\)\s*$/.test(lines[i])) break; + } + const inner = (text.match(/\(([\s\S]*?)\)/) || [, ''])[1]; + const types = []; + const rb = inner.match(/@RequestBody\s+(?:@\w+(?:\([^)]*\))?\s+)*(\w+)/); + if (rb) types.push(rb[1]); + for (const m of inner.matchAll(/(?:^|,)\s*(?:final\s+)?([A-Z]\w+)\s+\w+/g)) if (!types.includes(m[1])) types.push(m[1]); + return types; +} + +function render(a, indent) { + const parts = [`title = "${a.menu}"`, `action = "${a.action}"`, `businessType = BusinessType.${a.type}`]; + if (a.entity) parts.push(`entityClass = ${a.entity}.class`); + if (a.mapper) parts.push(`mapperClass = ${a.mapper}.class`); + if (a.idField) parts.push(`idField = "${a.idField}"`); + const one = `${indent}@Log(${parts.join(', ')})`; + if (one.length <= 130) return [one]; + return [`${indent}@Log(${parts.slice(0, 3).join(', ')},`, `${indent} ${parts.slice(3).join(', ')})`]; +} + +function insertImports(lines, entries) { + for (const e of entries) { + // 集合里既有全限定名(com.ruoyi...),也有短名(TtUser),分开处理 + const fqn = e.includes('.') ? e : IMPORTS[e]; + if (!fqn) continue; + const simple = fqn.split('.').pop(); + if (lines.some((l) => new RegExp(`^import\\s+[\\w.]*\\b${simple};`).test(l.trim()))) continue; + let at = lines.findIndex((l) => /^import com\.ruoyi\./.test(l)); + if (at < 0) at = lines.findIndex((l) => /^import /.test(l)); + if (at >= 0) lines.splice(at, 0, `import ${fqn};`); + } +} + +const byFile = new Map(); +for (const [file, method, attrs] of TARGETS) { + if (!byFile.has(file)) byFile.set(file, []); + byFile.get(file).push({ method, attrs }); +} +if (process.argv.includes('--debug')) { + console.log(`实体索引 ${ENTITY.size},Mapper 索引 ${MAPPER.size}`); + for (const t of ['TtWelfare', 'TtRedPack', 'TtTaskCenter', 'TtBox']) { + console.log(` ${t}: entity=${ENTITY.get(t) || '无'} mapper=${MAPPER.has(t) ? MAPPER.get(t).fqn : '无'}`); + } +} + +let done = 0, skipped = 0; +for (const [rel, list] of byFile) { + const abs = path.join(ROOT, rel); + const lines = fs.readFileSync(abs, 'utf8').split(/\r?\n/); + const edits = []; + const imports = new Set(['com.ruoyi.common.annotation.Log', 'com.ruoyi.common.enums.BusinessType']); + for (const { method, attrs } of list) { + // 找方法签名 + let sig = -1; + for (let i = 0; i < lines.length; i++) { + if (new RegExp(`^\\s*(?:public|protected|private)\\s+[\\w<>\\[\\],\\.\\? ]+\\s+${method}\\s*\\(`).test(lines[i])) { sig = i; break; } + } + if (sig < 0) { console.log(` [跳过] 找不到方法 ${path.basename(rel)}.${method}`); skipped++; continue; } + // 注解块顶部 + let top = sig; + for (let j = sig - 1; j >= 0; j--) { + if (lines[j].trim() === '' || !isBlockMember(lines[j])) break; + top = j; + } + // 已有 @Log 就整块删掉(支持多行) + let start = -1, end = -1; + for (let j = top; j < sig; j++) { + if (/^\s*@Log\(/.test(lines[j])) { + start = j; + let depth = 0; + outer: + for (let k = j; k < sig; k++) { + for (const ch of lines[k]) { + if (ch === '(') depth++; + else if (ch === ')') { depth--; if (depth === 0) { end = k; break outer; } } + } + } + break; + } + } + const indent = (lines[top].match(/^\s*/) || [' '])[0]; + // 自动认实体:入参里第一个"有 @TableName 且有对应 Mapper"的类型 + if (attrs.auto && !attrs.entity) + { + for (const t of methodTypes(lines, sig)) + { + if (ENTITY.has(t) && MAPPER.has(t)) { attrs.entity = t; attrs.mapper = MAPPER.get(t).simple; break; } + } + } + edits.push({ start, end, top, newLines: render(attrs, indent), method, attrs }); + if (attrs.entity && ENTITY.has(attrs.entity)) imports.add(ENTITY.get(attrs.entity)); + if (attrs.entity && MAPPER.has(attrs.entity)) imports.add(MAPPER.get(attrs.entity).fqn); + if (attrs.mapper && IMPORTS[attrs.mapper]) imports.add(IMPORTS[attrs.mapper]); + } + for (const ed of edits.sort((a, b) => b.top - a.top)) { + if (ed.start >= 0) lines.splice(ed.start, ed.end - ed.start + 1, ...ed.newLines); + else lines.splice(ed.top, 0, ...ed.newLines); + console.log(` ${path.basename(rel)}.${ed.method} → ${ed.attrs.menu} / ${ed.attrs.action}` + + `${ed.attrs.entity ? ' diff=' + ed.attrs.entity : ''}${ed.attrs.idField ? ' idField=' + ed.attrs.idField : ''}`); + done++; + } + insertImports(lines, imports); + if (apply) fs.writeFileSync(abs, lines.join('\r\n'), 'utf8'); +} + +console.log(`\n${apply ? 'APPLIED' : 'DRY-RUN'}: 写入 ${done} 个方法,跳过 ${skipped}`); diff --git a/bin/clean.bat b/bin/clean.bat new file mode 100644 index 0000000..578e77d --- /dev/null +++ b/bin/clean.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [信息] 清理工程target生成路径。 +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean + +pause \ No newline at end of file diff --git a/bin/logtool.js b/bin/logtool.js new file mode 100644 index 0000000..7cd667e --- /dev/null +++ b/bin/logtool.js @@ -0,0 +1,43 @@ +// 只读查询/受控清库小工具(连 docker 里的 mysql_db) +// 用法: +// node bin/logtool.js q "select count(*) from sys_oper_log" +// node bin/logtool.js clear-operlog 清理操作日志(先备份到 bin/backup/) +// node bin/logtool.js clear-operlog --yes 跳过确认 +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +const MYSQL = ['exec', 'mysql_db', 'mysql', '-uchangzhou', '-piTSJSPPZM3LSGAPC', '--default-character-set=utf8mb4', '-D', 'changzhou']; + +function sql(statement) { + return execFileSync('docker', [...MYSQL, '-N', '-B', '-e', statement], + { encoding: 'buffer', maxBuffer: 256 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).toString('utf8'); +} + +const [cmd, ...rest] = process.argv.slice(2); + +if (cmd === 'q') { + console.log(sql(rest.join(' '))); +} else if (cmd === 'clear-operlog') { + const before = sql('select count(*) from sys_oper_log').trim(); + console.log(`清空前 sys_oper_log: ${before} 行`); + if (before === '0') process.exit(0); + // 备份成 INSERT 语句,清错了还能捞回来 + const dir = path.resolve(__dirname, 'backup'); + fs.mkdirSync(dir, { recursive: true }); + const dump = execFileSync('docker', [...MYSQL, '-N', '-B', '-e', + 'select concat("insert into sys_oper_log (title,business_type,method,request_method,operator_type,oper_name,dept_name,oper_url,oper_ip,oper_location,oper_param,json_result,status,error_msg,cost_time,oper_time) values (",' + + 'quote(title),",",business_type,",",quote(method),",",quote(request_method),",",operator_type,",",quote(oper_name),",",quote(dept_name),",",quote(oper_url),",",quote(oper_ip),",",quote(oper_location),",",quote(oper_param),",",quote(json_result),",",status,",",quote(error_msg),",",cost_time,",",quote(oper_time),");") from sys_oper_log'], + { encoding: 'buffer', maxBuffer: 256 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).toString('utf8'); + const file = path.join(dir, `sys_oper_log_${new Date().toISOString().replace(/[:.]/g, '-')}.sql`); + fs.writeFileSync(file, dump, 'utf8'); + console.log(`已备份 → ${file}`); + if (!rest.includes('--yes')) { + console.error('要真清空请加 --yes'); + process.exit(2); + } + sql('truncate table sys_oper_log'); + console.log(`清空后 sys_oper_log: ${sql('select count(*) from sys_oper_log').trim()} 行`); +} else { + console.log('用法: node bin/logtool.js q "" | node bin/logtool.js clear-operlog [--yes]'); +} diff --git a/bin/package.bat b/bin/package.bat new file mode 100644 index 0000000..735b65e --- /dev/null +++ b/bin/package.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [信息] 打包Web工程,生成war/jar包文件。 +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean package -Dmaven.test.skip=true + +pause \ No newline at end of file diff --git a/bin/run.bat b/bin/run.bat new file mode 100644 index 0000000..ebeb8f8 --- /dev/null +++ b/bin/run.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [信息] 使用Jar命令运行Web工程。 +echo. + +cd %~dp0 +cd ../ruoyi-admin/target + +set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -jar %JAVA_OPTS% ruoyi-admin.jar --server.port=8081 + +cd bin +pause \ No newline at end of file diff --git a/docker/WINDOWS_QUICKSTART.md b/docker/WINDOWS_QUICKSTART.md new file mode 100644 index 0000000..1888044 --- /dev/null +++ b/docker/WINDOWS_QUICKSTART.md @@ -0,0 +1,93 @@ +# Docker Windows 新手启动指南 + +本地 Windows 开发环境启动 MySQL / Redis / RabbitMQ 三个容器。 + +## 前置条件 + +- 已安装并启动 **Docker Desktop**(任务栏图标变绿才算启动完成) + +## 首次启动(只做一次) + +1. 创建 MySQL 数据卷: + +```powershell +docker volume create changzhou_mysql_data +``` + +2. 启动容器(⚠️ 必须先 cd 到 compose 目录,且必须带两个 -f 参数): + +```powershell +cd d:\changzhou_app\changzhou_backend\docker\compose +docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d +``` + +> ⚠️ **不要只执行 `docker-compose up -d`**! +> 少了 `-f docker-compose.dev.yml` 会导致 Redis 容器找不到 redis.conf, +> 状态一直是 Restarting 无限重启。 +> dev 覆盖文件负责把 `docker/config/redis/redis.dev.conf` 挂载进容器。 + +3. 验证: + +```powershell +docker ps +# 三个容器都应为 Up 状态:mysql_db / redis_cache / rabbitmq_server + +docker exec redis_cache redis-cli ping +# 返回 NOAUTH Authentication required. 是正常的(Redis 已启动且带密码) +``` + +## 端口对照表(宿主机 → 容器) + +| 服务 | 本机地址 | 容器端口 | 账号信息 | +|------|---------|---------|---------| +| MySQL | 127.0.0.1:23306 | 3306 | root/123456,业务库 changzhou | +| Redis | 127.0.0.1:26379 | 6379 | 密码见 redis.dev.conf | +| RabbitMQ | 127.0.0.1:25672 | 5672 | skins/mk,vhost /skins | +| RabbitMQ 管理台 | http://127.0.0.1:35672 | 15672 | skins/mk | + +## 日常操作 + +```powershell +# 都在 docker\compose 目录下执行 +cd d:\changzhou_app\changzhou_backend\docker\compose + +# 启动 +docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d + +# 停止 +docker-compose -f docker-compose.yml -f docker-compose.dev.yml stop + +# 查看状态 +docker ps + +# 查看某个容器日志 +docker logs redis_cache --tail 50 + +# 重建某个容器(比如改了配置) +docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d --force-recreate redis +``` + +## 常见问题 + +### 1. Redis 一直 Restarting + +启动命令少了 `-f docker-compose.dev.yml`,重建即可: + +```powershell +cd d:\changzhou_app\changzhou_backend\docker\compose +docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d --force-recreate redis +``` + +### 2. 提示 `external volume "changzhou_mysql_data" not found` + +```powershell +docker volume create changzhou_mysql_data +``` + +### 3. 提示 `cannot find the file specified` + +没有 cd 到 `docker\compose` 目录就执行了命令,`-f` 的相对路径按当前目录解析。 + +### 4. 和 chengdu 项目切换 + +端口可能冲突,先到另一个项目的 compose 目录执行 `docker-compose stop`,再启动本项目。 diff --git a/docker/compose/deploy.sh b/docker/compose/deploy.sh new file mode 100644 index 0000000..dbb0ae6 --- /dev/null +++ b/docker/compose/deploy.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +# 自动创建 external 数据卷(首次部署时) +for vol in changzhou_mysql_data; do + docker volume inspect "$vol" >/dev/null 2>&1 || docker volume create "$vol" >/dev/null +done + +# 环境选择:ENV=dev 使用开发规格,否则按 CPU 核数自动检测 +if [ "${ENV:-prod}" = "dev" ]; then + SIZE="dev" +else + CPUS=$(nproc) + if [ "$CPUS" -le 16 ]; then + SIZE="16c32g" + else + SIZE="32c64g" + fi +fi + +# 镜像源:默认海外,PROXY=cn 切换国内代理 +if [ "${PROXY:-overseas}" = "cn" ]; then + export COMPOSE_FILE="docker-compose.yml:docker-compose.cn.yml:docker-compose.${SIZE}.yml" +else + export COMPOSE_FILE="docker-compose.yml:docker-compose.${SIZE}.yml" +fi + +echo "Env: ${ENV:-prod} | Size: ${SIZE} | Proxy: ${PROXY:-cn}" +exec docker compose "$@" diff --git a/docker/compose/docker-compose.16c32g.yml b/docker/compose/docker-compose.16c32g.yml new file mode 100644 index 0000000..7e4e31b --- /dev/null +++ b/docker/compose/docker-compose.16c32g.yml @@ -0,0 +1,18 @@ +services: + mysql: + build: + args: + MYSQL_CONF: my.cnf.16c32g + shm_size: 128m + mem_limit: 8g + cpus: 4 + + redis: + volumes: + - ../config/redis/redis.16c32g.conf:/usr/local/etc/redis/redis.conf:ro + mem_limit: 2g + cpus: 1 + + rabbitmq: + mem_limit: 2g + cpus: 1 diff --git a/docker/compose/docker-compose.32c64g.yml b/docker/compose/docker-compose.32c64g.yml new file mode 100644 index 0000000..d68df7e --- /dev/null +++ b/docker/compose/docker-compose.32c64g.yml @@ -0,0 +1,18 @@ +services: + mysql: + build: + args: + MYSQL_CONF: my.cnf.32c64g + shm_size: 256m + mem_limit: 12g + cpus: 8 + + redis: + volumes: + - ../config/redis/redis.32c64g.conf:/usr/local/etc/redis/redis.conf:ro + mem_limit: 4g + cpus: 2 + + rabbitmq: + mem_limit: 4g + cpus: 2 diff --git a/docker/compose/docker-compose.cn.yml b/docker/compose/docker-compose.cn.yml new file mode 100644 index 0000000..1c40807 --- /dev/null +++ b/docker/compose/docker-compose.cn.yml @@ -0,0 +1,11 @@ +services: + mysql: + build: + args: + MYSQL_BASE_IMAGE: m.daocloud.io/docker.io/mysql:8.0.24 + + redis: + image: m.daocloud.io/docker.io/redis:latest + + rabbitmq: + image: m.daocloud.io/docker.io/rabbitmq:3.9-management diff --git a/docker/compose/docker-compose.dev.yml b/docker/compose/docker-compose.dev.yml new file mode 100644 index 0000000..87ceff8 --- /dev/null +++ b/docker/compose/docker-compose.dev.yml @@ -0,0 +1,15 @@ +services: + mysql: + build: + args: + MYSQL_CONF: my.cnf.dev + shm_size: 64m + mem_limit: 1g + + redis: + volumes: + - ../config/redis/redis.dev.conf:/usr/local/etc/redis/redis.conf:ro + mem_limit: 256m + + rabbitmq: + mem_limit: 256m diff --git a/docker/compose/docker-compose.yml b/docker/compose/docker-compose.yml new file mode 100644 index 0000000..0e23938 --- /dev/null +++ b/docker/compose/docker-compose.yml @@ -0,0 +1,89 @@ +services: + # MySQL 8.0.24 服务 + mysql: + build: + context: ../config/mysql + dockerfile: Dockerfile + args: + MYSQL_BASE_IMAGE: mysql:8.0.24 + MYSQL_CONF: my.cnf.16c32g + container_name: mysql_db + restart: unless-stopped + environment: + TZ: Asia/Shanghai + MYSQL_ROOT_PASSWORD: 123456 + MYSQL_DATABASE: changzhou + MYSQL_USER: changzhou + MYSQL_PASSWORD: iTSJSPPZM3LSGAPC + ports: + - "127.0.0.1:23306:3306" + volumes: + - mysql_data:/var/lib/mysql + logging: + driver: json-file + options: + max-size: "50m" + max-file: "3" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p123456"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - changzhou_app_network + + # Redis 服务 + redis: + image: redis:latest + container_name: redis_cache + restart: unless-stopped + command: redis-server /usr/local/etc/redis/redis.conf + ports: + - "127.0.0.1:26379:6379" + logging: + driver: json-file + options: + max-size: "50m" + max-file: "3" + networks: + - changzhou_app_network + + # RabbitMQ 3.9 服务 + rabbitmq: + image: rabbitmq:3.9-management + container_name: rabbitmq_server + restart: unless-stopped + environment: + RABBITMQ_DEFAULT_USER: skins + RABBITMQ_DEFAULT_PASS: mk + ports: + - "127.0.0.1:25672:5672" + - "127.0.0.1:35672:15672" + command: > + bash -c "rabbitmq-server & + sleep 20 && + rabbitmqctl add_vhost /skins && + rabbitmqctl set_permissions -p /skins skins '.*' '.*' '.*' && + rabbitmqadmin --vhost=/skins --username=skins --password=mk declare queue name=notice_queue durable=true && + rabbitmqadmin --vhost=/skins --username=skins --password=mk declare queue name=dlk_queue durable=true && + rabbitmqadmin --vhost=/skins --username=skins --password=mk declare queue name=delivery_queue durable=true && + wait" + logging: + driver: json-file + options: + max-size: "50m" + max-file: "3" + networks: + - changzhou_app_network + +# 定义数据卷(external 固定卷名,不随目录变化,防止换目录丢数据) +volumes: + mysql_data: + external: true + name: changzhou_mysql_data + +# 定义网络 +networks: + changzhou_app_network: + driver: bridge diff --git a/docker/compose/migrate-mysql-volume.sh b/docker/compose/migrate-mysql-volume.sh new file mode 100644 index 0000000..668f8f6 --- /dev/null +++ b/docker/compose/migrate-mysql-volume.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# MySQL 数据卷迁移脚本 +# 将旧自动命名卷的数据复制到 external 卷 changzhou_mysql_data +# 用法:bash migrate-mysql-volume.sh [旧卷名] +# 不传参数时自动检测 docker_changzhou_mysql_data 和 docker-cn_changzhou_mysql_data + +set -e + +NEW_VOL="changzhou_mysql_data" +IMAGE="mysql:8.0.24" + +# 自动检测旧卷名 +if [ -n "$1" ]; then + OLD_VOL="$1" +else + for candidate in docker_changzhou_mysql_data docker-cn_changzhou_mysql_data; do + if docker volume inspect "$candidate" >/dev/null 2>&1; then + OLD_VOL="$candidate" + break + fi + done +fi + +if [ -z "$OLD_VOL" ]; then + echo "ERROR: 未找到旧数据卷,请手动指定:bash migrate-mysql-volume.sh <旧卷名>" + echo "可用的卷列表:" + docker volume ls --format ' {{.Name}}' | grep -i mysql || echo " (无 mysql 相关卷)" + exit 1 +fi + +# 检查新卷是否存在 +if ! docker volume inspect "$NEW_VOL" >/dev/null 2>&1; then + echo "创建 external 卷: $NEW_VOL" + docker volume create "$NEW_VOL" +fi + +# 检查旧卷数据大小 +OLD_SIZE=$(docker run --rm -v "${OLD_VOL}:/data" "$IMAGE" sh -c "du -sh /data" 2>/dev/null | awk '{print $1}') +echo "旧卷 $OLD_VOL 数据量: $OLD_SIZE" + +# 停掉 MySQL 容器(如果在运行) +if docker inspect mysql_db >/dev/null 2>&1; then + echo "停止 MySQL 容器..." + docker stop mysql_db 2>/dev/null || true +fi + +# 清空新卷(避免数据叠加) +echo "清空新卷 $NEW_VOL ..." +docker volume rm "$NEW_VOL" >/dev/null +docker volume create "$NEW_VOL" >/dev/null + +# 复制数据 +echo "开始迁移 $OLD_VOL -> $NEW_VOL ..." +docker run --rm \ + -v "${OLD_VOL}:/from" \ + -v "${NEW_VOL}:/to" \ + "$IMAGE" \ + sh -c "cp -a /from/. /to/" + +# 验证 +NEW_SIZE=$(docker run --rm -v "${NEW_VOL}:/data" "$IMAGE" sh -c "du -sh /data" 2>/dev/null | awk '{print $1}') +echo "新卷 $NEW_VOL 数据量: $NEW_SIZE" +echo "迁移完成!旧卷 $OLD_VOL 保留作为备份。" diff --git a/docker/config/mysql/Dockerfile b/docker/config/mysql/Dockerfile new file mode 100644 index 0000000..d7434a4 --- /dev/null +++ b/docker/config/mysql/Dockerfile @@ -0,0 +1,5 @@ +ARG MYSQL_BASE_IMAGE=mysql:8.0.24 +FROM ${MYSQL_BASE_IMAGE} + +ARG MYSQL_CONF=my.cnf.16c32g +COPY ${MYSQL_CONF} /etc/mysql/conf.d/my.cnf diff --git a/docker/config/mysql/my.cnf.16c32g b/docker/config/mysql/my.cnf.16c32g new file mode 100644 index 0000000..9f24765 --- /dev/null +++ b/docker/config/mysql/my.cnf.16c32g @@ -0,0 +1,32 @@ +[mysqld] +# ===== 网络 ===== +skip-host-cache +skip-name-resolve + +# ===== InnoDB 核心性能(16C/32G 服务器 / SSD) ===== +innodb_buffer_pool_size = 5G +innodb_flush_method = O_DIRECT +innodb_flush_log_at_trx_commit = 2 +innodb_log_file_size = 512M +innodb_log_files_in_group = 2 +innodb_log_buffer_size = 32M +innodb_io_capacity = 1000 +innodb_io_capacity_max = 4000 +innodb_read_io_threads = 4 +innodb_write_io_threads = 4 + +# ===== 禁用 binlog(无需主从复制) ===== +skip-log-bin + +# ===== 连接数 ===== +max_connections = 200 +max_user_connections = 180 + +# ===== 临时表 ===== +tmp_table_size = 32M +max_heap_table_size = 32M + +# ===== 慢查询日志 ===== +slow_query_log = ON +slow_query_log_file = /var/lib/mysql/slow.log +long_query_time = 1 diff --git a/docker/config/mysql/my.cnf.32c64g b/docker/config/mysql/my.cnf.32c64g new file mode 100644 index 0000000..3321a7a --- /dev/null +++ b/docker/config/mysql/my.cnf.32c64g @@ -0,0 +1,32 @@ +[mysqld] +# ===== 网络 ===== +skip-host-cache +skip-name-resolve + +# ===== InnoDB 核心性能(32C/64G 服务器 / SSD) ===== +innodb_buffer_pool_size = 8G +innodb_flush_method = O_DIRECT +innodb_flush_log_at_trx_commit = 2 +innodb_log_file_size = 1G +innodb_log_files_in_group = 2 +innodb_log_buffer_size = 64M +innodb_io_capacity = 1000 +innodb_io_capacity_max = 4000 +innodb_read_io_threads = 8 +innodb_write_io_threads = 8 + +# ===== 禁用 binlog(无需主从复制) ===== +skip-log-bin + +# ===== 连接数 ===== +max_connections = 300 +max_user_connections = 280 + +# ===== 临时表 ===== +tmp_table_size = 64M +max_heap_table_size = 64M + +# ===== 慢查询日志 ===== +slow_query_log = ON +slow_query_log_file = /var/lib/mysql/slow.log +long_query_time = 1 diff --git a/docker/config/mysql/my.cnf.dev b/docker/config/mysql/my.cnf.dev new file mode 100644 index 0000000..5ec331b --- /dev/null +++ b/docker/config/mysql/my.cnf.dev @@ -0,0 +1,31 @@ +[mysqld] +# ===== 网络 ===== +skip-host-cache +skip-name-resolve + +# ===== InnoDB 核心性能(开发环境) ===== +innodb_buffer_pool_size = 256M +innodb_flush_log_at_trx_commit = 2 +innodb_log_file_size = 128M +innodb_log_files_in_group = 2 +innodb_log_buffer_size = 8M +innodb_io_capacity = 1000 +innodb_io_capacity_max = 2000 +innodb_read_io_threads = 4 +innodb_write_io_threads = 4 + +# ===== 禁用 binlog ===== +skip-log-bin + +# ===== 连接数 ===== +max_connections = 30 +max_user_connections = 25 + +# ===== 临时表 ===== +tmp_table_size = 32M +max_heap_table_size = 32M + +# ===== 慢查询日志 ===== +slow_query_log = ON +slow_query_log_file = /var/lib/mysql/slow.log +long_query_time = 1 diff --git a/docker/config/redis/redis.16c32g.conf b/docker/config/redis/redis.16c32g.conf new file mode 100644 index 0000000..6cf3d49 --- /dev/null +++ b/docker/config/redis/redis.16c32g.conf @@ -0,0 +1,46 @@ +# Redis configuration file +bind 0.0.0.0 +port 6379 +timeout 300 +tcp-keepalive 300 +daemonize no +pidfile /var/run/redis.pid +loglevel notice +logfile stdout +databases 16 +save 900 1 +save 300 10 +save 60 10000 +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +dir /data +requirepass FireSkins@99999 +masterauth FireSkins@99999 +rename-command FLUSHDB "" +rename-command FLUSHALL "" +maxclients 10000 +maxmemory 1gb +maxmemory-policy volatile-lru +appendonly no +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +lua-time-limit 5000 +slowlog-log-slower-than 10000 +slowlog-max-len 128 +hash-max-listpack-entries 512 +hash-max-listpack-value 64 +list-max-listpack-size -2 +set-max-intset-entries 512 +set-max-listpack-entries 128 +set-max-listpack-value 64 +zset-max-listpack-entries 128 +zset-max-listpack-value 64 +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +hz 10 diff --git a/docker/config/redis/redis.32c64g.conf b/docker/config/redis/redis.32c64g.conf new file mode 100644 index 0000000..ed73970 --- /dev/null +++ b/docker/config/redis/redis.32c64g.conf @@ -0,0 +1,46 @@ +# Redis configuration file +bind 0.0.0.0 +port 6379 +timeout 300 +tcp-keepalive 300 +daemonize no +pidfile /var/run/redis.pid +loglevel notice +logfile stdout +databases 16 +save 900 1 +save 300 10 +save 60 10000 +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +dir /data +requirepass FireSkins@99999 +masterauth FireSkins@99999 +rename-command FLUSHDB "" +rename-command FLUSHALL "" +maxclients 10000 +maxmemory 2gb +maxmemory-policy volatile-lru +appendonly no +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +lua-time-limit 5000 +slowlog-log-slower-than 10000 +slowlog-max-len 128 +hash-max-listpack-entries 512 +hash-max-listpack-value 64 +list-max-listpack-size -2 +set-max-intset-entries 512 +set-max-listpack-entries 128 +set-max-listpack-value 64 +zset-max-listpack-entries 128 +zset-max-listpack-value 64 +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +hz 10 diff --git a/docker/config/redis/redis.dev.conf b/docker/config/redis/redis.dev.conf new file mode 100644 index 0000000..2d55033 --- /dev/null +++ b/docker/config/redis/redis.dev.conf @@ -0,0 +1,46 @@ +# Redis configuration file +bind 0.0.0.0 +port 6379 +timeout 300 +tcp-keepalive 300 +daemonize no +pidfile /var/run/redis.pid +loglevel notice +logfile stdout +databases 16 +save 900 1 +save 300 10 +save 60 10000 +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +dir /data +requirepass FireSkins@99999 +masterauth FireSkins@99999 +rename-command FLUSHDB "" +rename-command FLUSHALL "" +maxclients 10000 +maxmemory 128mb +maxmemory-policy volatile-lru +appendonly no +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +lua-time-limit 5000 +slowlog-log-slower-than 10000 +slowlog-max-len 128 +hash-max-listpack-entries 512 +hash-max-listpack-value 64 +list-max-listpack-size -2 +set-max-intset-entries 512 +set-max-listpack-entries 128 +set-max-listpack-value 64 +zset-max-listpack-entries 128 +zset-max-listpack-value 64 +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +hz 10 diff --git a/docker/readme.md b/docker/readme.md new file mode 100644 index 0000000..dc485d3 --- /dev/null +++ b/docker/readme.md @@ -0,0 +1,69 @@ +# Docker 基础设施 + +## 目录结构 + +``` +docker/ +├── compose/ # Docker Compose 文件 + 脚本 +│ ├── docker-compose.yml # 主文件 (MySQL + Redis + RabbitMQ) +│ ├── docker-compose.cn.yml # 国内镜像源 override +│ ├── docker-compose.dev.yml # 开发环境资源限制 +│ ├── docker-compose.16c32g.yml # 16C/32G 服务器资源规格 +│ ├── docker-compose.32c64g.yml # 32C/64G 服务器资源规格 +│ ├── deploy.sh # 智能部署脚本 +│ └── migrate-mysql-volume.sh # 数据卷迁移脚本 +├── config/ # 服务配置文件 +│ ├── mysql/ +│ │ ├── Dockerfile # 参数化 MySQL 镜像 +│ │ ├── my.cnf.dev # 开发环境配置 +│ │ ├── my.cnf.16c32g # 16C/32G 配置 +│ │ └── my.cnf.32c64g # 32C/64G 配置 +│ └── redis/ +│ ├── redis.dev.conf # 开发环境配置 +│ ├── redis.16c32g.conf # 16C/32G 配置 +│ └── redis.32c64g.conf # 32C/64G 配置 +└── readme.md # 本文件 +``` + +## 快速使用 + +```bash +cd docker/compose + +# 自动检测环境并启动(默认:国内镜像 + 自动检测 CPU 规格) +./deploy.sh up -d + +# 开发环境 +ENV=dev ./deploy.sh up -d + +# 海外镜像源 +PROXY=overseas ./deploy.sh up -d + +# 停止 +./deploy.sh stop + +# 删除容器 +./deploy.sh down + +# 重新创建某个容器 +./deploy.sh up -d --build --force-recreate rabbitmq +``` + +## 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `ENV` | `prod` | `dev` 使用开发资源规格,`prod` 按 CPU 核数自动检测 | +| `PROXY` | `cn` | `cn` 使用国内镜像源,`overseas` 使用官方源 | +| `JVM_PROFILE` | (自动) | 可手动指定 `16c32g` 或 `32c64g` | + +## 首次部署:数据卷迁移 + +如果之前使用旧版 docker-compose(自动命名卷),需要迁移数据到 external 卷: + +```bash +cd docker/compose +bash migrate-mysql-volume.sh +# 或手动指定旧卷名 +bash migrate-mysql-volume.sh docker_chengdu_mysql_data +``` diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..6998f8e --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,201 @@ +# Skins 业务后端 — 详细部署文档 + +> 适用项目:chengdu-backend(基于若依 RuoYi 开发,Spring Boot 4.0.3 + JDK 21) +> 适用场景:本地开发、测试环境、生产环境 +> 项目根 README 见 [README.md](README.md),本文为补充的详细部署手册 + +## 1. 部署架构总览 + +- 后端单进程:ruoyi-admin.jar,监听28081 +- 基础设施:MySQL 8.x、Redis、RabbitMQ(参考 docker/docker-compose.yml) +- 前端:ruoyi-ui(Vue2 经典管理端),promo-ui2(Vue3 主播后台),cocos前端 + + +## 2. 环境准备 + +### 2.1 必备软件 + +JDK 21(项目使用 /root/jdk-21.0.10) 必须为 JDK 21 +Maven 3.6.x(/root/apache-maven-3.6.3) + +### 2.2 安装示例 + +# JDK 21 +cd /root +# 上传 jdk-21.x.tar.gz 到 /root +tar -xzf jdk-21.0.10_linux-x64_bin.tar.gz +mv jdk-21.0.10 /root/jdk-21.0.10 + +# Maven +cd /root +tar -xzf apache-maven-3.6.3-bin.tar.gz + +# 配置全局环境变量(在脚本内配置,见 ry.sh / package.sh) +cat >> /etc/profile <<'EOF' +export JAVA_HOME=/root/jdk-21.0.10 +export MAVEN_HOME=/root/apache-maven-3.6.3 +export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH +EOF +source /etc/profile + +# 验证 +java -version +mvn -v + + +## 3. 基础设施部署(MySQL / Redis / RabbitMQ) + +### 3.1 使用仓库自带 docker-compose + +仓库 docker/ 目录下已提供文件,采用 compose override 模式,通过 deploy.sh 自动选择镜像源和资源规格。 + +bash +cd chengdu-backend/docker/compose + +# 自动检测环境并启动(默认:国内镜像 + 自动检测 CPU 规格) +./deploy.sh up -d + +# 开发环境 +ENV=dev ./deploy.sh up -d + +# 海外镜像源(如国内镜像不可用) +PROXY=overseas ./deploy.sh up -d + +# 停止 / 删除 +./deploy.sh stop +./deploy.sh down + +容器常用操作: + +./deploy.sh up -d # 启动 +./deploy.sh stop # 停止 + +### 3.2 首次部署:数据卷迁移 + +如果之前使用旧版 docker-compose(自动命名卷),需要迁移数据到 external 卷: + +bash +cd docker/compose +bash migrate-mysql-volume.sh + + +## 4. 后端配置yml说明(Spring Profile) + +入口配置文件:[ruoyi-admin/src/main/resources/application.yml](ruoyi-admin/src/main/resources/application.yml) + +### 4.1 Profile 文件 + +| Profile | 文件 +| dev | application-dev.yml +| pro | application-pro.yml + +### 4.2 生产关键配置(节选自 application-pro.yml) + +yaml +ruoyi: + profile: /www/static # 上传文件存储目录 + demoEnabled: true + +spring: + datasource: + druid: + master: + url: jdbc:mysql://127.0.0.1:23306/chengdu?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: chengdu + password: <数据库密码> + data: + redis: + host: 127.0.0.1 + port: 26379 + password: + rabbitmq: + host: 127.0.0.1 + port: 25672 + username: skins + password: + virtual-host: /skins + +token: + secret: <强随机 32 位以上字符串> # JWT 密钥,必须修改 + expireTime: 1440 # 分钟 + +### 4.3 项目文件上传目录 + +ruoyi.profile 指上传文件物理路径(默认 /www/static),创建并赋权(宝塔直接手动创建): + + +## 5. 编译与打包 +### 5.1 服务器编译(脚本封装) + +仓库根目录提供 [package.sh](package.sh): + +package.sh +主要工作: +1. 检查 mvn 和 java 命令是否可用。 +2. git pull 拉取最新代码。 +3. 调用 mvn clean package -Dmaven.test.skip=true。 +4. 将 ruoyi-admin/target/ruoyi-admin.jar 复制到仓库根目录 ./ruoyi-admin.jar,供 ry.sh 启动。 + + +## 6. 一键部署脚本(宝塔推荐) + +仓库根 [publish.sh](publish.sh) 封装「备份旧jar → 打包 → 重启」完整流程: + +bash执行流程 +cd /www/chengdu_app/chengdu-backend +./publish.sh + +执行解释步骤: +1. 备份旧 jar:cp ruoyi-admin.jar ruoyi-admin.jar.bak +2. 打包:调用 ./package.sh(含 git pull + mvn package) +3. 重启:./ry.sh stop && ./ry.sh start + +ry.sh 会自动检测 CPU 核数选择 JVM 规格(16c32g: 10g堆 / 32c64g: 22g堆),使用 G1GC 并开启 GC 日志。可通过 JVM_PROFILE 环境变量覆盖规格,SPRING_PROFILE 覆盖 Spring Profile(默认 pro)。 + + +## 7. 前端工程部署 + +### 7.1 ruoyi-ui(Vue2 经典管理端) + +bash +cd ruoyi-ui +npm install +npm run build:prod # 将dist目录下文件打包 +# 将 dist压缩包 上传到/www/wwwroot/静态目录 + + +### 7.2 promo-ui2(Vue3 推广后台) + +bash +cd promo-ui2 +pnpm install +pnpm build:pro # 将dist目录下文件打包 +# 将 dist压缩包 上传到/www/wwwroot/静态目录 + + +## 8. 宝塔端口放行 + +| 端口 | 用途 +| 80 / 443 | Nginx +| 28081 | 后端 Spring Boot +| 23306 | MySQL +| 26379 | Redis +| 25672 / 35672 | RabbitMQ AMQP + +执行流程 +宝塔 → 安全 → 放行 80/443/26379/23306/28081 等等所需要的端口号,其余端口可在宝塔安全设置添加和修改。 + +## 9. 日志与监控 + +### 9.1 日志文件 + +| 路径 | 内容 | +| nohup.out | 启动 + 控制台日志(由 ry.sh 输出) | +| /www/ruoyi/logs/sys-info.log | 系统日志(logback,按天滚动,保留60天) | +| /www/ruoyi/logs/sys-error.log | 错误日志(logback,按天滚动,保留60天) | +| /www/ruoyi/logs/gc.log | GC 日志(滚动 10 个文件,每个 50M) | + +实时跟踪: +bash脚本 +tail -f nohup.out + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6ff8cbf --- /dev/null +++ b/docs/README.md @@ -0,0 +1,181 @@ +# Skins 业务后端 + + +基于 **若依(RuoYi)4.8.2** 扩展的多模块 Java 后端,集成 CSGO/饰品相关业务(用户、玩法、第三方支付与回调、定时任务、推广后台等)。仓库同时包含两套管理端前端工程。 + +克隆后请在项目根目录执行 `mvn -v` 确认 JDK 21 与 Maven 可用,再按下文完成数据库与中间件初始化。 + +--- + +## 技术栈概览 + +| 层级 | 技术 | +|------|------| +| 运行时 | Java **21** | +| 框架 | Spring Boot **4.0.3**(启用虚拟线程 `spring.threads.virtual.enabled`) | +| Web 与安全 | **Spring Security 6**(无状态会话)、JWT(`Authorization` 请求头)、若依式菜单/权限模型 | +| 持久层 | MyBatis / MyBatis-Plus、PageHelper、Druid(含多数据源扩展) | +| 数据库 | MySQL | +| 缓存 | Redis(Spring Data Redis + Lettuce) | +| 消息 | RabbitMQ(业务侧配置虚拟主机等,详见各 profile) | +| 实时 | WebSocket(如 `/ws/**` 等路径,与安全放行配置保持一致) | +| API 文档 | **Springfox 3.x**(主用,Swagger / OAS3 分组);另引入 **springdoc-openapi** Starter,可与 Springfox 并行存在 | + +--- + +## 仓库结构 + +``` +chengdu-backend/ +├── ruoyi-admin/ # Web 入口(Spring Boot 启动模块) +├── ruoyi-framework/ # 安全、过滤器、资源配置等封装 +├── ruoyi-system/ # 系统管理(用户、角色、菜单等) +├── ruoyi-common/ # 通用工具、常量、MQ 配置等 +├── ruoyi-quartz/ # 定时任务(若依 Quartz 集成) +├── ruoyi-generator/ # 代码生成 +├── skins-model/ # 业务领域模型(实体等) +├── skins-service/ # 业务服务聚合 POM(子模块见下) +│ ├── service-admin/ # 运营管理侧接口 +│ ├── service-user/ # 用户侧接口 +│ ├── service-playingmethod/ # 玩法相关 +│ ├── service-thirdparty/ # 第三方对接 +│ └── service-task/ # 任务与调度配合 +├── skins-promo/ # 推广后台 API(路径前缀多为 /promo) +├── ruoyi-ui/ # 管理端 Vue2 + Element UI(若依经典前端) +├── promo-ui2/ # 推广/运营后台 Vue3 + Element Plus + Vite +└── docker/ # MySQL / Redis / RabbitMQ 编排 +``` + +### 启动类 + +后端入口:`ruoyi-admin` 模块中的 `com.ruoyi.RuoYiApplication`。使用 `@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })` 排除 Spring Boot 默认数据源自动配置(Spring Boot 4 下为 `org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration`),由若依多数据源与 Druid 配置接管。 + +--- + +## 运行环境与配置 + +### 必备依赖 + +- JDK **21** +- Maven **3.6+** +- MySQL **8.x**(库名与账号见各 profile;本地可参考 `docker/docker-compose.yml`) +- Redis +- RabbitMQ(若启用了 MQ 监听或相关消费者则需可用) + +根 `pom.xml` 已配置 **阿里云 Maven 公共仓库**,便于国内环境拉取依赖。 + +### Spring Profile + +`ruoyi-admin/src/main/resources/application.yml` 中通过 `spring.profiles.active` 切换环境,配套文件: + +- `application-dev.yml` — 本地/开发 +- `application-test.yml` — 测试 +- `application-pro.yml` — 生产 + +**重要:** 上述 profile 文件中常含数据库口令、第三方支付与短信密钥等敏感信息。**勿将真实密钥提交到公开仓库**;上线请改用环境变量、密钥管理系统或独立于仓库的配置挂载。 + +### 默认服务端口与上下文 + +- HTTP 端口:**28081**(见根配置 `ruoyi-admin/src/main/resources/application.yml` 中的 `server.port`;各环境可被 profile 或部署参数覆盖,默认绑定 `0.0.0.0`) +- 上下文路径:`/`(与管理端、`VUE_APP_BASE_API` 及反向代理前缀需一致) + +### 对外匿名接口(简述) + +与安全配置对齐的常见分类包括:`/login`、`/register`、`/captchaImage`,以及业务侧的 `/api/**`、`/admin/**`、WebSocket、部分 Swagger 与 Druid 监控路径等。具体请以 `ruoyi-framework` 中 **SecurityFilterChain** 与白名单配置为准。 + +--- + +## 本地启动后端 + +**方式一:先打包再在启动模块运行** + +在项目根目录执行: + +```bash +mvn clean package -DskipTests +cd ruoyi-admin +mvn spring-boot:run +``` + +**方式二:根目录一键拉起(会解析 `ruoyi-admin` 所需模块依赖)** + +```bash +mvn -pl ruoyi-admin -am spring-boot:run -DskipTests +``` + +也可在 IDE 中直接运行 `RuoYiApplication`。 + +首次运行前请: + +1. 创建 MySQL 库并完成若依及业务所需的 SQL 初始化(以团队提供的脚本为准)。 +2. 按所选 profile 修改数据源、Redis、RabbitMQ 等连接信息。 + +--- + +## Docker 基础设施 + +目录:`docker/` + +- `docker-compose.yml`:MySQL(含自定义镜像)、Redis、RabbitMQ(管理端口及初始化示例)。 +- `docker/docker-cn/`:面向大陆镜像拉取环境的编排变体。 + +常用操作见 **`docker/readme.md`**(如单独重建某个服务等)。 + +--- + +## 前端工程 + +### ruoyi-ui(Vue 2) + +- 技术栈:Vue CLI、Element UI(`package.json` 声明 Node `>=8.9`,实际开发建议 Node **16 LTS** 或 **18 LTS**,并按需开启 OpenSSL 兼容)。 +- 脚本见 `ruoyi-ui/package.json`(Windows 脚本含 `set NODE_OPTIONS=--openssl-legacy-provider`;在 macOS/Linux 上需 `export NODE_OPTIONS=...` 或使用等价脚本)。 +- 开发与打包:`npm run dev`、`npm run build:prod` 等。 +- 系统工具内「Swagger」页当前 iframe 指向 **`/doc.html`**;若后端未部署对应聚合页,请改为与下文 **API 文档** 中实际地址一致(例如 `/swagger-ui.html`)。 + +### promo-ui2(Vue 3) + +- 技术栈:Vue 3、Vite、TypeScript、Element Plus(基于 vue-element-plus-admin 演进);要求 **Node ≥ 18**、`pnpm ≥ 8.1`(以 `promo-ui2/package.json` 中 `engines` 为准)。 +- 推荐使用 **pnpm**:`pnpm install`、`pnpm dev`、`pnpm build:pro` 等。 +- 详细说明见 `promo-ui2/README.zh-CN.md`。 + +前端请求的接口基地址需指向本后端;若经 Nginx 网关,注意与团队约定的前缀(如 `/prod-api`)一致。 + +--- + +## 业务模块说明(简述) + +| 模块 | 职责 | +|------|------| +| **skins-model** | 共享实体与领域常量 | +| **service-user** | 用户相关业务 | +| **service-admin** | 运营管理侧业务 | +| **service-playingmethod** | 玩法相关逻辑 | +| **service-thirdparty** | 第三方对接(支付、交易平台回调等,密钥在各 profile 中) | +| **service-task** | 异步任务 / 与 Quartz 等协同 | +| **skins-promo** | 推广后台:登录注册、流水与佣金、充值卡等(REST 前缀示例:`/promo`) | + +--- + +## API 文档 + +- **Springfox(当前主要使用)**:启动后访问(端口与 `server.port` 一致,仓库默认 **28081**) + `http://localhost:28081/swagger-ui.html` + 在页面右上角切换分组(如 `user`、`thirdParty`、`playingMethod`、`task`、`promo` 等;与 `SwaggerConfig` 中 Docket 一致)。OpenAPI JSON 路径形如 `//api-docs`(Spring Security 已对 `/*/api-docs` 等放行,以实际配置为准)。 +- **springdoc-openapi**:`ruoyi-admin` 已引入依赖;若单独启用 SpringDoc UI,需确保 **Security** 对 `/swagger-ui/**`、`/v3/api-docs` 等路径放行,避免 401。 + +--- + +## 版本信息 + +- 父 POM:`com.ruoyi:ruoyi:4.8.2`(`spring-boot.version` **4.0.3**,`springdoc.version` **3.0.2**,`swagger` / Springfox **3.0.0**) +- 若依官网参考: + +--- + +## 安全建议 + +1. 轮换并私有化所有第三方(支付宝、聚合支付、短信等)密钥;生产环境勿使用仓库内示例明文。 +2. 修改 JWT `token.secret` 与 Druid 监控口令等非默认值。 +3. 按需收紧 Druid、`/actuator`(若启用)及 Swagger 相关路径的访问范围。 + +如有与本说明不一致之处,以当前仓库代码及团队部署文档为准。 diff --git a/docs/cleanup_records.sql b/docs/cleanup_records.sql new file mode 100644 index 0000000..d4d1e08 --- /dev/null +++ b/docs/cleanup_records.sql @@ -0,0 +1,160 @@ +-- ============================================================ +-- 常州项目 数据库清理脚本(TRUNCATE 各类业务记录表) +-- 适用库:changzhou(127.0.0.1:23306,docker 容器 mysql_db;dev/pro 配置一致) +-- 对照时间:2026-09-17,库内共 163 张表,本脚本覆盖全部业务表 +-- ⚠️ 高危操作!执行前务必完整备份数据库! +-- ⚠️ TRUNCATE 不可回滚,且会重置自增主键 +-- ⚠️ 若某张表在你的环境中不存在,请注释掉对应行(TRUNCATE 无 IF EXISTS) +-- ============================================================ + +-- 关闭外键检查,避免因约束顺序报错 +SET FOREIGN_KEY_CHECKS = 0; + +-- ============================================================ +-- Part 1: 系统日志表 +-- ============================================================ +TRUNCATE TABLE sys_logininfor; -- 登录日志 +TRUNCATE TABLE sys_oper_log; -- 操作日志 +TRUNCATE TABLE sys_job_log; -- 定时任务日志 + +-- 清用户 +TRUNCATE TABLE tt_user; + +-- ============================================================ +-- Part 2: 财务流水 +-- ============================================================ +TRUNCATE TABLE tt_user_blend_ercash; -- 余额/积分变动主流水 +TRUNCATE TABLE tt_user_blend_ercash_archive; -- 流水归档表 +TRUNCATE TABLE tt_user_amount_records; -- 金币流水明细 +TRUNCATE TABLE tt_user_credits_records; -- 积分流水明细 +TRUNCATE TABLE tt_user_source_report; -- 用户来源日报表(累计流水统计) +TRUNCATE TABLE tt_user_daily_turnover; -- 用户每日流水统计(VIP 返利/排行榜口径) +TRUNCATE TABLE tt_recharge_record; -- 充值记录 +TRUNCATE TABLE tt_recharge_card; -- 充值卡密 +TRUNCATE TABLE tt_blank_card_key; -- 空白卡密(后台生成) +TRUNCATE TABLE promo_card_key; -- 卡商卡密 +TRUNCATE TABLE promo_cdkey; -- 推广CDKEY创建记录表 +TRUNCATE TABLE promo_cdkey_redeem; -- 推广CDKEY兑换记录表 +TRUNCATE TABLE tt_order; -- 订单 +TRUNCATE TABLE tianxin_order; -- 天心支付订单 +TRUNCATE TABLE tt_loss_rebate; -- 亏损返水结算明细(回血红包) +TRUNCATE TABLE tt_comp_record; -- 补偿值记录(user_id + box_id + comp_amount) +TRUNCATE TABLE tt_promotion_update; -- 推广上下级关系变更记录 +TRUNCATE TABLE tt_fight_ranking_reward; -- 对战排行榜奖励发放记录 +TRUNCATE TABLE tt_recharge_ranking_reward; -- 充值排行榜奖励发放记录 +TRUNCATE TABLE tt_user_sub_account; -- 主号-副号关联 +TRUNCATE TABLE tt_user_avatar; -- 用户头像 + +-- ============================================================ +-- Part 3: 游戏记录 +-- ============================================================ + +-- 开箱 +TRUNCATE TABLE tt_box_records; +TRUNCATE TABLE tt_user_box_open_chance; -- 用户开箱机会 +TRUNCATE TABLE tt_box_third_explosive_user; -- 第三方爆率用户记录 + +-- 对战(tt_fight_round / tt_fight_result 为 DB 标注的废弃表,0 行) +TRUNCATE TABLE tt_fight; +ALTER TABLE tt_fight AUTO_INCREMENT = 1000001; +TRUNCATE TABLE tt_fight_user; +TRUNCATE TABLE tt_fight_round; +TRUNCATE TABLE tt_fight_result; + +-- Roll房 +TRUNCATE TABLE tt_roll; +TRUNCATE TABLE tt_roll_user; +TRUNCATE TABLE tt_roll_user_prize; +TRUNCATE TABLE tt_time_roll; -- 定时Roll房主表 +TRUNCATE TABLE tt_time_roll_user; -- 定时Roll房参与记录 + +-- 幸运升级 +TRUNCATE TABLE tt_upgrade_record; + +-- 指数盲盒(tt_exponent 是指数期号历史,勿清空,见 Part 7) +TRUNCATE TABLE tt_exponent_user; +TRUNCATE TABLE tt_exponent_user_box; + +-- 极速糖果(sweet) +TRUNCATE TABLE game_sweet_record; -- 对局记录 +TRUNCATE TABLE game_sweet_user; -- 用户累计统计 +TRUNCATE TABLE game_sweet_win; -- 每日盈亏统计 + +-- 疯狂转盘(wheel) +TRUNCATE TABLE game_wheel_round; -- 每期开奖 +TRUNCATE TABLE game_wheel_round_bet; -- 下注明细 +TRUNCATE TABLE game_wheel_user; -- 用户累计统计 +TRUNCATE TABLE game_wheel_pool_adjustment; -- 奖池调节记录 + +-- 玫瑰女郎(olympus) +TRUNCATE TABLE game_olympus_record; -- 玩家游戏记录 +TRUNCATE TABLE game_olympus_user; -- 用户累计统计 +TRUNCATE TABLE game_olympus_win; -- 每日盈亏 +TRUNCATE TABLE game_olympus_reward_log; -- 奖池水位调整日志 + +-- ============================================================ +-- Part 4: 预生成回合池(默认注释) +-- ⚠️ 这些不是配置,是预生成回合池:清空后必须在后台重新生成/重算, +-- 否则对应玩法开局时取不到回合。数据量大(约 90 万行), +-- 要彻底重置磁盘占用再放开本段。 +-- ============================================================ +-- TRUNCATE TABLE game_sweet_round; -- 极速糖果预生成回合(约 8.4 万行) +-- TRUNCATE TABLE game_sweet_step; -- 极速糖果回合步骤(约 75 万行 / 626MB) +-- TRUNCATE TABLE game_olympus_round; -- 玫瑰女郎预生成回合(约 2.9 万行) +-- TRUNCATE TABLE game_olympus_step; -- 玫瑰女郎回合步骤(约 12.9 万行) + +-- ============================================================ +-- Part 5: 辅助数据 +-- ============================================================ +TRUNCATE TABLE tt_attendance_record; -- 签到记录 +TRUNCATE TABLE tt_message; -- 用户消息 +TRUNCATE TABLE tt_message_send; -- 消息下发记录 +TRUNCATE TABLE tt_notice; -- 用户通知 +TRUNCATE TABLE sys_notice; -- 系统公告 +TRUNCATE TABLE tt_announcement_read; -- 公告已读记录 +TRUNCATE TABLE tt_bonus_receive_record; -- 奖励包领取记录 +TRUNCATE TABLE tt_red_packet; -- 红包 +TRUNCATE TABLE tt_red_packet_record; -- 红包领取记录 +TRUNCATE TABLE tt_commission_record; -- 佣金收益记录 +TRUNCATE TABLE tt_promotion_record; -- 推广记录 +TRUNCATE TABLE tt_task_center_user; -- 任务中心用户进度 +TRUNCATE TABLE tt_welfare_record; -- 福利领取记录 +TRUNCATE TABLE tt_welfare_box_inventory; -- 用户福利箱子库存 +TRUNCATE TABLE tt_turnover_lottery_record; -- 流水抽奖记录 + +-- ============================================================ +-- Part 6: 发货链路 +-- ============================================================ +TRUNCATE TABLE tt_delivery_record; -- 发货记录 +TRUNCATE TABLE tt_box_transfer_record; -- 饰品转移记录 +TRUNCATE TABLE tt_replacement_record; -- 饰品替换记录 +TRUNCATE TABLE tt_transfer_apply; -- 转赠申请(按批次聚合审核) +TRUNCATE TABLE tt_user_transfer_daily; -- 用户每日转赠次数记录 + +-- ============================================================ +-- Part 7: 明确【不要】清空的表(说明,勿放开) +-- ============================================================ +-- game_wheel_reward_matrix 疯狂转盘 120 条固定奖励矩阵,删行会导致「命中缺行漏发」 +-- tt_exponent 指数期号历史;TtExponentGenerateTask 的 @Scheduled 已注释停用, +-- 清空后当天无指数记录,指数盲盒无法开局 +-- 宝箱 / 饰品 / 各玩法配置 tt_box*、tt_ornament*、tt_game_mould*、game_sweet_*、 +-- game_olympus_*、game_wheel_prize_*、tt_roll_prize_template*、 +-- tt_roll_jackpot*、tt_upgrade_ornaments*、tt_recharge_config/prod、 +-- tt_vip_level、tt_welfare*、tt_task_center、tt_turnover_lottery*、 +-- tt_robot_fight_*、tt_content*、tt_banner、tt_advertisement、 +-- tt_announcement、promo_admin / promo_menu_config / promo_role_menu +-- 一律不清:清空后要重新导入配置,本脚本不负责恢复 +-- qrtz_* 调度器运行状态(任务定义在 sys_job) +-- sys_user / sys_role / sys_menu / sys_config / sys_dict_* / gen_table* +-- 框架、权限、字典与代码生成器数据,清了后台就进不去 +-- *_new / *_old 历史重建脚本(ruoyi-ui/main.py)遗留的影子表,确认无引用后直接 DROP,不必纳入本脚本 + +-- 恢复外键检查 +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================ +-- 维护提示:新增业务表后请同步本文件,可用下面两条反查差异 +-- 1) 库内全部表:SELECT table_name FROM information_schema.tables +-- WHERE table_schema='changzhou' AND table_type='BASE TABLE' ORDER BY table_name; +-- 2) 本脚本已覆盖的表:在本文件中搜索 TRUNCATE TABLE +-- ============================================================ diff --git a/docs/updata_sql.md b/docs/updata_sql.md new file mode 100644 index 0000000..4d53804 --- /dev/null +++ b/docs/updata_sql.md @@ -0,0 +1,1748 @@ +-- VIP等级表新增流水返利百分比字段 +ALTER TABLE tt_vip_level ADD COLUMN flow_rebate_percent DECIMAL(10,4) DEFAULT 0.0000 COMMENT 'VIP流水返利百分比(%),如 2.0000 代表 2%'; + +新增疯狂转盘/极速糖果维护配置 +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('疯狂转盘维护', 'wheelMaintenance', '0', 'Y', 'admin', NOW(), '疯狂转盘维护开关 0正常 1维护'); + +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('极速糖果维护', 'sweetMaintenance', '0', 'Y', 'admin', NOW(), '极速糖果维护开关 0正常 1维护'); + +新增充值维护配置 +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('充值维护', 'rechargeMaintenance', '0', 'Y', 'admin', NOW(), '充值维护开关 0正常 1维护'); + +添加公告类型 +ALTER TABLE tt_announcement +ADD COLUMN `type` TINYINT NOT NULL DEFAULT 0 COMMENT '公告类型 0用户公告 1系统公告' AFTER content; + +ALTER TABLE tt_announcement MODIFY COLUMN title VARCHAR(255) NULL; + + +添加索引 +ALTER TABLE tt_box_records +ADD INDEX idx_tbr_holder_status_time (holder_user_id, status, create_time); + +ALTER TABLE tt_upgrade_record +ADD INDEX idx_tur_victory_gain (is_victory, gain_ornaments_price); + + +添加公告目标用户 +ALTER TABLE tt_announcement ADD COLUMN target_user_id BIGINT DEFAULT NULL COMMENT '目标用户ID,NULL表示全部用户'; + +公告加对个人 +ALTER TABLE tt_box_type ADD COLUMN is_show VARCHAR(2) DEFAULT '0' COMMENT '是否前端显示 0-显示 1-隐藏'; + +增加字典数据 +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('西部对决维护', 'westernDuelMaintenance', '0', 'Y', 'admin', NOW(), '西部对决维护开关 0正常 1维护'); + +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('幸运升级维护', 'luckyUpgradeMaintenance', '0', 'Y', 'admin', NOW(), '幸运升级维护开关 0正常 1维护'); + +-- 推广后台角色菜单权限表(按用户类型分配菜单,支持排序) +CREATE TABLE `promo_role_menu` ( + `id` INT NOT NULL AUTO_INCREMENT, + `role_type` VARCHAR(10) NOT NULL COMMENT '用户类型: 01=主播, 04=招商, 05=卡商', + `menu_path` VARCHAR(255) NOT NULL COMMENT '菜单路径', + `sort_order` INT DEFAULT 0 COMMENT '排序序号,越小越靠前', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_menu` (`role_type`, `menu_path`), + KEY `idx_role_type` (`role_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广后台角色菜单权限表'; + +-- 推广后台管理员表(独立于tt_user前端用户表) +CREATE TABLE `promo_admin` ( + `id` INT NOT NULL AUTO_INCREMENT, + `username` VARCHAR(50) NOT NULL COMMENT '登录账号(手机号)', + `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)', + `nick_name` VARCHAR(50) DEFAULT '管理员' COMMENT '昵称', + `phone_number` VARCHAR(20) DEFAULT NULL COMMENT '手机号', + `email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱', + `avatar` TEXT DEFAULT NULL COMMENT '头像(base64 dataURL)', + `status` CHAR(1) DEFAULT '0' COMMENT '状态 0正常 1停用', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`), + KEY `idx_phone_number` (`phone_number`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广后台管理员表'; + +-- 老库升级(已存在 promo_admin 表时执行这几条 + 回填手机号) +-- ALTER TABLE promo_admin ADD COLUMN phone_number VARCHAR(20) DEFAULT NULL COMMENT '手机号' AFTER nick_name; +-- ALTER TABLE promo_admin ADD COLUMN email VARCHAR(64) DEFAULT NULL COMMENT '邮箱' AFTER phone_number; +-- ALTER TABLE promo_admin ADD COLUMN avatar TEXT DEFAULT NULL COMMENT '头像(base64 dataURL)' AFTER email; +-- ALTER TABLE promo_admin ADD KEY idx_phone_number (phone_number); +-- UPDATE promo_admin SET phone_number = username WHERE phone_number IS NULL OR phone_number = ''; + +-- 插入超级管理员账号(密码: admin123) +-- BCrypt加密后的值需根据实际环境生成,这里给出模板 +INSERT INTO promo_admin (username, password, nick_name, status, create_time) +VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '超级管理员', '0', NOW()); + +-- 卡商卡密表(推广后台独立卡密系统) +CREATE TABLE `promo_card_key` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `card_code` VARCHAR(32) NOT NULL COMMENT '卡密编码(唯一)', + `amount` DECIMAL(10,2) NOT NULL COMMENT '面值金额', + `recharge_list_id` INT DEFAULT NULL COMMENT '充值列表ID', + `creator_id` BIGINT NOT NULL COMMENT '创建者ID(卡商)', + `creator_name` VARCHAR(50) DEFAULT '' COMMENT '创建者名称', + `used_by_id` BIGINT DEFAULT NULL COMMENT '使用者ID', + `used_by_name` VARCHAR(50) DEFAULT NULL COMMENT '使用者名称', + `used_time` DATETIME DEFAULT NULL COMMENT '使用时间', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0=未使用, 1=已使用', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_card_code` (`card_code`), + KEY `idx_creator_id` (`creator_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡商卡密表'; + +-- 推广后台菜单配置表(所有可分配的菜单从这里读取,不再硬编码) +CREATE TABLE `promo_menu_config` ( + `id` INT NOT NULL AUTO_INCREMENT, + `menu_path` VARCHAR(255) NOT NULL COMMENT '菜单路径', + `menu_label` VARCHAR(100) NOT NULL COMMENT '菜单显示名称', + `sort_order` INT DEFAULT 0 COMMENT '排序序号', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_menu_path` (`menu_path`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广后台菜单配置表'; + +-- 初始菜单数据(menu_label必须与前端路由title一致) +INSERT INTO promo_menu_config (menu_path, menu_label, sort_order) VALUES +('/home/index', '首页', 1), +('/dashboard/index', '数据面板', 2), +('/user/index', '个人信息', 3), +('/mymoney/index', '我的统计', 4), +('/turnover/index', '流水明细', 5), +('/subFlow/index', '下级流水', 6), +('/cardMerchant/index', '卡密管理', 7), +('/permission/index', '权限管理', 8), +('/businessManage/index', '招商管理', 9), +('/anchorManage/index', '主播管理', 10), +('/cardMerchantManage/index', '卡商管理', 11); + +-- 修正已有菜单标签(如果表中已有数据,执行以下UPDATE) +UPDATE promo_menu_config SET menu_label = '数据面板' WHERE menu_path = '/dashboard/index'; +UPDATE promo_menu_config SET menu_label = '我的统计' WHERE menu_path = '/mymoney/index'; +UPDATE promo_menu_config SET menu_label = '流水明细' WHERE menu_path = '/turnover/index'; +UPDATE promo_menu_config SET menu_label = '下级流水' WHERE menu_path = '/subFlow/index'; + +-- 新增超管专属页面菜单 +INSERT IGNORE INTO promo_menu_config (menu_path, menu_label, sort_order) VALUES +('/businessManage/index', '招商管理', 9), +('/anchorManage/index', '主播管理', 10), +('/cardMerchantManage/index', '卡商管理', 11); + +-- 新增9个页面菜单配置 +INSERT IGNORE INTO promo_menu_config (menu_path, menu_label, sort_order) VALUES +('/mySubordinates/index', '我的下级', 12), +('/flowRecord/index', '流水记录', 13), +('/cdkeyCreate/index', '创建CDKEY记录', 14), +('/cdkeyRedeem/index', '兑换CDKEY记录', 15), +('/cdkeyCreateDaily/index', '创建CDKEY日报', 16), +('/cdkeyRedeemDaily/index', '兑换CDKEY日报', 17), +('/rollRoom/index', 'ROLL房管理', 18), +('/commissionReport/index', '返点报表', 19), +('/subordinateRecharge/index', '下级充值报表', 20); + +-- 新增「管理员管理」菜单 +-- ① 菜单注册表(前端路由 + 权限管理页可勾选列表都从这里读) +INSERT IGNORE INTO promo_menu_config (menu_path, menu_label, sort_order) VALUES +('/adminManage/index', '管理员管理', 21); + +-- ② 给超级管理员角色授权(role_type=99 已有配置行时必须补这行,否则侧边栏不显示; +-- 也可以直接去「权限管理 → 超级管理员 → 勾选管理员管理 → 保存权限」) +INSERT IGNORE INTO promo_role_menu (role_type, menu_path, sort_order) VALUES +('99', '/adminManage/index', 21); + +-- 修复 user_type 字典:补充卡商类型(05) +INSERT IGNORE INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +VALUES (5, '卡商', '05', 'user_type', '', '', 'N', '0', 'admin', NOW(), '卡商用户类型'); + +-- tt_user 表新增卡商专用余额字段 +ALTER TABLE tt_user ADD COLUMN merchant_balance DECIMAL(18,2) DEFAULT 0.00 COMMENT '卡商专用余额(仅卡商用户使用)'; + + +ALTER TABLE tt_box_ornaments ADD COLUMN odds_percentum VARCHAR(20) DEFAULT NULL COMMENT '显示爆率百分比'; + +ALTER TABLE tt_recharge_record ADD COLUMN card_merchant_id INT DEFAULT 0 COMMENT '卡商ID,默认0表示非卡商充值'; + + +ALTER TABLE tt_user +ADD COLUMN new_game_commission_rate DECIMAL(10,4) DEFAULT NULL COMMENT '西部对决新游戏消费返点比例', +ADD COLUMN classic_game_commission_rate DECIMAL(10,4) DEFAULT NULL COMMENT '经典玩法游戏消费返点比例'; + +-- 推广CDKEY创建记录表 +CREATE TABLE `promo_cdkey` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `title` VARCHAR(255) DEFAULT NULL COMMENT 'CDKEY标题', + `cdkey_code` VARCHAR(100) NOT NULL COMMENT 'CDKEY码(唯一)', + `amount_min` DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '最低金额(金币)', + `amount_max` DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '最高金额(金币)', + `num` INT NOT NULL DEFAULT 1 COMMENT '每个码可用次数', + `usable_count` INT NOT NULL DEFAULT 1 COMMENT '可用次数', + `used_count` INT NOT NULL DEFAULT 0 COMMENT '已用次数', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `opening_time` DATETIME DEFAULT NULL COMMENT '开启时间', + `validity` DATETIME DEFAULT NULL COMMENT '有效期', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0=进行中, 1=已结束', + `creator_id` BIGINT NOT NULL COMMENT '创建者ID', + `creator_name` VARCHAR(100) DEFAULT NULL COMMENT '创建者名称', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_cdkey_code` (`cdkey_code`), + KEY `idx_creator_id` (`creator_id`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广CDKEY创建记录表'; + +-- 推广CDKEY兑换记录表 +CREATE TABLE `promo_cdkey_redeem` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `cdkey_id` BIGINT NOT NULL COMMENT '关联promo_cdkey.id', + `cdkey_code` VARCHAR(100) NOT NULL COMMENT 'CDKEY码', + `redeemer_id` BIGINT NOT NULL COMMENT '兑换者ID', + `redeemer_name` VARCHAR(100) DEFAULT NULL COMMENT '兑换者名称', + `unit_value` DECIMAL(12,2) NOT NULL COMMENT '单个价值(金币)', + `redeem_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '兑换时间', + PRIMARY KEY (`id`), + KEY `idx_cdkey_id` (`cdkey_id`), + KEY `idx_redeemer_id` (`redeemer_id`), + KEY `idx_redeem_time` (`redeem_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广CDKEY兑换记录表'; + +-- tt_recharge_card 表新增空白卡密标识字段 +ALTER TABLE tt_recharge_card ADD COLUMN is_blank TINYINT DEFAULT 0 COMMENT '是否空白卡密: 0=否, 1=是(空白卡密用户使用后仅加余额,不计入充值记录)'; + +-- tt_recharge_record 表新增空白卡密标记字段(空白卡密产生的充值记录在充值记录列表中不展示) +ALTER TABLE tt_recharge_record ADD COLUMN is_blank_card TINYINT DEFAULT 0 COMMENT '是否空白卡密产生的充值记录: 0=否, 1=是'; + +-- tt_roll_jackpot_ornaments 表新增指定获奖用户字段(多个用户ID用英文逗号分隔) +ALTER TABLE tt_roll_jackpot_ornaments ADD COLUMN designated_user_id VARCHAR(500) DEFAULT NULL COMMENT '指定获奖用户ID,多个用英文逗号分隔,NULL表示不指定(随机分配)'; + +-- tt_user 表新增白号标识字段(白号:可正常游戏、可出现在榜单,但提货虚假成功、充值不计真实充值、不触发返佣) +ALTER TABLE tt_user ADD COLUMN is_blank_account TINYINT DEFAULT 0 COMMENT '是否白号: 0=否, 1=是'; + +-- 疯狂转盘返奖饰品表 +CREATE TABLE game_wheel_prize_ornament ( + id BIGINT NOT NULL AUTO_INCREMENT, + game_type VARCHAR(32) DEFAULT 'wheel', + name VARCHAR(100) NOT NULL, + image_url VARCHAR(500) DEFAULT NULL, + grade INT DEFAULT 1, + is_fallback TINYINT DEFAULT 0 COMMENT '是否兜底: 0否 1是', + sort INT DEFAULT 0, + status CHAR(1) DEFAULT '1', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疯狂转盘返奖饰品'; + +CREATE TABLE game_wheel_prize_range ( + id BIGINT NOT NULL AUTO_INCREMENT, + game_type VARCHAR(32) DEFAULT 'wheel', + win_low DECIMAL(14,2) NOT NULL DEFAULT 0.00 COMMENT '返奖金额下限', + win_high DECIMAL(14,2) NOT NULL DEFAULT 0.00 COMMENT '返奖金额上限', + ornament_id BIGINT NOT NULL, + sort INT DEFAULT 0, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疯狂转盘返奖区间'; + +-- 疯狂转盘返奖区间初始数据 +INSERT INTO game_wheel_prize_range (game_type, win_low, win_high, ornament_id, sort) VALUES +('wheel', 0.01, 0.02, 1, 1), +('wheel', 0.02, 10.00, 2, 2), +('wheel', 10.00, 50.00, 3, 3), +('wheel', 50.00, 200.00, 4, 4), +('wheel', 200.00, 500.00, 5, 5), +('wheel', 500.00, 1000.00, 6, 6), +('wheel', 1000.00, 2000.00, 7, 7), +('wheel', 2000.00, 5000.00, 8, 8), +('wheel', 5000.00, 99999999.00, 9, 9); + +-- 对战出金数据 +ALTER TABLE tt_fight ADD COLUMN gold_drop JSON DEFAULT NULL COMMENT 'first gold ornament drop info for hall display'; + +-- 极速糖果步骤表:新增消除位置、下落数据、填充数据字段 +ALTER TABLE game_sweet_step + ADD COLUMN removed_positions JSON DEFAULT NULL COMMENT '消除位置 [pos,...]' AFTER symbol_links, + ADD COLUMN drop_data JSON DEFAULT NULL COMMENT '下落数据 [{"from":int,"to":int,"symbol":"A"},...]' AFTER removed_positions, + ADD COLUMN fill_data JSON DEFAULT NULL COMMENT '填充数据 [{"pos":int,"symbol":"A"},...]' AFTER drop_data; + +-- 推广后台:网站设置新增"支付公司"配置项 +INSERT INTO sys_config (config_id, config_name, config_key, config_value, config_type, create_by, create_time, remark) +SELECT 135, '支付公司', 'payCompany', 'chengduhipay', 'Y', 'admin', NOW(), '充值对接支付平台选择' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE config_key = 'payCompany'); + +-- 推广后台:网站设置新增"支付公司权重"配置项 +INSERT INTO sys_config (config_id, config_name, config_key, config_value, config_type, create_by, create_time, remark) +SELECT 136, '支付公司权重', 'payCompanyWeights', '[{"name":"chengduhipay","weight":10}]', 'Y', 'admin', NOW(), '支付公司权重分配,JSON格式' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE config_key = 'payCompanyWeights'); + +-- ROLL房奖项模板表 +CREATE TABLE `tt_roll_prize_template` ( + `id` INT NOT NULL AUTO_INCREMENT, + `template_name` VARCHAR(100) NOT NULL COMMENT '模板名称', + `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建者', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ROLL房奖项模板'; + +-- ROLL房奖项模板明细表(模板内每个饰品) +CREATE TABLE `tt_roll_prize_template_detail` ( + `id` INT NOT NULL AUTO_INCREMENT, + `template_id` INT NOT NULL COMMENT '关联tt_roll_prize_template.id', + `ornament_id` BIGINT NOT NULL COMMENT '饰品ID', + `ornament_name` VARCHAR(255) DEFAULT NULL COMMENT '饰品名称', + `img_url` VARCHAR(500) DEFAULT NULL COMMENT '饰品图片', + `price` DECIMAL(12,2) DEFAULT NULL COMMENT '饰品价格', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_template_id` (`template_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ROLL房奖项模板明细'; + +-- 奖项模板明细表新增饰品数量字段 +ALTER TABLE tt_roll_prize_template_detail ADD COLUMN ornaments_num INT DEFAULT 1 COMMENT '饰品数量'; + +-- 极速糖果:游戏记录表新增结算/补发字段 +ALTER TABLE game_sweet_record + ADD COLUMN settled TINYINT DEFAULT 0 COMMENT '是否已结算 0未1已', + ADD COLUMN mode_type VARCHAR(20) DEFAULT NULL COMMENT '游戏模式 normal/ante/buy_fs/buy_super_fs', + ADD COLUMN auto_dismantle TINYINT DEFAULT 0 COMMENT '开局自动分解快照 0关1开', + ADD COLUMN reward_type VARCHAR(10) DEFAULT NULL COMMENT '发放类型 gold/ornament', + ADD COLUMN settled_win DECIMAL(10,2) DEFAULT 0.00 COMMENT '本局价值(金币金额=饰品价格)', + ADD COLUMN prize_id BIGINT DEFAULT NULL COMMENT '饰品ID game_sweet_prize_ornament.id', + ADD COLUMN prize_image_url VARCHAR(500) DEFAULT NULL COMMENT '饰品图片URL', + ADD COLUMN prize_level_img VARCHAR(500) DEFAULT NULL COMMENT '品质图片URL', + ADD COLUMN settle_source VARCHAR(20) DEFAULT NULL COMMENT '结算来源 finish/reconnect/scheduled'; + +-- 极速糖果:返奖饰品表新增品质名称字段 +ALTER TABLE game_sweet_prize_ornament + ADD COLUMN exterior_name VARCHAR(100) DEFAULT NULL COMMENT '品质名称 崭新出厂/略有磨损/久经沙场/破损不堪/战痕累累'; + +-- 极速糖果:未结算记录兜底补发定时任务(后台:系统监控>定时任务) +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('极速糖果未结算补发', 'SWEET', 'sweetSettleTask.scheduledSettle()', '0 */10 * * * ?', '1', '0', '0', 'admin', NOW(), '每10分钟扫描超35分钟未结算记录并补发金币/饰品'); + +-- RichKu:支付宝金额<300强制走richku,>=300按权重分配(chengduhipay:richku=7:3),银联仅走chengduhipay +UPDATE sys_config +SET config_value = '[{"name":"chengduhipay","weight":7},{"name":"richku","weight":3,"payType":1}]' +WHERE config_key = 'payCompanyWeights'; + +-- itpay:接入新支付渠道并纳入权重分配(权重值按需调整;payType 可选:1=支付宝,2=银联,不填=全渠道) +-- 示例:chengduhipay:5、richku:3(仅支付宝)、itpay:2 +UPDATE sys_config +SET config_value = '[{"name":"chengduhipay","weight":5},{"name":"richku","weight":3,"payType":1},{"name":"itpay","weight":2}]' +WHERE config_key = 'payCompanyWeights'; + +-- 极速糖果:饰品桶新增已消耗回合数(库存配额计数),剩余 = count - used +-- 玩家抽奖消耗对应桶一个回合(used+1);整盒回合抽干时自动重算、或后台手动重算分布时 used 归 0(剩余回归满,玩家可重新抽奖) +ALTER TABLE game_sweet_box_ornament + ADD COLUMN used INT DEFAULT 0 COMMENT '已消耗回合数,剩余=count-used' AFTER count; + +-- 极速糖果:奖品饰品表填充磨损度名称(根据 grade 映射) +UPDATE game_sweet_prize_ornament SET exterior_name = '崭新出厂' WHERE grade = 1 AND (exterior_name IS NULL OR exterior_name = ''); +UPDATE game_sweet_prize_ornament SET exterior_name = '略有磨损' WHERE grade = 2 AND (exterior_name IS NULL OR exterior_name = ''); +UPDATE game_sweet_prize_ornament SET exterior_name = '久经沙场' WHERE grade = 3 AND (exterior_name IS NULL OR exterior_name = ''); +UPDATE game_sweet_prize_ornament SET exterior_name = '破损不堪' WHERE grade = 4 AND (exterior_name IS NULL OR exterior_name = ''); +UPDATE game_sweet_prize_ornament SET exterior_name = '战痕累累' WHERE grade = 5 AND (exterior_name IS NULL OR exterior_name = ''); + +-- 白号管理目录及子页面菜单 +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +VALUES (3100, '白号管理', 0, 5, 'blank', NULL, NULL, 1, 0, 'M', '0', '0', '', 'peoples', 'admin', NOW(), '', NULL, '白号管理目录'); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +VALUES (3101, '白号用户', 3100, 1, 'blankAccount', 'skins/blankAccount/index', NULL, 1, 0, 'C', '0', '0', 'admin:blankAccount:list', 'user', 'admin', NOW(), '', NULL, '白号用户列表'); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +VALUES (3102, '生成空白卡密', 3100, 2, 'blankCardKey', 'skins/blankCardKey/index', NULL, 1, 0, 'C', '0', '0', 'admin:blankCardKey:list', 'edit', 'admin', NOW(), '', NULL, '空白卡密管理'); + +-- 空白卡密表(管理后台独立生成,与卡商卡密表 promo_card_key 分离) +-- 表名规范化:加 tt_ 前缀(原 blank_card_key) +CREATE TABLE `tt_blank_card_key` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `card_code` VARCHAR(32) NOT NULL COMMENT '卡密编码(唯一)', + `amount` DECIMAL(10,2) NOT NULL COMMENT '面值金额', + `used_by_id` BIGINT DEFAULT NULL COMMENT '使用者ID', + `used_by_name` VARCHAR(50) DEFAULT NULL COMMENT '使用者名称', + `used_time` DATETIME DEFAULT NULL COMMENT '使用时间', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0=未使用, 1=已使用', + `creator_name` VARCHAR(50) DEFAULT '' COMMENT '创建者名称', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_card_code` (`card_code`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='空白卡密表(管理后台生成)'; + +-- 已上线库执行(老库表名规范化,幂等:表不存在时执行 rename 报错可忽略/或先判断) +RENAME TABLE blank_card_key TO tt_blank_card_key; + +-- 饰品表新增商城价格字段(独立于 usePrice,专供前端商城展示) +ALTER TABLE tt_ornament ADD COLUMN store_price DECIMAL(18,2) DEFAULT NULL COMMENT '商城价格(= 第三方价格 × 汇率 × 溢价率)'; + + +ALTER TABLE game_wheel_round_bet ADD COLUMN settled INT DEFAULT 1 COMMENT '0=等决定 1=已结算'; + +-- 疯狂转盘:奖品饰品表新增磨损度字段 +ALTER TABLE game_wheel_prize_ornament + ADD COLUMN exterior_name VARCHAR(100) DEFAULT NULL COMMENT '磨损度名称 崭新出厂/略有磨损/久经沙场/破损不堪/战痕累累'; + +-- 疯狂转盘:奖品饰品表填充磨损度名称(根据 grade 映射) +UPDATE game_wheel_prize_ornament SET exterior_name = '崭新出厂' WHERE grade = 1 AND (exterior_name IS NULL OR exterior_name = ''); +UPDATE game_wheel_prize_ornament SET exterior_name = '略有磨损' WHERE grade = 2 AND (exterior_name IS NULL OR exterior_name = ''); +UPDATE game_wheel_prize_ornament SET exterior_name = '久经沙场' WHERE grade = 3 AND (exterior_name IS NULL OR exterior_name = ''); +UPDATE game_wheel_prize_ornament SET exterior_name = '破损不堪' WHERE grade = 4 AND (exterior_name IS NULL OR exterior_name = ''); +UPDATE game_wheel_prize_ornament SET exterior_name = '战痕累累' WHERE grade = 5 AND (exterior_name IS NULL OR exterior_name = ''); + + +-- 西部对决 → 荒野争锋:更新 sys_config 维护开关显示名 +UPDATE sys_config SET config_name = '荒野争锋维护', remark = '荒野争锋维护开关 0正常 1维护' WHERE config_key = 'westernDuelMaintenance'; + +-- 新增用户流水管理页面菜单 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('用户流水', 2000, 10, 'userTurnover', 'skins/ttuser/userTurnover', 'C', '0', '0', 'system:user:turnover', 'money', 'admin', NOW(), '用户流水统计页面'); + +-- ========== 招商主播报表系统 ========== + +-- 1. 用户来源日报表(按user_id+source+report_date统计累计流水) +CREATE TABLE `tt_user_source_report` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + `user_id` INT NOT NULL COMMENT '用户ID', + `parent_id` INT DEFAULT NULL COMMENT '邀请人ID(来自tt_user.parent_id)', + `source` INT NOT NULL COMMENT '来源source码(对应TtAccountRecordSource.code)', + `total` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '截至当日的累计金额绝对值总和', + `report_date` DATE NOT NULL COMMENT '报表日期', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_source_date` (`user_id`, `source`, `report_date`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_source` (`source`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户来源日报表(按user_id+source统计累计流水)'; + +-- 2. 流水归档表(存储7天前的历史流水数据) +CREATE TABLE `tt_user_blend_ercash_archive` ( + `id` BIGINT NOT NULL COMMENT '与原表id一致', + `user_id` INT DEFAULT NULL, + `amount` DECIMAL(18,2) DEFAULT NULL, + `final_amount` DECIMAL(18,2) DEFAULT NULL, + `credits` DECIMAL(18,2) DEFAULT NULL, + `final_credits` DECIMAL(18,2) DEFAULT NULL, + `total` DECIMAL(18,2) DEFAULT NULL, + `source` INT DEFAULT NULL, + `type` INT DEFAULT NULL, + `remark` VARCHAR(500) DEFAULT NULL, + `create_time` DATETIME DEFAULT NULL, + `update_time` DATETIME DEFAULT NULL, + `archive_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '归档时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='流水归档表(7天前的历史流水)'; + +-- 3. 注册定时任务到 sys_job 表 + +-- 用户来源日报表生成(每天凌晨3点,增量递推统计各用户各source累计流水) +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('用户来源日报表生成', 'REPORT', 'userSourceReportTask.generateDailyReport()', '0 0 3 * * ?', '3', '1', '0', 'admin', NOW(), '每天凌晨3点生成用户来源日报表,增量递推统计累计流水'); + +-- 流水数据归档(每天凌晨5点,将7天前的流水迁移到归档表) +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('流水数据归档', 'ARCHIVE', 'flowArchiveTask.archiveOldData()', '0 0 5 * * ?', '3', '1', '0', 'admin', NOW(), '每天凌晨5点将7天前的流水数据迁移到归档表,分批1000条'); + +-- 荒野争锋(西部对决)奖励池同步(每5分钟执行一次) +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('荒野争锋奖励池同步', 'BULLET', 'bulletSettleTask.syncRewardPool()', '0 */5 * * * ?', '3', '1', '0', 'admin', NOW(), '每5分钟把Redis奖励池增量同步到game_bullet_win表'); + +-- 荒野争锋(西部对决)未结算兜底(每5分钟执行一次) +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('荒野争锋未结算兜底', 'BULLET', 'bulletSettleTask.closeTimeoutRecords()', '0 */5 * * * ?', '3', '1', '0', 'admin', NOW(), '每5分钟扫描超30分钟未结算的status=0记录并自动兜底结算'); + +-- VIP流水返利发放(每天凌晨0点10分,按昨日游戏流水总额 × VIP等级 flow_rebate_percent 统一发放,取代原实时发放) +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('VIP流水返利发放', 'VIP_REBATE', 'vipFlowRebateTask.grantDailyVipRebate()', '0 10 0 * * ?', '3', '1', '0', 'admin', NOW(), '每天凌晨0点10分发放昨日游戏流水的VIP返利,白号不发放,含当日幂等保护'); + +-- 修复 tt_user_blend_ercash_archive 与 tt_user_blend_ercash 排序规则不一致导致的 UNION 报错 +ALTER TABLE tt_user_blend_ercash_archive CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- ========== 流水归档保留天数可配置化 ========== + +-- 推广后台:网站设置新增“流水归档保留天数”配置项(兜底 7 天,超过该天数的流水迁移至归档表) +INSERT INTO sys_config (config_id, config_name, config_key, config_value, config_type, create_by, create_time, remark) +SELECT 137, '流水归档保留天数', 'flowArchiveRetainDays', '7', 'Y', 'admin', NOW(), '流水归档保留天数,超过该天数的流水迁移至归档表,兜底 7' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE config_key = 'flowArchiveRetainDays'); + + +-- 归档明细页菜单:余额变动-归档 / 积分变动-归档 +INSERT INTO `chengdu`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (3104, '余额变动-归档', 2000, 11, 'blendErcashArchiveAmount', 'skins/ttuser/blendErcashArchive', NULL, 1, 0, 'C', '0', '0', 'admin:blendErcash:archiveList', 'money', 'admin', '2026-07-07 14:46:52', '', NULL, '余额变动归档明细页(仅归档表)'); +INSERT INTO `chengdu`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (3105, '积分变动-归档', 2000, 12, 'blendErcashArchiveCredits', 'skins/ttuser/blendErcashArchive', NULL, 1, 0, 'C', '0', '0', 'admin:blendErcash:archiveList', 'star', 'admin', '2026-07-07 14:46:57', '', NULL, '积分变动归档明细页(仅归档表)'); + + +-- 对战宝箱-查看奖项 页面性能优化:tt_box_records 复合索引 +ALTER TABLE tt_box_records +ADD INDEX idx_tbr_box_ornament_time (box_id, ornament_id, create_time); + +-- /admin/blendErcash/list 收支明细页面性能优化:tt_user_blend_ercash 索引 +-- 执行前先 SHOW INDEX FROM tt_user_blend_ercash; 确认无等价索引; +-- 1) 默认打开页面无筛选时按 create_time 倒序分页,解决全表扫描+filesort(核心) +ALTER TABLE tt_user_blend_ercash +ADD INDEX idx_tube_ctime (create_time); +-- 2) 常见「按用户ID查流水」+ 时间倒序场景 +ALTER TABLE tt_user_blend_ercash +ADD INDEX idx_tube_user_ctime (user_id, create_time); +-- 3) 归档表 /admin/blendErcash/archive/list 同样按 create_time 倒序,归档表当前仅有主键 +ALTER TABLE tt_user_blend_ercash_archive +ADD INDEX idx_tubea_ctime (create_time); +ALTER TABLE tt_user_blend_ercash_archive +ADD INDEX idx_tubea_user_ctime (user_id, create_time); + +-- 首页统计折线图 /admin/rechargeRecord/chartData 性能优化:tt_user_blend_ercash 按 source+时间聚合 +-- 执行前先 SHOW INDEX FROM tt_user_blend_ercash; 确认无等价索引; +-- 命中 WHERE source = ? AND create_time >= ? AND create_time < ? GROUP BY source(等值列 source 在前、范围列 create_time 在后) +ALTER TABLE tt_user_blend_ercash +ADD INDEX idx_tube_source_ctime (source, create_time); + +-- 首页折线图按周维度(近4周=28天)会 UNION 归档表聚合:归档表补 (source, create_time) 复合索引, +-- 让 UNION 归档侧的按 source+时间聚合也走索引(归档表原本仅有 create_time / user_id+create_time 索引) +ALTER TABLE tt_user_blend_ercash_archive +ADD INDEX idx_tubea_source_ctime (source, create_time); + +-- ========== 招商佣金周结改造 ========== + +-- 扩展佣金结算表,记录结算周期与流水快照 +ALTER TABLE tt_commission_record +ADD COLUMN settlement_start_time DATETIME DEFAULT NULL COMMENT '结算周期开始时间', +ADD COLUMN settlement_end_time DATETIME DEFAULT NULL COMMENT '结算周期结束时间', +ADD COLUMN total_flow DECIMAL(18,2) DEFAULT 0.00 COMMENT '结算周期内参与计算的流水总额', +ADD COLUMN rate DECIMAL(10,4) DEFAULT 0.0000 COMMENT '结算时综合费率快照(%)'; + +-- 注册招商佣金周结定时任务(后台:系统监控 > 定时任务) +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('招商佣金周结', 'PROMO', 'commissionSettlementTask.settleWeeklyCommission()', '0 30 0 ? * MON', '0', '1', '0', 'admin', NOW(), '每周一 00:30 结算招商账号上周佣金(上周一 00:00 ~ 本周一 00:00)'); + +-- ========== 极速糖果箱子饰品分布模板持久化(模板从前端 localStorage 迁移到后端) ========== +-- 用途:保存每个箱子的饰品回合分布模板;箱子库存抽干时,按该箱子「最近一次应用的生效模板(is_active=1)」重新填桶并重算总回合。 +CREATE TABLE IF NOT EXISTS game_sweet_box_template ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + box_id BIGINT NOT NULL COMMENT '所属箱子ID', + name VARCHAR(100) NOT NULL COMMENT '模板名称(同一箱子内唯一)', + total_rounds INT NOT NULL DEFAULT 0 COMMENT '总回合数 = items 各 count 之和', + items_json TEXT NOT NULL COMMENT '饰品分布JSON: [{"ornamentId":1,"count":100},...]', + is_active TINYINT NOT NULL DEFAULT 0 COMMENT '是否为该箱子生效模板(抽干补桶依据): 1=是 0=否', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uk_box_name (box_id, name), + KEY idx_box_active (box_id, is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='极速糖果箱子饰品分布模板'; + +-- ========== 修复:Roll房奖池饰品ID列存不下饰品雪花大ID(如金币饰品 1783581913332) ========== +-- 根因:tt_roll_jackpot_ornaments.ornaments_id 旧表定义为 int,无法存储 tt_ornament.id(BIGINT大值), +-- 创建Roll房填充奖池时报错:Out of range value for column 'ornaments_id'。改为 BIGINT 与实体 Long 对齐。 +ALTER TABLE tt_roll_jackpot_ornaments MODIFY COLUMN ornaments_id BIGINT COMMENT 'Roll房奖池饰品ID'; + +-- ========== 修复:指定获奖用户列类型错误导致写入截断 ========== +-- 根因:tt_roll_jackpot_ornaments.designated_user_id 实际列类型为数值型(int/bigint), +-- 而实体字段为 String 且业务支持多个用户ID用英文逗号分隔(如 '874624,123456'), +-- 推广后台"指定获奖用户"保存时报错:Data truncated for column 'designated_user_id'。 +-- 改为 VARCHAR(500) 与实体定义及第198行的ADD COLUMN定义对齐。 +ALTER TABLE tt_roll_jackpot_ornaments MODIFY COLUMN designated_user_id VARCHAR(500) DEFAULT NULL COMMENT '指定获奖用户ID,多个用英文逗号分隔,NULL表示不指定(随机分配)'; + +-- ========== 修复:机器人VIP等级统一固定为 1-10(保证大厅/房间/观战/所有提示显示一致) ========== +-- 根因:机器人创建时未设置 vip_level(为 NULL),而对战落座时曾用 ThreadLocalRandom 随机 0-13, +-- 大厅列表与观战又从 tt_user.vip_level 读取(NULL→0),导致同一机器人在不同场景 VIP 显示不一致。 +-- 现改为:机器人 VIP 在创建时固定入库为 1-10,所有场景统一读 tt_user.vip_level。 +-- 本脚本为历史机器人(user_type='03')补齐固定的 1-10 随机 VIP(每个机器人固定一个值,不再动态变化)。 +UPDATE tt_user +SET vip_level = FLOOR(1 + RAND() * 10) +WHERE user_type = '03' AND (vip_level IS NULL OR vip_level < 1 OR vip_level > 10); + +-- ========== 修复:管理后台Roll房奖池 total_price 存量数据(填货后未维护导致为0) ========== +-- 根因:管理后台批量填货/单个新增/删除奖池饰品时未重算 tt_roll_jackpot.total_price, +-- 导致奖池总价值一直为0,前端显示“奖池总价值 0”且主播房创建时不扣款。 +-- 代码已修复(填货/新增/删除后都会重算),本脚本用于一次性修复历史存量奖池。 +UPDATE tt_roll_jackpot j +SET j.total_price = ( + SELECT COALESCE(SUM(o.price * o.ornaments_num), 0) + FROM tt_roll_jackpot_ornaments o + WHERE o.jackpot_id = j.jackpot_id +); + + +ALTER TABLE tt_roll ADD COLUMN deduct_credits DECIMAL(10,2) NOT NULL DEFAULT 0.00 +COMMENT '主播房创建时实际扣除的积分,用于开奖/删除按比例退还'; + +-- ========== 每日福利箱子发放:@Scheduled 迁移到 sys_job 统一调度 ========== +-- 原 WelfareBoxTask.autoIssueDailyWelfareBox() 使用硬编码 @Scheduled(cron="0 0 0 * * ?"), +-- 现改为 sys_job 可配置调度,便于后台系统监控>定时任务动态启停/调整 cron。 +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('每日福利箱子发放', 'WELFARE', 'welfareBoxTask.autoIssueDailyWelfareBox()', '0 0 0 * * ?', '3', '1', '0', 'admin', NOW(), '每天0点为所有正常用户发放对应VIP等级的福利箱子'); + +-- ========== 官方周期Roll房:配置驱动 + 定时自动创建 ========== +-- 需求:官方周roll(一周一次)、日roll(一天一次)自动创建,创建时按周期设置充值统计起点(recharge_start_time) +-- 与开奖时间(end_time)。日roll统计"今日充值",周roll统计"本周充值",门槛复用 tt_roll.min_recharge。 +-- 复用现有 tt_roll 房间体系、加入校验(joinRollCheck)与到期开奖(RollTask),不引入并行体系。 + +-- 1. 官方周期Roll配置表(模板,可配置多条,各自按周期自动再生房间) +CREATE TABLE `tt_official_roll_config` ( + `id` INT NOT NULL AUTO_INCREMENT, + `config_name` VARCHAR(100) NOT NULL COMMENT '配置标识(便于后台识别)', + `jackpot_id` INT NOT NULL COMMENT '指定奖池ID(tt_roll_jackpot.jackpot_id)', + `period_type` VARCHAR(16) NOT NULL COMMENT '周期类型: DAILY=每日, WEEKLY=每周', + `room_name` VARCHAR(100) NOT NULL COMMENT '生成的Roll房名称', + `description` VARCHAR(500) DEFAULT NULL COMMENT '房间描述', + `roll_password` VARCHAR(50) DEFAULT NULL COMMENT '参与密码(可空)', + `min_recharge` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '充值门槛金额,0=无门槛(日=今日充值,周=本周充值)', + `vip_level` INT DEFAULT 0 COMMENT 'VIP等级门槛,0=不限', + `people_num` INT NOT NULL DEFAULT 100 COMMENT '人数上限', + `sort_by` INT DEFAULT 0 COMMENT '排序依据', + `status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '状态: 0=启用, 1=停用', + `last_generate_time` DATETIME DEFAULT NULL COMMENT '上次成功生成房间的时间(幂等辅助)', + `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建者', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + `update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新者', + `update_time` DATETIME DEFAULT NULL, + `del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志: 0=存在, 2=删除', + PRIMARY KEY (`id`), + KEY `idx_status_period` (`status`, `period_type`), + KEY `idx_jackpot_id` (`jackpot_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='官方周期Roll房配置模板'; + +-- 2. tt_roll 增列 official_config_id:标记该房由哪条配置生成(溯源 + 幂等去重键的一部分) +ALTER TABLE tt_roll ADD COLUMN official_config_id INT DEFAULT NULL COMMENT '官方周期Roll配置ID(tt_official_roll_config.id),手动创建的房间为NULL'; +-- 幂等去重键:同一配置 + 同一充值统计起点 至多生成一个房间 +ALTER TABLE tt_roll ADD INDEX idx_official_config_start (official_config_id, recharge_start_time); + +-- 3. 注册定时任务(后台:系统监控 > 定时任务) +-- 日roll生成:每天 00:01 为所有启用的 DAILY 配置创建当日房间 +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('官方日Roll生成', 'ROLL', 'officialRollTask.generateDailyRolls()', '0 1 0 * * ?', '3', '1', '0', 'admin', NOW(), '每天00:01为启用的DAILY配置创建官方日roll房,起点=当天0点,开奖=次日0点'); + +-- 周roll生成:每周一 00:10 为所有启用的 WEEKLY 配置创建本周房间 +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('官方周Roll生成', 'ROLL', 'officialRollTask.generateWeeklyRolls()', '0 10 0 ? * MON', '3', '1', '0', 'admin', NOW(), '每周一00:10为启用的WEEKLY配置创建官方周roll房,起点=本周一0点,开奖=下周一0点'); + +-- 4. 后台菜单:在“Roll房列表”所在目录下新增“官方周期Roll配置”页面(作为其兄弟菜单,parent 自动取现有Roll房列表页的父级) +-- 菜单驱动路由:component 指向前端 src/views/skins/ttRoll/officialRollConfig.vue +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT '官方周期Roll配置', m.parent_id, 20, 'officialRollConfig', 'skins/ttRoll/officialRollConfig', 1, 0, 'C', '0', '0', 'admin:officialRollConfig:list', 'time', 'admin', NOW(), '官方周期Roll配置页面(定时自动创建官方日/周Roll房)' +FROM (SELECT parent_id FROM sys_menu WHERE component = 'skins/ttRoll/index' LIMIT 1) m; + +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT '支付订单', m.menu_id, 13, 'ttOrder', 'skins/ttOrder/index', 1, 0, 'C', '0', '0', 'admin:order:list', 'money', 'admin', NOW(), '网站支付订单查询页面(可按状态查看订单)' +FROM (SELECT menu_id FROM sys_menu WHERE path = 'currency' AND menu_type = 'M' LIMIT 1) m; + +-- ========== 主播推广CDKEY 过期/兑完 精确退还 ========== +-- 需求:CDK创建时按 amountMax×num×createNum 等额预扣主播金币+积分;兑换时仅随机派发 min~max 金币。 +-- 当CDK过期(validity到期)或兑完(usable_count用尽)后,按「预扣总额 - 实际已派发金币总额」精确返还 +-- 主播的金币与积分(同额),由定时任务统一兜底,refund_time 幂等防重复。 + +-- 1. promo_cdkey 增加退还标记字段(MySQL 不支持 ADD COLUMN IF NOT EXISTS,直接 ADD) +ALTER TABLE promo_cdkey + ADD COLUMN refund_amount DECIMAL(18,2) NULL COMMENT 'CDK结束退还金额(金币/积分同额)', + ADD COLUMN refund_time DATETIME NULL COMMENT '退还时间(NULL=未退还)'; + +-- 2. 注册定时任务(后台:系统监控 > 定时任务):每10分钟扫描已结束(过期/兑完)且未退还的CDK并精确退还 +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('CDK结束退还', 'PROMO', 'promoCdkeyRefundTask.settleEndedCdkeyRefunds()', '0 0/10 * * * ?', '3', '1', '0', 'admin', NOW(), '每10分钟扫描已过期或已兑完且未退还的CDK,按预扣-实际派发精确返还主播金币/积分并通知'); + + +-- 转赠申请表 +CREATE TABLE IF NOT EXISTS `tt_transfer_apply` ( +`id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', +`batch_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '转赠批次号(同一批次内多个饰品一起审核)', +`box_record_id` BIGINT NULL COMMENT '背包记录ID(tt_box_records.id)', +`from_user_id` INT NOT NULL COMMENT '转赠人用户ID', +`to_user_id` INT NOT NULL COMMENT '受赠人用户ID', +`vip_level` INT NOT NULL DEFAULT 0 COMMENT '提交转赠时的VIP等级', +`ornament_id` BIGINT NULL COMMENT '饰品ID', +`ornament_name` VARCHAR(255) NULL COMMENT '饰品名称', +`ornament_image_url` VARCHAR(500) NULL COMMENT '饰品图片URL', +`ornament_price` DECIMAL(10,2) NOT NULL COMMENT '饰品价格(单件超过1000才可转赠)', +`status` TINYINT DEFAULT 0 COMMENT '状态:0-待审核,1-审核通过,2-审核驳回', +`reject_reason` VARCHAR(500) NULL COMMENT '驳回原因', +`create_by` VARCHAR(64) NULL COMMENT '创建者', +`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', +`update_by` VARCHAR(64) NULL COMMENT '更新者', +`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', +INDEX `idx_batch_id` (`batch_id`), +INDEX `idx_from_user_id` (`from_user_id`), +INDEX `idx_to_user_id` (`to_user_id`), +INDEX `idx_status` (`status`), +INDEX `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='转赠申请表(按批次聚合审核)'; + +-- 如果表已存在但缺少 box_record_id 列,执行以下语句: +-- ALTER TABLE `tt_transfer_apply` ADD COLUMN `box_record_id` BIGINT NULL COMMENT '背包记录ID(tt_box_records.id)' AFTER `id`; + +-- ============================================================ +-- 菜单配置(请根据实际数据库中的父菜单ID调整 parent_id) +-- 查找"三方发货链路"父菜单ID:SELECT menu_id, menu_name FROM sys_menu WHERE menu_name LIKE '%三方发货%' OR menu_name LIKE '%发货%'; +-- ============================================================ + +-- 新增"转赠申请"菜单项(menu_type=C 表示菜单) +-- 注意:parent_id 需替换为实际的"三方发货链路"目录菜单ID +-- component 路径对应 ruoyi-ui/src/views/skins/delivery/transferApply.vue +INSERT INTO `sys_menu` ( +`menu_name`, +`parent_id`, +`order_num`, +`path`, +`component`, +`is_frame`, +`is_cache`, +`menu_type`, +`visible`, +`status`, +`perms`, +`icon`, +`create_by`, +`create_time`, +`remark` +) +VALUES +( +'转赠申请', +( SELECT menu_id FROM ( SELECT menu_id FROM sys_menu WHERE menu_name LIKE '%三方发货%' LIMIT 1 ) AS tmp ),-- 替换为实际的父菜单ID +10, +'transferApply', +'skins/delivery/transferApply', +1, +0, +'C', +'0', +'0', +'skins:delivery:transferApply', +'transfer', +'admin', +NOW( ), +'转赠申请管理页面' +);-- 新增按钮权限 +-- 同意按钮 +INSERT INTO `sys_menu` ( +`menu_name`, +`parent_id`, +`order_num`, +`path`, +`component`, +`is_frame`, +`is_cache`, +`menu_type`, +`visible`, +`status`, +`perms`, +`icon`, +`create_by`, +`create_time`, +`remark` +) +VALUES +( +'转赠审核通过', +( SELECT menu_id FROM ( SELECT menu_id FROM sys_menu WHERE menu_name = '转赠申请' LIMIT 1 ) AS tmp ), +1, +'', +NULL, +1, +0, +'F', +'0', +'0', +'skins:delivery:transferApply:approve', +'#', +'admin', +NOW( ), +'' +);-- 驳回按钮 +INSERT INTO `sys_menu` ( +`menu_name`, +`parent_id`, +`order_num`, +`path`, +`component`, +`is_frame`, +`is_cache`, +`menu_type`, +`visible`, +`status`, +`perms`, +`icon`, +`create_by`, +`create_time`, +`remark` +) +VALUES +( +'转赠审核驳回', +( SELECT menu_id FROM ( SELECT menu_id FROM sys_menu WHERE menu_name = '转赠申请' LIMIT 1 ) AS tmp ), +2, +'', +NULL, +1, +0, +'F', +'0', +'0', +'skins:delivery:transferApply:reject', +'#', +'admin', +NOW( ), +'' +); + +-- 新增"已处理转赠申请"菜单项(与"转赠申请"同级) +-- component 路径对应 ruoyi-ui/src/views/skins/delivery/transferApplyProcessed.vue +INSERT INTO `sys_menu` ( +`menu_name`, +`parent_id`, +`order_num`, +`path`, +`component`, +`is_frame`, +`is_cache`, +`menu_type`, +`visible`, +`status`, +`perms`, +`icon`, +`create_by`, +`create_time`, +`remark` +) +VALUES +( +'已处理转赠申请', +( SELECT menu_id FROM ( SELECT menu_id FROM sys_menu WHERE menu_name LIKE '%三方发货%' LIMIT 1 ) AS tmp ), +11, +'transferApplyProcessed', +'skins/delivery/transferApplyProcessed', +1, +0, +'C', +'0', +'0', +'skins:delivery:transferApplyProcessed', +'transfer', +'admin', +NOW( ), +'已处理转赠申请管理页面' +); + +-- 全服背包饰品批量分解定时任务(每天凌晨2点,分解所有用户背包中的饰品,等额返还金币和积分,跳过正在提货中的饰品和机器人用户) +-- 分解记录通过 tt_user_blend_ercash 流水表记录(source=10 背包分解饰品收入) +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('全服背包饰品分解', 'DECOMPOSE', 'boxRecordsDecomposeTask.decomposeAllUserPackSack()', '0 0 2 * * ?', '3', '1', '1', 'admin', NOW(), '每天凌晨2点分解所有用户背包中的饰品(status IN 0,2),等额增加金币和积分,写入分解流水记录;跳过正在提货中的饰品和机器人用户'); + +-- ========== 转赠记录增加状态列 ========== +-- 前端「赠送记录」三态:0-审核中,1-成功,2-失败 +ALTER TABLE `tt_box_transfer_record` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 0 COMMENT '转赠状态:0-审核中,1-成功,2-失败'; +ALTER TABLE `tt_box_transfer_record` ADD INDEX `idx_status` (`status`); + +-- 删除唯一索引:同一个饰品可多次转赠给同一副号(如驳回后再转赠),每次应独立成记录 +ALTER TABLE `tt_box_transfer_record` DROP INDEX `idx_src_dst_boxid`; + +-- 对战机器人爆率分离:新增欧皇/非酋独立爆率字段 +ALTER TABLE tt_box_ornaments ADD COLUMN robot_odds_lucky INT DEFAULT 0 COMMENT '机器人欧皇模式爆率'; +ALTER TABLE tt_box_ornaments ADD COLUMN robot_odds_unlucky INT DEFAULT 0 COMMENT '机器人非酋模式爆率'; + +-- 数据回填:初始值等于原 robot_odds +UPDATE tt_box_ornaments SET robot_odds_lucky = robot_odds, robot_odds_unlucky = robot_odds; + +-- ========== VIP 等级新增每日转赠次数配置 ========== +ALTER TABLE tt_vip_level ADD COLUMN transfer_count INT NOT NULL DEFAULT 0 COMMENT '达到该VIP等级后每日可转赠次数'; + +-- ========== VIP 等级新增每日提货次数配置 ========== +ALTER TABLE tt_vip_level ADD COLUMN delivery_count INT NOT NULL DEFAULT 0 COMMENT '达到该VIP等级后每日可提货次数'; + +-- ========== 用户每日转赠次数记录 ========== +-- 按 (用户ID, VIP等级, 日期) 记录,升级后不同 VIP 等级的次数独立计算 +CREATE TABLE IF NOT EXISTS tt_user_transfer_daily ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + user_id INT NOT NULL COMMENT '用户ID', + vip_level INT NOT NULL DEFAULT 0 COMMENT '提交/生效时的VIP等级(tt_vip_level.id)', + stat_date DATE NOT NULL COMMENT '统计日期', + total_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日转赠次数上限', + used_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日已成功转赠次数', + pending_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日审核中预扣次数', + delivery_total_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日提货次数上限', + delivery_used_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日已成功提货次数', + delivery_pending_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日审核中提货预扣次数', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (id), + UNIQUE KEY uk_user_vip_date (user_id, vip_level, stat_date), + INDEX idx_stat_date (stat_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户每日转赠/提货次数记录(按VIP等级隔离)'; + +-- 若此前已按旧结构 (user_id, stat_date) 建表,执行以下迁移语句 +-- ALTER TABLE tt_user_transfer_daily DROP PRIMARY KEY; +-- ALTER TABLE tt_user_transfer_daily ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; +-- ALTER TABLE tt_user_transfer_daily ADD UNIQUE KEY uk_user_vip_date (user_id, vip_level, stat_date); + +-- ========== 转赠/提货次数拆分上线初始化 ========== +-- 1. 把 VIP 等级表中的 delivery_count 初始化为和 transfer_count 一致(避免上线后用户无法提货) +UPDATE tt_vip_level SET delivery_count = transfer_count WHERE delivery_count = 0; + +-- 2. 把已有每日次数记录中的 delivery_total_count 初始化为对应 VIP 等级的 delivery_count +UPDATE tt_user_transfer_daily t +JOIN tt_vip_level v ON t.vip_level = v.id +SET t.delivery_total_count = v.delivery_count +WHERE t.delivery_total_count = 0; + +-- 转赠申请表记录提交时的 VIP 等级,审核时按该等级扣/返次数 +ALTER TABLE tt_transfer_apply ADD COLUMN vip_level INT NOT NULL DEFAULT 0 COMMENT '提交转赠时的VIP等级'; + +-- 转赠申请表增加批次号,同一批次内的饰品需整体审核通过/驳回 +ALTER TABLE tt_transfer_apply ADD COLUMN batch_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '转赠批次号(同一批次内多个饰品一起审核)'; +ALTER TABLE tt_transfer_apply ADD INDEX idx_batch_id (batch_id); + +-- 用户表新增赠送状态:0=启用(可被赠送),1=禁用(不可被赠送) +ALTER TABLE tt_user ADD COLUMN gift_status VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '赠送状态:0=启用,1=禁用'; +## 极速糖果领取流程简化 - 存量数据迁移 + +### 部署前执行(处理待决定记录) + +```sql +-- 1. 查看待处理记录数量(先确认影响范围) +SELECT COUNT(*) AS pending_count FROM game_sweet_record +WHERE settled = 0 AND auto_dismantle = 0 AND prize_name IS NOT NULL; + +-- 2. 将待处理记录标记为已结算(reward_type=ornament) +UPDATE game_sweet_record +SET settled = 1, + reward_type = 'ornament', + settle_source = 'data_migration' +WHERE settled = 0 + AND auto_dismantle = 0 + AND prize_name IS NOT NULL; + +-- 3. 补写背包记录(对于有 prize_id 的记录写入 tt_box_records) +INSERT INTO tt_box_records (user_id, holder_user_id, box_id, box_name, ornament_id, + ornament_name, ornaments_price, image_url, ornaments_level_id, source, status, is_show, create_time) +SELECT user_id, user_id, box_id, 'SWEET', prize_id, prize_name, settled_win, + prize_image_url, 1, 11, 0, 1, NOW() +FROM game_sweet_record +WHERE settle_source = 'data_migration' AND prize_id IS NOT NULL; + +-- 4. 验证迁移结果 +SELECT COUNT(*) AS migrated_records FROM game_sweet_record WHERE settle_source = 'data_migration'; +SELECT COUNT(*) AS new_box_records FROM tt_box_records WHERE source = 11 AND DATE(create_time) = CURDATE(); +``` + +新增推广后台维护配置 +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('推广后台维护', 'promoMaintenance', '0', 'Y', 'admin', NOW(), '推广后台维护开关 0正常 1维护'); + + +-- 推广后台-Roll房中奖详情接口性能优化:tt_box_records 复合索引 +-- 查询模式:WHERE roll_id = ? AND source = ? ORDER BY create_time(等值列在前、排序列在后) +-- 执行前先 SHOW INDEX FROM tt_box_records; 确认无等价索引;大表请加索引在低峰期执行 +ALTER TABLE tt_box_records +ADD INDEX idx_tbr_roll_source_time (roll_id, source, create_time); + +-- 疯狂转盘全服中奖记录过滤白号和机器人 +ALTER TABLE game_wheel_round_bet ADD INDEX idx_win_filter (win, user_id); +-- 优化查询性能:WHERE win > 0 AND user 状态过滤 + + +-- VIP归档兼容性修复:tt_user增加累计充值缓存字段(避免归档后VIP归零和未充值误判) +ALTER TABLE tt_user ADD COLUMN total_recharge_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00 COMMENT '累计充值金额(缓存,充值时原子递增)'; + +-- 初始化回填:从主表+归档表计算历史充值总额(仅更新有充值记录的用户,避免全表扫描) +UPDATE tt_user u SET u.total_recharge_amount = ( + SELECT COALESCE(SUM(sub.total), 0) FROM ( + SELECT total FROM tt_user_blend_ercash WHERE user_id = u.user_id AND type = 0 AND source = 1 + UNION ALL + SELECT total FROM tt_user_blend_ercash_archive WHERE user_id = u.user_id AND type = 0 AND source = 1 + ) sub +) +WHERE EXISTS ( + SELECT 1 FROM tt_user_blend_ercash WHERE user_id = u.user_id AND type = 0 AND source = 1 +) OR EXISTS ( + SELECT 1 FROM tt_user_blend_ercash_archive WHERE user_id = u.user_id AND type = 0 AND source = 1 +); + +-- 2026-07-22 修复CDK兑换重复检查并发漏洞,添加唯一索引兄底 +ALTER TABLE promo_cdkey_redeem +ADD UNIQUE KEY uk_cdkey_redeemer (cdkey_id, redeemer_id); + +-- 2026-07-22 修复口令红包重复领取漏洞,添加唯一索引兄底 +ALTER TABLE tt_red_packet_record +ADD UNIQUE KEY uk_packet_user (red_packet_id, user_id); + +-- 2026-07-22 后台菜单:在“cdk管理”目录下新增“推广CDK记录”子菜单 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT '推广CDK记录', m.parent_id, 4, 'promoCdkeyRedeem', 'skins/promoCdkeyRedeem/index', 1, 0, 'C', '0', '0', '', '#', 'admin', NOW(), '推广后台CDKEY兑换记录' +FROM (SELECT parent_id FROM sys_menu WHERE component = 'skins/ttGenerateCard/index' LIMIT 1) m; + + +-- 2026-07-22 百佳与历史接口性能优化索引 +ALTER TABLE tt_box_records ADD INDEX idx_tbr_fight_id (fight_id); +ALTER TABLE tt_fight ADD INDEX idx_ttf_status_model_endtime (status, model, end_time); +ALTER TABLE tt_fight_user ADD INDEX idx_tfu_user_fight (user_id, fight_id); + +-- 2026-07-24 极速糖果一轮内RTP动态收敛:箱子新增收敛回合占比(空=默认0.15,逐箱可配) +ALTER TABLE game_sweet_box ADD COLUMN rtp_converge_ratio DECIMAL(4,3) NULL COMMENT '一轮收敛回合占比,空=默认0.15'; + +-- 2026-07-25 abkpay:接入新代收支付渠道并纳入统一支付权重分配 +-- 说明:abkpay 是否被 /api/recharge/unifiedPay 路由选中由本配置决定(PayCompanyRouter 读取 payCompanyWeights); +-- 不在列表或 weight<=0 视为停用;weight 按需调整;payType 可选(1=支付宝,2=银联,不填=全渠道,abkpay 不限渠道故不填)。 +-- 注意:本语句为整段 JSON 覆盖,若线上权重已被运营调整过,请以现有值为基础追加 {"name":"abkpay","weight":N} 项后再执行。 +-- 示例:chengduhipay:5、richku:3(仅支付宝)、itpay:2、abkpay:2 +UPDATE sys_config +SET config_value = '[{"name":"chengduhipay","weight":5},{"name":"richku","weight":3,"payType":1},{"name":"itpay","weight":2},{"name":"abkpay","weight":2}]' +WHERE config_key = 'payCompanyWeights'; + +-- 2026-07-25 支付路由按金额分层调整:<300 默认走 itpay(取代原“支付宝<300强制 richku”规则),>=300 按权重分配; +-- abkpay 内部再按金额选产品编码(yml 配置:300<=金额<800 用 wayCode=8012,金额>=800 用 wayCodeHigh=8003)。 +-- 注意:itpay 权重必须>0,否则 <300 默认规则失效回落到权重选择;itpay 的权重同时决定其在 >=300 流量中的占比。 +-- 示例:itpay:2、abkpay:8 → <300 全部 itpay;>=300 按 2:8 分流(若希望 >=300 基本全走 abkpay,可改配 itpay:1、abkpay:9) +UPDATE sys_config +SET config_value = '[{"name":"itpay","weight":2},{"name":"abkpay","weight":8}]' +WHERE config_key = 'payCompanyWeights'; + +-- 2026-07-25 VIP等级奖励防重复领取修复(配合代码:归档任务 selectArchiveIds 永久跳过 source=30) +-- 背景:VIP等级达标奖励(source=30)的领取判重只查主表 tt_user_blend_ercash, +-- 领取流水被归档后主表查不到,玩家重新登录可重复领取。 +-- 方案:领取凭证永久保留在主表;存量已归档的凭证从归档表搬回主表(同一事务先插后删,避免 UNION 双算)。 +-- 【踩坑订正】TtAccountRecordType 枚举:INPUT(收入)=1、OUTPUT(支出)=0,与直觉相反! +-- 初版 SQL 误写 type=0 导致回填 0 行;凭证是收入流水,此处直接不限 type(source=30 只会以收入入账)。 +START TRANSACTION; + +-- 1) 回填主表(id 与归档表一致,IGNORE 防止极端情况下主键冲突报错) +INSERT IGNORE INTO tt_user_blend_ercash + (id, user_id, amount, final_amount, credits, final_credits, total, source, type, remark, create_time, update_time) +SELECT id, user_id, amount, final_amount, credits, final_credits, total, source, type, remark, create_time, update_time +FROM tt_user_blend_ercash_archive +WHERE source = 30; + +-- 2) 从归档表删除,避免主表+归档表 UNION ALL 查询时同一条记录双算 +DELETE FROM tt_user_blend_ercash_archive +WHERE source = 30 + AND id IN (SELECT id FROM tt_user_blend_ercash WHERE source = 30); + +COMMIT; + +-- 排查:找出已重复领取的玩家(同一用户同一等级 remark 领取超过 1 次,跨主表+归档表),用于追损 +SELECT t.user_id, t.remark, COUNT(*) AS claim_count, SUM(t.amount) AS total_claimed, + MIN(t.create_time) AS first_claim, MAX(t.create_time) AS last_claim +FROM ( + SELECT user_id, remark, amount, create_time FROM tt_user_blend_ercash WHERE source = 30 + UNION ALL + SELECT user_id, remark, amount, create_time FROM tt_user_blend_ercash_archive WHERE source = 30 +) t +GROUP BY t.user_id, t.remark +HAVING COUNT(*) > 1 +ORDER BY claim_count DESC, total_claimed DESC; + + + +## 2026-07-26 机器人对战饰品清出机器人背包 + +-- 背景:对战结算时曾把整场 tt_box_records 统一置为 status=0(在背包显示),导致机器人(user_type=03) +-- 赢得的对战饰品进入机器人背包。代码已修复(FightStateManager.settleFight / ApiFightServiceImpl.fightEnd / +-- fightTask.refreshDayTask 三处结算排除机器人持有记录),此 SQL 清理存量:机器人持有的对战来源饰品 +-- 翻回 status=1(背包不显示),不影响前端对战结果展示(结果查询不按 status 过滤)。 +UPDATE tt_box_records tbr +JOIN tt_user tu ON tbr.holder_user_id = tu.user_id +SET tbr.status = 1, tbr.update_time = NOW() +WHERE tu.user_type = '03' + AND tbr.fight_id IS NOT NULL + AND tbr.status = 0; + +-- 验证:应返回 0 +SELECT COUNT(*) FROM tt_box_records tbr +JOIN tt_user tu ON tbr.holder_user_id = tu.user_id +WHERE tu.user_type = '03' AND tbr.fight_id IS NOT NULL AND tbr.status = 0; + + + +## 2026-07-26 招商周结防重复结算唯一索引 + +-- 背景:CommissionSettlementTask 幂等仅靠 countCommissionRecord 先查后插, +-- 存在并发窗口可能重复结算。数据库层面加唯一索引兜底: +-- 同一招商(user_id) + 同一结算周期起点(settlement_start_time) 只允许一条记录。 +-- 注意:执行前先用下方排查 SQL 确认无存量重复数据,否则建索引会失败。 + +-- 排查存量重复(应返回空) +SELECT user_id, settlement_start_time, COUNT(*) AS cnt +FROM tt_commission_record +GROUP BY user_id, settlement_start_time +HAVING COUNT(*) > 1; + +-- 新增唯一索引 +ALTER TABLE tt_commission_record + ADD UNIQUE INDEX uk_commission_user_period (user_id, settlement_start_time); + + + +## 2026-07-26 疯狂转盘注单表增加 date 列并重建唯一键 + +-- 背景:注单改为 doBet 扣款事务内即落库(settled=0),但 round_id 每日重置, +-- 原唯一键 uk_round_user_symbol(round_id,user_id,bet_symbol) 不含日期, +-- 跨天同 roundId+用户+符号会插入冲突导致下注失败。 +-- 增加 date 列(yyyyMMdd)并将唯一键改为 (date, round_id, user_id, bet_symbol)。 +-- 同步修复 selectByDateAndRoundId 改为直接按注单 date 列过滤。 + +-- 1. 增加 date 列 +ALTER TABLE game_wheel_round_bet + ADD COLUMN `date` varchar(10) NOT NULL DEFAULT '' COMMENT '所属日期yyyyMMdd(roundId每日重置,需与date联合定位回合)' AFTER `round_id`; + +-- 2. 回填存量数据(按创建时间推断所属日期) +UPDATE game_wheel_round_bet SET `date` = DATE_FORMAT(create_time, '%Y%m%d') WHERE `date` = ''; + +-- 3. 排查新唯一键的存量重复(应返回空;若有重复需先人工处理,否则建索引失败) +SELECT `date`, round_id, user_id, bet_symbol, COUNT(*) AS cnt +FROM game_wheel_round_bet +GROUP BY `date`, round_id, user_id, bet_symbol +HAVING COUNT(*) > 1; + +-- 4. 重建唯一键 +ALTER TABLE game_wheel_round_bet + DROP INDEX uk_round_user_symbol, + ADD UNIQUE INDEX uk_date_round_user_symbol (`date`, round_id, user_id, bet_symbol); + + +## 2026-07-27 修复用户积分为负数的存量数据 + +-- 背景:TtUserMapper.subtractAccountAmount 原 SQL 为 +-- set account_amount = account_amount - #{money}, +-- account_credits = LEAST(account_credits, account_amount - #{money}) +-- MySQL 的 SET 子句从左到右求值,第二个赋值中的 account_amount 已是扣减后的新值, +-- 实际执行等价于 LEAST(积分, 原金币 - 2×扣款额),单笔扣款超过金币余额一半时积分被扣成负数。 +-- 代码已修复(积分赋值前置 + GREATEST 兜底防负),存量负积分数据需执行以下 SQL 归零。 + +-- 排查存量负积分用户 +SELECT user_id, user_name, account_amount, account_credits +FROM tt_user +WHERE account_credits < 0; + +-- 修复:负积分归零 +UPDATE tt_user SET account_credits = 0 WHERE account_credits < 0; + + +## 2026-07-28 SnapPay代收渠道接入:新增后台开关配置 + +-- 背景:SnapPay(snappay168)代收渠道不参与 payCompanyWeights 权重池, +-- 充值金额 1000-50000 时强制路由到 SnapPay(ApiRechargeController.unifiedPay); +-- 本开关控制该强制路由:1=启用,0=停用(停用/下单失败时自动回落权重路由)。 +-- 后台入口:推广后台 → 网站设置 → 支付设置 → SnapPay开关(未执行本SQL时页面保存会自动跳过该项,代码兜底视为启用)。 +INSERT INTO sys_config (config_id, config_name, config_key, config_value, config_type, create_by, create_time, remark) +SELECT 139, 'SnapPay开关', 'snappayEnabled', '1', 'Y', 'admin', NOW(), 'SnapPay代收开关:1=启用(1000-50000强制走SnapPay),0=停用(回落权重路由)' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE config_key = 'snappayEnabled'); + + +## 2026-07-30 对战新增2v2组队模式 + +-- 背景:欧皇/非酋对战新增2v2组队玩法。座位0/1为一队、座位2/3为二队, +-- 按队伍累计掉落总价值判胜(欧皇高者胜、非酋低者胜); +-- 胜方两人各自保留自己的掉落,并平分败方掉落总价值(复用平分奖机制); +-- 败方仅获得安慰奖(fightLoserPrize 配置)。 +-- team_mode:0或NULL=个人对战(存量数据不受影响),1=2v2组队(创建时强制 player_num=4)。 +ALTER TABLE tt_fight ADD COLUMN team_mode TINYINT DEFAULT 0 COMMENT '组队模式:0=个人对战 1=2v2组队(座位0/1一队、座位2/3二队)'; + + +## 2026-07-31 Olympus 电子游戏模块建表 + +-- 背景:新增 Olympus 消除类电子游戏(类 sweet 体系)。全量 DDL 见 olympus-py/schema.sql,已在本地开发库执行。 +-- 表清单(11张,前缀 game_olympus_):box / ornament(17档桶 0x~1000x 含种子) / box_ornament(含 used) +-- / round / step / record(建表即带 settled 等结算字段) / user / win / prize_ornament / prize_range / box_template +-- 设计要点:三模式互斥 normal|ante|buy_fs(CM=1/2/20);单局封顶 1000x; +-- step 表自带 accumulator_before/after(跨回合炸弹累计)与 win_capped(封顶截断标记)。 +-- Java 服务接入时再执行(语句注释在 schema.sql 尾部): +-- sys_config 新增 olympusMaintenance 维护开关。 + + +## 2026-08-01 Olympus Java 服务接入(game-olympus-service) + +-- 背景:olympus Java 游戏服务已建成并编译通过。以下为上线需执行的配置。 +-- 账户流水源:TtAccountRecordSource 新增 OLYMPUS_SPIN(60,消费) / OLYMPUS_REWARD(61,奖励); +-- 背包源 TtboxRecordSource 新增 OLYMPUS_BOX(13)。(枚举已在代码中,无需 SQL) + +-- 1) 维护开关 +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('奥林匹斯维护', 'olympusMaintenance', '0', 'Y', 'admin', NOW(), '奥林匹斯维护开关 0正常 1维护'); + +-- 2) 后台菜单(ruoyi-ui 动态菜单,component 对应 src/views/game/olympus/*/index.vue) +-- 自动挂到“游戏管理”下(与极速糖果同级);定位失败则挂到顶级根目录,可在后台菜单管理里拖动。 +SET @sweet_dir := (SELECT parent_id FROM sys_menu WHERE component = 'game/sweet/box/index' LIMIT 1); +SET @game_root := (SELECT parent_id FROM sys_menu WHERE menu_id = @sweet_dir LIMIT 1); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('奥林匹斯', IFNULL(@game_root, 0), 5, 'olympus', '', 1, 0, 'M', '0', '0', '', 'star', 'admin', NOW(), '奥林匹斯电子游戏管理'); +SET @oly_dir := LAST_INSERT_ID(); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES +('箱子配置', @oly_dir, 1, 'box', 'game/olympus/box/index', 1, 0, 'C', '0', '0', 'game:olympus:box', 'shopping', 'admin', NOW(), '奥林匹斯箱子配置与身份桶分布'), +('饰品列表', @oly_dir, 2, 'ornament', 'game/olympus/ornament/index', 1, 0, 'C', '0', '0', 'game:olympus:ornament', 'star', 'admin', NOW(), '奥林匹斯 17 档饰品桶定义'), +('回合管理', @oly_dir, 3, 'round', 'game/olympus/round/index', 1, 0, 'C', '0', '0', 'game:olympus:round', 'documentation', 'admin', NOW(), '奥林匹斯预生成回合查询'), +('奖池水位', @oly_dir, 4, 'rewardPool', 'game/olympus/rewardPool/index', 1, 0, 'C', '0', '0', 'game:olympus:pool', 'money', 'admin', NOW(), '奥林匹斯按箱奖池水位(注水/抽水/批量/重置)'), +('玩家记录', @oly_dir, 5, 'record', 'game/olympus/record/index', 1, 0, 'C', '0', '0', 'game:olympus:record', 'list', 'admin', NOW(), '奥林匹斯游戏记录'), +('每日统计', @oly_dir, 6, 'dailywin', 'game/olympus/dailywin/index', 1, 0, 'C', '0', '0', 'game:olympus:win', 'chart', 'admin', NOW(), '奥林匹斯每日盈亏统计'), +('奖品管理', @oly_dir, 7, 'prize', 'game/olympus/prize/index', 1, 0, 'C', '0', '0', 'game:olympus:prize', 'gift', 'admin', NOW(), '奥林匹斯返奖饰品与区间配置'); + +-- 前端页面文件(已在 ruoyi-ui 创建,7 页对齐糖果): +-- src/api/game/olympus/{box,ornament,round,rewardPool,record,dailywin,prize}/api.js +-- src/views/game/olympus/{box,ornament,round,rewardPool,record,dailywin,prize}/index.vue + +-- 3) 【返奖饰品】从极速糖果整套导入到奥林匹斯 +-- 幂等:先清空 olympus 两张返奖表再导入;会覆盖 olympus 现有返奖饰品配置。保留原 id 以保持区间映射。 +DELETE FROM game_olympus_prize_range; +DELETE FROM game_olympus_prize_ornament; + +INSERT INTO game_olympus_prize_ornament + (id, game_type, name, image_url, grade, exterior_name, sort, status, is_fallback, create_time) +SELECT id, 'OLYMPUS', name, image_url, grade, exterior_name, sort, status, is_fallback, NOW() +FROM game_sweet_prize_ornament; + +-- 糖果 range 用 ornament_id,olympus range 用 prize_id,一一对应 +INSERT INTO game_olympus_prize_range (prize_id, win_low, win_high) +SELECT ornament_id, win_low, win_high +FROM game_sweet_prize_range; + +-- 4) 【奖池水位调整日志】新增审计表 +CREATE TABLE IF NOT EXISTS game_olympus_reward_log ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + box_id BIGINT DEFAULT NULL COMMENT '箱子ID(批量/重置为汇总,可空)', + box_name VARCHAR(100) DEFAULT NULL COMMENT '箱子名/汇总描述', + type TINYINT NOT NULL COMMENT '1注水 2抽水 3重置', + amount DECIMAL(14,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额(绝对值)', + balance_before DECIMAL(14,2) DEFAULT NULL COMMENT '调整前水位', + balance_after DECIMAL(14,2) DEFAULT NULL COMMENT '调整后水位', + pool_balance DECIMAL(14,2) DEFAULT NULL COMMENT '操作后总池余额', + operator VARCHAR(64) DEFAULT NULL COMMENT '操作人', + remark VARCHAR(200) DEFAULT NULL COMMENT '备注', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_create (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus奖池水位调整日志'; +-- 本地也可跑:cd olympus-py && python _create_reward_log.py(需 DB 可连)。前端奖池水位页已对齐 bullet:说明面板/本轮RTP进度/收敛占比/备注/预估/调整后预览/调整历史。 +-- 【已建过该表的环境】补列(总池余额): +ALTER TABLE game_olympus_reward_log ADD COLUMN pool_balance DECIMAL(14,2) DEFAULT NULL COMMENT '操作后总池余额' AFTER balance_after; + +-- 5) 【奖池水位拆身份】奖池由「按箱 1 个池」改为「按 箱子 × 身份 4 个池」(common/anchor/robot/white) +-- bet/win 由 game_olympus_record + tt_user 按身份派生(口径同 OlympusAudience.resolve),历史数据自动分流,无需回填。 +ALTER TABLE game_olympus_reward_log ADD COLUMN audience VARCHAR(16) NOT NULL DEFAULT 'common' COMMENT '身份 common/anchor/robot/white' AFTER box_name; +ALTER TABLE game_olympus_reward_log ADD KEY idx_audience (audience); + +## 2026-08-01 经典开箱管理菜单(复用宝箱列表/开箱记录,按 isFight=1 / source=1 过滤) + +-- 背景:新增独立“经典开箱管理”菜单模块。复用现有页面 skins/ttBox/index 与 skins/ttBox/boxRecords, +-- 通过菜单 query 传 isFight=1(仅经典宝箱)与 source=1(仅经典开箱记录)过滤;不改数据模型。 +-- 说明:顶级目录 parent_id=0(可在后台“菜单管理”拖动到合适位置);两个子菜单 is_cache=1(不做 keep-alive 缓存), +-- 避免与原“宝箱管理”下同组件菜单共享实例导致 query 过滤失效。按钮权限沿用 admin:box:add / admin:box:edit。 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('经典开箱管理', 0, 8, 'classicBox', '', NULL, 1, 0, 'M', '0', '0', '', 'box', 'admin', NOW(), '经典开箱管理目录'); +SET @classic_dir := LAST_INSERT_ID(); +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES +('经典宝箱列表', @classic_dir, 1, 'classicBoxList', 'skins/ttBox/index', '{"isFight":"1"}', 1, 1, 'C', '0', '0', 'admin:box:list', 'list', 'admin', NOW(), '仅展示经典开箱宝箱(isFight=1)'), +('经典开箱记录', @classic_dir, 2, 'classicBoxRecords', 'skins/ttBox/boxRecords', '{"source":"1"}', 1, 1, 'C', '0', '0', '', 'documentation', 'admin', NOW(), '仅展示经典开箱记录(source=1)'); + + +-- 2026-08-05 对战宝箱查看奖项增加白号爆率列 +-- 对战宝箱饰品表增加白号数量字段 +ALTER TABLE tt_box_ornaments ADD COLUMN white_odds INT DEFAULT 0 COMMENT '白号数量(白号爆率池)'; + + +## 2026-08-01 经典开箱管理补充:经典宝箱分类 + 临时倍率迁移 +-- (type=0=对战分类、type=1=经典分类、不传=全部),boxType.vue 已加 $route 监听避免串味。 +SET @classic_dir := (SELECT menu_id FROM sys_menu WHERE menu_name = '经典开箱管理' AND menu_type = 'M' LIMIT 1); + +-- 1) 新增“经典宝箱分类”:复用 skins/ttBox/boxType,query type=1(仅经典分类 isFightType=1)。is_cache=1 不缓存。 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('经典宝箱分类', @classic_dir, 3, 'classicBoxType', 'skins/ttBox/boxType', '{"type":"1"}', 1, 1, 'C', '0', '0', '', 'component', 'admin', NOW(), '仅展示经典宝箱分类(isFightType=1)'); + +-- 2) 将“临时倍率”从“宝箱管理”迁移到“经典开箱管理”下(仅改归属与排序,组件/权限不变)。 +UPDATE sys_menu SET parent_id = @classic_dir, order_num = 4 WHERE menu_name = '临时倍率'; + + +## 2026-08-05 充值维护拆分为充值渠道维护 + 卡密充值维护 + +-- 业务原因 rechargeMaintenance 同时控制卡密充值 (cardPay) 和充值渠道 (unifiedPay) +-- 两个开关会同时生效,无法独立维护,互不影响。 + +-- 1) 将原"充值维护"重命名为"充值渠道维护",仅控制第三方支付渠道 unifiedPay +UPDATE sys_config +SET config_name = '充值渠道维护', + remark = '充值渠道维护开关 0 正常 1 维护(仅控制第三方支付渠道)' +WHERE config_key = 'rechargeMaintenance'; + +-- 2) 新增"卡密充值维护"配置,仅控制卡密充值 cardPay +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('卡密充值维护', 'cardRechargeMaintenance', '0', 'Y', 'admin', NOW(), '卡密充值维护开关 0 正常 1 维护(仅控制卡密充值)') +ON DUPLICATE KEY UPDATE config_value = config_value; + +## 2026-08-05 用户ID改为8位随机数字 + +-- 1) 取消 tt_user.user_id 的自增属性,改为程序生成 +ALTER TABLE tt_user MODIFY COLUMN user_id INT NOT NULL COMMENT '用户ID'; + +-- 2) 说明:新注册用户/后台生成账号的 user_id 将由程序生成 10000000~99999999 之间的8位随机数字,并保证不与已有用户ID及副号绑定记录重复 + +## 2026-08-06 白号管理支持生成白号用户 + +-- 1) 在 user_type 字典中新增“白号”选项(键值 06) +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, status, create_by, create_time, remark) +SELECT 6, '白号', '06', 'user_type', '0', 'admin', NOW(), '白号用户' +FROM dual +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'user_type' AND dict_value = '06'); + + +## 2026-08-06 对战房间房主昵称入库(用户被物理删除后仍可展示) + +-- 背景:owner_nick_name 原为 @TableField(exist=false) 不入库字段,每次查询时从 tt_user 实时填充。 +-- 用户被物理删除(forceDelete)后,tt_user 查不到,对战大厅/历史记录中的房主昵称显示为空。 +-- 现将 owner_nick_name 入库,创建对战时写入昵称快照;enrichOwnerNickNames 方法仅在用户存在时 +-- 用实时昵称覆盖(用户改了昵称能同步),用户被删除时保留入库快照。 +ALTER TABLE tt_fight ADD COLUMN owner_nick_name VARCHAR(100) DEFAULT NULL COMMENT '房主昵称快照(创建时写入,用户被物理删除后仍可展示)'; + +-- 回填存量数据:从 tt_user 把现有对战的房主昵称填入 owner_nick_name +UPDATE tt_fight tf +LEFT JOIN tt_user tu ON tf.user_id = tu.user_id +SET tf.owner_nick_name = COALESCE(tf.owner_nick_name, tu.nick_name) +WHERE tf.owner_nick_name IS NULL; + + +## 2026-08-06 流水抽奖系统建表及菜单 + +-- ========== 1. 流水抽奖活动表 ========== +CREATE TABLE IF NOT EXISTS `tt_turnover_lottery` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键 ID', + `lottery_name` VARCHAR(200) NOT NULL COMMENT '抽奖名称', + `status` INT DEFAULT 0 COMMENT '状态:0=关闭,1=开启', + `lottery_type` INT DEFAULT 1 COMMENT '类型:0=普通宝箱,1=流水抽奖', + `turnover_threshold` INT DEFAULT 3000 COMMENT '流水门槛(多少流水可抽1次)', + `image_url` VARCHAR(500) DEFAULT NULL COMMENT '宝箱封面图', + `sort` INT DEFAULT 0 COMMENT '排序', + `total_value` DECIMAL(18,2) DEFAULT NULL COMMENT '奖品总价值', + `profit_per_round` INT DEFAULT NULL COMMENT '每轮利润', + `profit_rate` DECIMAL(10,4) DEFAULT NULL COMMENT '利润率', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者', + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` INT DEFAULT 0 COMMENT '删除标志:0=存在,2=删除', + PRIMARY KEY (`id`), + KEY `idx_status` (`status`), + KEY `idx_sort` (`sort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流水抽奖活动表'; + +-- ========== 2. 流水抽奖奖品表 ========== +CREATE TABLE IF NOT EXISTS `tt_turnover_lottery_prize` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键 ID', + `lottery_id` BIGINT(20) NOT NULL COMMENT '关联抽奖活动 ID', + `ornament_id` BIGINT(20) DEFAULT NULL COMMENT '饰品 ID(已废弃,奖品改为自定义)', + `ornament_name` VARCHAR(200) DEFAULT NULL COMMENT '饰品名称/自定义名称', + `image_url` VARCHAR(500) DEFAULT NULL COMMENT '图片 URL', + `price` DECIMAL(18,2) DEFAULT NULL COMMENT '饰品价格', + `ornament_level` INT DEFAULT NULL COMMENT '饰品等级 ID(关联 tt_ornaments_level)', + `display_count` INT DEFAULT 10 COMMENT '展示数量(前端库存显示)', + `real_count` INT DEFAULT 10 COMMENT '真实数量(实际权重,当前剩余)', + `initial_real_count` INT DEFAULT 10 COMMENT '初始真实数量(重置奖池时恢复值)', + `sort` INT DEFAULT 0 COMMENT '展示排序', + `total_count` INT DEFAULT 0 COMMENT '已发放次数', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_lottery_id` (`lottery_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流水抽奖奖品表'; + +-- 已创建表的环境补充字段( ornaments_id 改为可空,新增 ornament_level、initial_real_count) +ALTER TABLE `tt_turnover_lottery_prize` + MODIFY COLUMN `ornament_id` BIGINT(20) DEFAULT NULL COMMENT '饰品 ID(已废弃,奖品改为自定义)', + ADD COLUMN `ornament_level` INT DEFAULT NULL COMMENT '饰品等级 ID(关联 tt_ornaments_level)', + ADD COLUMN `initial_real_count` INT DEFAULT 10 COMMENT '初始真实数量(重置奖池时恢复值)'; + +-- 历史数据初始化:已存在的奖品把初始真实数量同步为当前真实数量 +UPDATE `tt_turnover_lottery_prize` SET `initial_real_count` = `real_count` WHERE `initial_real_count` = 10; + +-- 2026-08-08 流水抽奖奖品新增磨损度字段(自定义饰品可配置磨损度,发放入背包时写入 tt_box_records.exterior_name) +ALTER TABLE `tt_turnover_lottery_prize` + ADD COLUMN `exterior_name` VARCHAR(20) DEFAULT NULL COMMENT '磨损度名称(崭新出厂/略有磨损/久经沙场/破损不堪/战痕累累/无涂装)'; + +-- ========== 3. 流水抽奖记录表 ========== +CREATE TABLE IF NOT EXISTS `tt_turnover_lottery_record` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键 ID', + `user_id` INT NOT NULL COMMENT '用户 ID', + `lottery_id` BIGINT(20) NOT NULL COMMENT '抽奖活动 ID', + `prize_id` BIGINT(20) DEFAULT NULL COMMENT '奖品 ID', + `ornament_id` BIGINT(20) DEFAULT NULL COMMENT '饰品 ID', + `ornament_name` VARCHAR(200) DEFAULT NULL COMMENT '饰品名称', + `image_url` VARCHAR(500) DEFAULT NULL COMMENT '图片 URL', + `ornaments_price` DECIMAL(18,2) DEFAULT NULL COMMENT '奖品价值', + `cost_turnover` INT DEFAULT NULL COMMENT '消耗的流水值', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_lottery_id` (`lottery_id`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流水抽奖记录表'; + +-- 2026-08-08 全服中奖记录接口(/api/turnoverLottery/globalRecords)性能优化 +-- 查询模式:WHERE lottery_id = ? AND ornaments_price >= ? ORDER BY create_time DESC +-- 单列 idx_lottery_id / idx_create_time 无法同时命中「按宝箱过滤 + 按时间倒序」,补复合索引 +-- 执行前先 SHOW INDEX FROM tt_turnover_lottery_record; 确认无等价索引 +ALTER TABLE `tt_turnover_lottery_record` + ADD INDEX `idx_ttlr_lottery_ctime` (`lottery_id`, `create_time`); + +-- ========== 4. 后台菜单 ========== +-- 流水抽奖目录 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('流水抽奖', 0, 9, 'turnoverLottery', '', 1, 0, 'M', '0', '0', '', 'gift', 'admin', NOW(), '流水抽奖管理目录'); + +SET @lottery_dir := LAST_INSERT_ID(); + +-- 抽奖活动列表 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('抽奖活动', @lottery_dir, 1, 'lotteryList', 'turnoverLottery/list', 1, 0, 'C', '0', '0', 'system:turnoverLottery:list', 'list', 'admin', NOW(), '流水抽奖活动管理'); + +-- 抽奖记录查询 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('抽奖记录', @lottery_dir, 2, 'lotteryRecords', 'turnoverLottery/records', 1, 0, 'C', '0', '0', 'system:turnoverLotteryRecord:list', 'documentation', 'admin', NOW(), '流水抽奖记录查询'); + +-- 奖项配置页面 (隐藏菜单,通过抽奖活动列表“查看奖项”按钮跳转) +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('奖项配置', @lottery_dir, 3, 'prize', 'turnoverLottery/prize', 1, 0, 'C', '1', '0', 'system:turnoverLotteryPrize:list', '#', 'admin', NOW(), '流水抽奖奖项配置页面'); + +-- 按钮权限 (menu_type='F',不显示在左侧导航) +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES +('抽奖新增', @lottery_dir, 4, '', NULL, 1, 0, 'F', '1', '0', 'system:turnoverLottery:add', '#', 'admin', NOW(), ''), +('抽奖修改', @lottery_dir, 5, '', NULL, 1, 0, 'F', '1', '0', 'system:turnoverLottery:edit', '#', 'admin', NOW(), ''), +('抽奖删除', @lottery_dir, 6, '', NULL, 1, 0, 'F', '1', '0', 'system:turnoverLottery:remove', '#', 'admin', NOW(), ''); + +-- ========== 5. 消息分类字段 ========== +ALTER TABLE `tt_notice` + ADD COLUMN `category` VARCHAR(50) DEFAULT NULL COMMENT '消息分类:GAME_*/SYS_*'; + +ALTER TABLE `tt_announcement` + ADD COLUMN `category` VARCHAR(50) DEFAULT NULL COMMENT '消息分类:GAME_*/SYS_*'; + + +-- ========== 6. 转赠/提货次数数据库幂等字段 ========== +-- 用于防止 Redis 防重标记过期后重复扣/放次数 + +ALTER TABLE `tt_delivery_record` + ADD COLUMN `times_deducted` TINYINT NOT NULL DEFAULT 0 COMMENT '该批次次数是否已处理:0=未处理,1=已处理(扣次或释放)'; + +ALTER TABLE `tt_transfer_apply` + ADD COLUMN `times_deducted` TINYINT NOT NULL DEFAULT 0 COMMENT '该批次次数是否已处理:0=未处理,1=已处理(扣次或释放)'; + +-- 上线后:历史已审核订单的 times_deducted 默认为 0,但因为 Redis 标记可能已过期, +-- 如果再次对历史订单操作,需要先把已完结的批次标记为已处理,避免重复扣/放次数。 +-- 以下 SQL 把状态为已完成(10)或已取消(11)的提货批次标记为已处理: +UPDATE `tt_delivery_record` +SET `times_deducted` = 1 +WHERE `status` IN (10, 11) + AND `times_deducted` = 0; + +-- 以下 SQL 把状态为已通过(1)或已驳回(2)的转赠批次标记为已处理: +UPDATE `tt_transfer_apply` +SET `times_deducted` = 1 +WHERE `status` IN (1, 2) + AND `times_deducted` = 0; + + +-- ======================================================================== +-- 疯狂转盘(彩虹旋风)内圈玩法改造 DDL +-- 详见 docs/疯狂转盘内圈玩法设计.md +-- ======================================================================== +-- 执行前务必先选中数据库,否则会报 1046 - No database selected +USE `changzhou`; + +-- 1) 新建奖励矩阵表(方案B自定义饰品:10档 × 3内圈色 × 4外圈符号 = 120条) +CREATE TABLE `game_wheel_reward_matrix` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + `bet_amount` INT NOT NULL COMMENT '下注额层:5/10/20/50/100/200/500/1000/2000/5000', + `color` VARCHAR(16) NOT NULL COMMENT '内圈颜色:gold/red/purple', + `symbol` VARCHAR(8) NOT NULL COMMENT '外圈符号:A/B/C/D', + `multiplier` INT NOT NULL COMMENT '倍率(后台可配,12个跨下注额共用)', + `prize_value` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '价值=下注额×倍率(自动算)', + `prize_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '饰品名称(自定义)', + `prize_grade` INT NULL COMMENT '品质等级(自定义,对应tt_ornaments_level.id)', + `prize_exterior` VARCHAR(64) NULL COMMENT '磨损度(自定义)', + `prize_image` VARCHAR(500) NULL COMMENT '饰品图片(自定义)', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '0停用 1启用', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_combo` (`bet_amount`,`color`,`symbol`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='疯狂转盘奖励矩阵(自定义饰品120条)'; + +-- 2) 回合表 game_wheel_round 改造 +-- period_no:全局唯一期号 = date(yyyyMMdd) × 100000 + 当日序号(round_id),作为业务主键替代 date+round_id 复合键 +-- inner_colors:本轮内圈24色随机排布快照(外圈 symbols 已存在,为固定布局) +-- result 扩容以容纳4池结果JSON,如 {"real":3,"robot":10,"blank":7,"promo":15} +ALTER TABLE `game_wheel_round` + ADD COLUMN `period_no` BIGINT NOT NULL DEFAULT 0 COMMENT '全局唯一期号=date×100000+当日序号' AFTER `round_id`, + ADD COLUMN `inner_colors` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '本轮内圈24色随机排布快照' AFTER `symbols`, + MODIFY COLUMN `result` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '4池开奖结果JSON {real,robot,blank,promo}'; +-- 历史数据回填 period_no(date 为 yyyyMMdd 文本;新数据由代码直接写入 period_no) +UPDATE `game_wheel_round` SET `period_no` = CAST(`date` AS UNSIGNED) * 100000 + `round_id` WHERE `period_no` = 0; +ALTER TABLE `game_wheel_round` ADD UNIQUE KEY `uk_period_no` (`period_no`); + +-- 3) 下注表 game_wheel_round_bet 改造 +-- period_no:关联回合的全局唯一期号(查询/结算/判重单列定位) +-- color:下注内圈颜色;pool_type:落库定池 real/robot/blank/promo +-- multiplier/prize_id:下注时快照倍率与命中奖品矩阵行ID(防中途改配置影响已下注) +-- credits_cut:下注时实际扣减的积分额(退款按此原路退回;仅金币扣款时为0) +ALTER TABLE `game_wheel_round_bet` + ADD COLUMN `period_no` BIGINT NOT NULL DEFAULT 0 COMMENT '全局唯一期号' AFTER `round_id`, + ADD COLUMN `color` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '下注内圈颜色 gold/red/purple' AFTER `bet_symbol`, + ADD COLUMN `pool_type` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '决策池 real/robot/blank/promo', + ADD COLUMN `multiplier` INT NOT NULL DEFAULT 0 COMMENT '下注时快照倍率', + ADD COLUMN `prize_id` BIGINT NULL COMMENT '命中奖品矩阵行ID(game_wheel_reward_matrix.id)快照', + ADD COLUMN `credits_cut` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '下注实际扣减积分额(退款原路退回)'; +UPDATE `game_wheel_round_bet` SET `period_no` = CAST(`date` AS UNSIGNED) * 100000 + `round_id` WHERE `period_no` = 0; +ALTER TABLE `game_wheel_round_bet` ADD KEY `idx_period_no` (`period_no`); +-- 移除旧唯一键:新玩法每次下注独立一条、绝不累加(同格同额也各记一条), +-- 旧 uk_date_round_user_symbol 会阻止同用户同符号多注独立落库,必须删除 +ALTER TABLE `game_wheel_round_bet` DROP INDEX `uk_date_round_user_symbol`; + +-- 4) 账变来源:疯狂转盘退款(孤儿局无法开奖时静默退本金)在 TtAccountRecordSource 枚举新增来源码即可, +-- 账变写入 tt_user_blend_ercash 复用现有结构,无需额外DDL。 +-- 新玩法余额变动仅两类:下注扣款(LUCKLY_WHEEL_SPIN) 与 退本金(新增退款来源); +-- 中奖与安慰奖均发饰品进背包(source=WHEEL_BOX),不动余额。 + +-- 5) 奖励矩阵种子:生成 120 行骨架(10档 × 3色 × 4符号), +-- multiplier 按默认倍率、prize_value = bet_amount × multiplier 自动计算, +-- prize_name/prize_grade/prize_exterior/prize_image 留空由后台配置(本 INSERT 为一次性,uk_combo 保证不重复)。 +INSERT INTO `game_wheel_reward_matrix` + (`bet_amount`, `color`, `symbol`, `multiplier`, `prize_value`, `prize_name`, `prize_grade`, `prize_exterior`, `prize_image`, `status`) +SELECT b.amount, m.color, m.symbol, m.mult, b.amount * m.mult, '', NULL, NULL, NULL, '1' +FROM ( + SELECT 5 amount UNION ALL SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 50 UNION ALL SELECT 100 + UNION ALL SELECT 200 UNION ALL SELECT 500 UNION ALL SELECT 1000 UNION ALL SELECT 2000 UNION ALL SELECT 5000 +) b +CROSS JOIN ( + SELECT 'gold' color, 'A' symbol, 44 mult + UNION ALL SELECT 'gold','B',26 UNION ALL SELECT 'gold','C',11 UNION ALL SELECT 'gold','D',8 + UNION ALL SELECT 'red','A',33 UNION ALL SELECT 'red','B',21 UNION ALL SELECT 'red','C',8 UNION ALL SELECT 'red','D',6 + UNION ALL SELECT 'purple','A',27 UNION ALL SELECT 'purple','B',17 UNION ALL SELECT 'purple','C',7 UNION ALL SELECT 'purple','D',5 +) m; + +-- 6) 生产配置 sys_config(game.wheel.setting) 迁移为新结构 +-- 现有旧结构(symbols/symbol_scores/special_effects)已不适用新玩法。 +-- 最稳妥方式:删除该配置行,应用下次 newRound/getGameConfig 检测到缺失会 +-- 由 CommonWheelService 自动按新结构(内圈分布/外圈布局/12倍率/10档下注/奖池比例0.6/RTP0.9)重建,无需手拼 JSON。 +DELETE FROM `sys_config` WHERE `config_key` = 'game.wheel.setting'; + +-- 机器人对战进房时间配置表新增随机秒数字段 +-- 含义:在 join_seconds 基础上额外随机增加 1~random_seconds 秒,保证机器人错峰进房 +ALTER TABLE tt_robot_fight_join_config ADD COLUMN random_seconds INT NOT NULL DEFAULT 10 COMMENT '机器人进房随机偏移秒数(在join_seconds基础上额外随机增加1~randomSeconds秒)'; + +-- 机器人对战价格档位配置表 +-- 含义:控制机器人为每种对战模式自动创建房间时,各价格档位需要维持的目标房间数量 +CREATE TABLE `tt_robot_fight_price_config` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `price_limit` DECIMAL(15,2) NOT NULL COMMENT '价格上限(房间宝箱总金额 <= price_limit 命中该档位)', + `target_count` INT NOT NULL COMMENT '该档位目标房间数量', + `sort_order` INT NOT NULL COMMENT '排序序号(越小越靠前)', + `status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '状态 0启用 1停用', + `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_status_sort` (`status`, `sort_order`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机器人对战价格档位配置表'; + +-- 默认初始化数据,与原硬编码配置保持一致:200档6个、800档4个、1500档1个 +INSERT INTO `tt_robot_fight_price_config` (`price_limit`, `target_count`, `sort_order`, `status`, `remark`, `create_time`, `update_time`) +VALUES + (200.00, 6, 1, '0', '0-200金额房间', NOW(), NOW()), + (800.00, 4, 2, '0', '200-800金额房间', NOW(), NOW()), + (1500.00, 1, 3, '0', '800-1500金额房间', NOW(), NOW()); + +-- 机器人对战分组表新增价格档位配置ID字段 +-- 含义:每个机器人对战分组关联一个价格档位,用于按档位目标数量创建房间 +ALTER TABLE tt_robot_fight_group ADD COLUMN price_config_id INT DEFAULT NULL COMMENT '价格档位配置ID(关联 tt_robot_fight_price_config.id)'; + +-- ===================================================== +-- 提货次数管理功能 - 数据库迁移脚本 +-- 创建时间:2025-08-05 +-- 说明:添加批次号、申请时VIP等级、次数扣减状态字段 +-- ===================================================== + +-- ===================================================== +-- 幸运升级新增"幸运值"(累计原始概率)字段 +-- 说明:按用户类型区分,玩家每次拉取累加前端原始概率(如90),出货成功清0重置;返回给前端cocos展示 +-- ===================================================== +ALTER TABLE tt_upgrade_ornaments + ADD COLUMN probability_invested INT NOT NULL DEFAULT 0 COMMENT '普通用户累计原始概率(幸运值),出货成功清0', + ADD COLUMN anchor_probability_invested INT NOT NULL DEFAULT 0 COMMENT '主播累计原始概率(幸运值),出货成功清0'; + +-- ===================================================== +-- 幸运升级记录页:持久化每条记录的"幸运值"快照 + 分页查询索引 +-- 说明:tt_upgrade_ornaments.probability_invested 只保存"当前累计值", +-- 历史每条记录的幸运值需单独持久化;新增列后历史旧数据为 NULL(前端显示0) +-- ===================================================== +ALTER TABLE tt_upgrade_record + ADD COLUMN lucky_value INT DEFAULT NULL COMMENT '本次升级幸运值(累计原始概率)快照' AFTER probability; + +-- 记录分页排序索引(open_time 倒序) +-- 全服+当前饰品 +ALTER TABLE tt_upgrade_record ADD INDEX idx_upgrade_opentime (target_upgrade_id, open_time); +-- 我的+当前饰品 +ALTER TABLE tt_upgrade_record ADD INDEX idx_user_upgrade_opentime (user_id, target_upgrade_id, open_time); +-- 注:我的+全部饰品依赖已有 (user_id, open_time);全服+全部饰品依赖 open_time 排序 + +-- 第 1 个字段 +ALTER TABLE `tt_user_blend_ercash` +ADD COLUMN `anchor_id` INT COMMENT '上级主播 ID(消费时的直属上级主播)' AFTER `user_id`; + +-- 第 2 个字段 +ALTER TABLE `tt_user_blend_ercash` +ADD COLUMN `business_id` INT COMMENT '最终招商 ID(上级主播所属的招商)' AFTER `anchor_id`; + +-- 创建索引 +CREATE INDEX idx_business_commission ON tt_user_blend_ercash (business_id, create_time, source); +CREATE INDEX idx_anchor_commission ON tt_user_blend_ercash (anchor_id, create_time, source); + + +-- 1. 先检查字段是否已存在 +DESCRIBE tt_user_blend_ercash_archive; + +-- 2. 如果 anchor_id 和 business_id 列不存在,就执行下面的语句: +ALTER TABLE `tt_user_blend_ercash_archive` +ADD COLUMN `anchor_id` INT COMMENT '上级主播 ID' AFTER `user_id`; + +ALTER TABLE `tt_user_blend_ercash_archive` +ADD COLUMN `business_id` INT COMMENT '最终招商 ID' AFTER `anchor_id`; + +-- 3. 创建索引 +CREATE INDEX idx_business_commission_archive ON tt_user_blend_ercash_archive (business_id, create_time, source); +CREATE INDEX idx_anchor_commission_archive ON tt_user_blend_ercash_archive (anchor_id, create_time, source); + + +CREATE INDEX idx_olympus_record_box_settled ON game_olympus_record (box_id, settled); + + +-- 为 tt_box_records 表添加 period_no 字段,用于精确匹配转盘记录 +ALTER TABLE tt_box_records +ADD COLUMN period_no BIGINT NULL COMMENT '转盘期号(WHEEL专用)' AFTER box_price; + +-- 为 period_no 添加索引,加速查询 +CREATE INDEX idx_period_no ON tt_box_records(period_no); + + +-- 疯狂转盘奖池调节记录表 +CREATE TABLE IF NOT EXISTS game_wheel_pool_adjustment ( +id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', +pool_type VARCHAR(20) NOT NULL COMMENT '池类型(real/display)', +adjustment_amount DECIMAL(12,2) NOT NULL COMMENT '调整金额(正数=注水,负数=抽水)', +balance_before DECIMAL(12,2) NOT NULL COMMENT '调整前余额', +balance_after DECIMAL(12,2) NOT NULL COMMENT '调整后余额', +reason VARCHAR(200) DEFAULT NULL COMMENT '调整原因', +operator VARCHAR(50) DEFAULT NULL COMMENT '操作人', +create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', +INDEX idx_pool_type (pool_type), +INDEX idx_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疯狂转盘奖池调节记录'; + +-- ========== tt_user 新增安全密码字段(提货时校验,与登录密码 password 独立) ========== +ALTER TABLE tt_user ADD COLUMN safe_password VARCHAR(100) DEFAULT NULL COMMENT '安全密码(6位数字,BCrypt加密,提货时校验;NULL=未设置)'; +-- 安全密码明文:仅供后台用户详情展示与编辑回填(与 password 明文存 remark 同一约定) +ALTER TABLE tt_user ADD COLUMN safe_password_plain VARCHAR(16) DEFAULT NULL COMMENT '安全密码明文(仅后台展示/回填,不参与校验)'; + + +-- ========== 玫瑰女郎:删除 game_olympus_record.auto_dismantle(历史列,已无代码读写) ========== +-- 背景:自动分解改为「前端开关 + 末帧前端调 /api/userPackSack/decompose」,后端不再记录该快照, +-- 实体 OlympusRecord.autoDismantle 字段已删除;该列历史上也从未被写过非 0 值(旧 Redis 开关无入口),全为默认 0。 +-- 执行前确认列是否还在(MySQL 不支持 DROP COLUMN IF EXISTS,先查可避免 1091 报错): +SELECT COLUMN_NAME FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'game_olympus_record' AND COLUMN_NAME = 'auto_dismantle'; + +-- 确认存在后再执行: +ALTER TABLE game_olympus_record DROP COLUMN auto_dismantle; + + +-- ===================================================== +-- 幸运升级:白号独立幸运池(幸运值 + 独立幸运区间) +-- 背景:白号原先走"纯概率"分支(不写任何共享字段),但记录/界面里的幸运值读的是普通池 +-- (tt_upgrade_ornaments.probability_invested),导致白号看到的是别人的数(恒 0 或别人垫出来的值)。 +-- 现改为:白号有自己的幸运值列 + 自己的幸运区间,进度与判定都走白号池 +-- (Redis key 用 userType=06,白号之间共享这一个池,与宝箱白号池/奥林匹斯 white audience 口径一致)。 +-- blank_luck_section 为空时回退普通 luck_section(后台「幸运升级饰品」编辑接口已支持传该字段)。 +-- ===================================================== +ALTER TABLE tt_upgrade_ornaments + ADD COLUMN blank_luck_section VARCHAR(20) DEFAULT NULL COMMENT '白号幸运区间[min,max),NULL/空=回退普通luck_section', + ADD COLUMN blank_probability_invested INT NOT NULL DEFAULT 0 COMMENT '白号累计原始概率(幸运值),出货成功清0', + ADD COLUMN blank_amount_invested DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '白号本轮已投金额(实际扣款口径),出货成功清0', + ADD COLUMN blank_effective_amount_invested DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '白号本轮已投金额(真实计算口径),出货成功清0'; + +-- ===================================================== +-- 疯狂转盘:注单增加"是否退过本金"标记 +-- 背景:game_wheel_round_bet.settled 只有两态(0=未结算 / 1=已结算), +-- 正常发奖与兜底退本金在库里长得完全一样,事后无法逐注区分,客服/对账只能靠流水 source=64 反推。 +-- 退款时额外打 refunded=1(ApiWheelService.refundBets 内与 settled 一起 CAS 更新)。 +-- ===================================================== +ALTER TABLE game_wheel_round_bet + ADD COLUMN refunded TINYINT NOT NULL DEFAULT 0 COMMENT '是否走过退本金 0=否 1=是(settled=1 时用于区分发奖/退款)'; + + +-- ===================================================== +-- 亏损返水(玩家可见名:亏损返利)—— 每半个月给亏损玩家自动发返水 CDK +-- ===================================================== + +-- 1. 结算明细表(后台「CDK管理 - 亏损返水」页的数据源) +CREATE TABLE IF NOT EXISTS `tt_loss_rebate` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + `period` VARCHAR(32) NOT NULL COMMENT '期次 yyyy-MM-dd~yyyy-MM-dd', + `user_id` INT NOT NULL COMMENT '用户ID', + `user_name` VARCHAR(64) DEFAULT NULL COMMENT '用户账号', + `nick_name` VARCHAR(64) DEFAULT NULL COMMENT '用户昵称', + `bet_amount` DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '本期游戏投入', + `win_amount` DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '本期游戏产出', + `loss_amount` DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '净亏=投入-产出', + `rebate_amount` DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '应返水=净亏×比例(人工发放时可能被下调为实发金额)', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=待人工发放(超单期上限) 1=已自动发放 2=已人工发放 3=自动发放失败', + `red_packet_id` INT DEFAULT NULL COMMENT '关联 tt_red_packet.id', + `cdkey` VARCHAR(64) DEFAULT NULL COMMENT '发放的CDK口令', + `grant_time` DATETIME DEFAULT NULL COMMENT '发放时间', + `grant_by` VARCHAR(64) DEFAULT NULL COMMENT '发放人(自动发放为 lossRebate,人工发放为管理员账号)', + `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_period_user` (`period`, `user_id`), + KEY `idx_status` (`status`), + KEY `idx_period` (`period`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='亏损返水结算明细(亏损返利)'; + +-- 1b. ⚠ 排序规则必须跟库里其他表一致(tt_red_packet / tt_notice / tt_user_blend_ercash 都是 utf8mb4_general_ci)。 +-- 2026-09-16 首次建表时漏了 COLLATE,MySQL 8 默认给了 utf8mb4_0900_ai_ci, +-- 导致「tt_red_packet.description LIKE tt_loss_rebate.period」这类跨表字符串比较直接报 +-- Illegal mix of collations。(Java 绑定参数的查询不受影响,但跨表关联会炸,必须修。) +-- 已建过表的环境执行这一条;新建环境上面的 CREATE TABLE 已带 COLLATE,不用跑。 +ALTER TABLE tt_loss_rebate CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- 2. 后台菜单:CDK管理 → 亏损返水(父菜单按名字找;若报错先确认「CDK管理」的 menu_name) +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT '亏损返水', menu_id, 4, 'lossRebate', 'skins/ttredPacket/lossRebate', 1, 0, 'C', '0', '0', 'admin:lossRebate:list', 'money', 'admin', NOW(), '亏损返水结算明细,超单期上限的记录在此人工发放CDK' +FROM (SELECT menu_id FROM sys_menu WHERE menu_name = 'CDK管理' AND menu_type = 'M' LIMIT 1) m; + +-- 3. 注册定时任务(每月 1 日与 16 日 01:30,结算上一个自然半月) +INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES ('亏损返水结算', 'LOSS_REBATE', 'lossRebateService.generateHalfMonthLossRebate()', '0 30 1 1,16 * ?', '3', '1', '0', 'admin', NOW(), '每月1日与16日01:30结算上一个自然半月:返水≤上限自动发CDK+站内信,超上限转后台人工发放;白号/机器人不参与'); + +-- 4. 可选:比例 / 最低发放金额 / 单期上限 / CDK有效期。不建这四条也能跑,代码内兜底 5% / 1元 / 500元 / 30天。 +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('亏损返水比例', 'lossRebateRate', '0.05', 'N', 'admin', NOW(), '0.05=5%,玩家可见的「亏损返利」返水比例'), + ('亏损返水最低发放金额', 'lossRebateMinAmount', '1', 'N', 'admin', NOW(), '返水低于该金额不结算,避免几毛钱的码刷屏'), + ('亏损返水单期上限', 'lossRebateMaxAmount', '500', 'N', 'admin', NOW(), '返水超过该金额不自动发放,转「CDK管理-亏损返水」页人工发放;填 0 表示不设上限'), + ('亏损返利CDK有效期(天)', 'lossRebateValidDays', '30', 'N', 'admin', NOW(), 'CDK有效期 = 生成时刻 + N 天'); + +-- 5. 改名(回血红包 → 亏损返利):只在已建过旧数据的环境执行,新环境不用跑。 +-- 代码里红包标题=「亏损返利CDK」、站内信标题=「亏损返利」,执行后历史数据与新数据命名一致。 +UPDATE tt_red_packet SET title = '亏损返利CDK' WHERE title = '回血红包'; +UPDATE tt_notice SET title = '亏损返利' WHERE category = 'SYS_LOSS_REBATE' AND title = '回血红包'; +UPDATE sys_job SET job_name = '亏损返水结算' WHERE invoke_target = 'lossRebateService.generateHalfMonthLossRebate()' AND job_name = '亏损返水回血红包'; +UPDATE sys_config SET config_name = '亏损返利CDK有效期(天)' WHERE config_key = 'lossRebateValidDays' AND config_name = '回血红包有效期(天)'; + +-- 6. 亏损返利CDK 绑定收款人(只有指定用户能兑,口令被转发/截图也兑不走) +-- ⚠ 必须先执行这条 ALTER,再重启后端:新代码建红包时会写 receiver_user_id,列不存在会插入失败(服务本身能起来)。 +ALTER TABLE tt_red_packet + ADD COLUMN receiver_user_id INT DEFAULT NULL COMMENT '指定领取人用户ID:NULL=不限用户(平台/主播红包原有行为);非NULL=仅该用户可领(亏损返利CDK)'; + +-- 历史数据不用回填:这列是 NULL 时行为不变(不限用户)。 +-- 已发出的那几条测试返利红包如果也想绑上收款人,可以从明细表按 red_packet_id 关联回填: +-- UPDATE tt_red_packet p INNER JOIN tt_loss_rebate r ON r.red_packet_id = p.id SET p.receiver_user_id = r.user_id WHERE p.title = '亏损返利CDK' AND p.receiver_user_id IS NULL; \ No newline at end of file diff --git a/docs/幸运升级接口文档.md b/docs/幸运升级接口文档.md new file mode 100644 index 0000000..4e83b42 --- /dev/null +++ b/docs/幸运升级接口文档.md @@ -0,0 +1,340 @@ +# 幸运升级接口文档(Cocos 客户端对接) + +> 适用版本:当前 master。所有接口路径前缀 `/api/luckyUpgrade`(另有 1 个关联接口在 `/api/userPackSack`)。 +> 代码位置:`skins-service/service-playingmethod/.../ApiLuckyUpgradeController.java`、`ApiLuckyUpgradeServiceImpl.java`、`ApiUpgradeRecordServiceImpl.java`。 + +--- + +## 一、玩法机制(实现口径) + +### 1.1 一局流程 + +``` +选饰品位(upgradeId) + 选成功率(probability) + ↓ 后端按实时价算钱:price = 饰品价 × probability / 100 + ↓ 校验(身份 / 封禁 / 删除 / 未充值 703 / 余额 702) + ↓ 加锁(饰品位 + 奖池类型)→ 扣款(source=40 幸运升级消费) + ↓ 判定(见 1.2) + ├─ 成功:获得该饰品位对应的整件饰品,进度与幸运值清 0 + └─ 失败:获得后台配置的「失败奖励」饰品(安慰奖,可多件),进度与幸运值累加 + ↓ 写 tt_upgrade_record 记录(含各项快照)+ 背包 tt_box_records(source=4 幸运升级)+ 站内消息 + ↓ 返回升级记录(含 gainOrnamentList,可用于结果页立即分解) +``` + +### 1.2 消耗与判定公式 + +| 项 | 公式 / 取值 | +|---|---| +| 单次消耗 | `price = 饰品实时价(tt_ornament.use_price) × probability / 100`,保留 2 位小数(**前端不传价格**,由后端算,防 0 元 100% 中奖) | +| 判定概率 | `eff = min(probability, 75)`(**封顶 75**:选 90% 也只按 75% 判定,但钱按 90% 收) | +| 本轮空间 | `fullV`:从该饰品位对应奖池的幸运区间 `[min, max)` 里随机取 `nextInt(max-min) + min` | +| 判定 | 每拉一次 `进度 += eff`;`进度 + eff >= fullV` 即中奖,中奖后 `fullV` 重新随机、进度清 0;否则失败继续累加 | +| 推论 | **玩家为一件饰品付出的总成本 ≈ 饰品价 × fullV / 100**。所以幸运区间 = 该池的"售价百分比区间":`[50,250]` 表示平均要付 50%~250% 原价才出;压到个位数(如 `[9,10]`)等于必中、平台必亏 | + +### 1.3 幸运值与幸运区间:三套独立奖池 + +`幸运值` = 该池"累计原始概率",失败累加 `probability`(原始值,不是封顶后的 75)、中奖清 0;界面与记录里展示的就是它。 + +| 奖池 | 判定/显示用 userType | 幸运区间字段 | 幸运值字段 | 本轮已投金额字段 | +|---|---|---|---|---| +| 普通用户 | `02` | `tt_upgrade_ornaments.luck_section` | `probability_invested` | `amount_invested` / `effective_amount_invested` | +| 主播 | `01` | `anchor_luck_section` | `anchor_probability_invested` | `anchor_amount_invested` / `anchor_effective_amount_invested` | +| 白号 | `06`(`is_blank_account=1` 优先判定) | `blank_luck_section`(未配置回退 `luck_section`) | `blank_probability_invested` | `blank_amount_invested` / `blank_effective_amount_invested` | + +- 三池**完全隔离**:进度缓存在 Redis `upgrade_range_fixed:{饰品位}:{池类型}` / `upgrade_range:{饰品位}:{池类型}`,互不影响、互不阻塞(锁也是按「饰品位 + 池类型」)。 +- 同一池内是**共享进度**:同一个池子的玩家一起把进度推到 `fullV`,谁那一拉跨过阈值谁中奖(和普通/主播现有行为一致,白号同理,白号之间共享 06 池)。 +- 白号记录里的幸运值/已投金额读的都是白号自己的池,不再是真实玩家池的读数。 + +### 1.4 获得与失败奖励 + +- **成功**:获得 `tt_upgrade_ornaments.ornaments_id` 对应的整件饰品,`gain_ornaments_price` = 该饰品实时价。 +- **失败**:获得后台「幸运升级 → 失败奖励」为该饰品位配置的饰品(`tt_upgrade_fail_ornaments`,可多件,按 `ornament_number` 计数),`gain_ornaments_price` = 安慰奖总价值;该饰品位**没配失败奖励时啥都不给**(`gainOrnamentList` 为 `[]`、金额 0)。 +- 所有获得物都写入背包 `tt_box_records`(`source = 4` 幸运升级、状态 = 在背包)。 + +### 1.5 流水与返佣口径 + +| 场景 | 流水 source | +|---|---| +| 升级消费 | `40`(GOLD_DEVICE,「幸运升级消费」,输出) | +| 游戏内分解(结果页立即分解) | `41`(GOLD_DEVICE_REWARD,「幸运升级奖励」,输入) | +| 背包里分解(事后在背包页分解) | `10`(DECOMPOSE_ORNAMENT,输入) | + +- 消费 source 40 计入:今日/历史投注流水、综合消费排行榜、流水抽奖资格、**上级返佣**(按 `classicGameCommissionRate` 口径)、**VIP 流水返利**。 +- **白号例外**:白号消费不触发上级返佣、不触发 VIP 流水返利;后台充值/流水/记录类页面普遍按 `is_blank_account=1` 排除白号。 + +--- + +## 二、通用约定 + +- **域名**:`{host}`(例:`http://103.108.67.60:28081`)。 +- **Content-Type**:`application/json`;**鉴权**:除 `getOrnamentsList` 外都需要请求头带 token,401 时按项目统一逻辑弹登录。 +- **统一响应**: + +```json +{ "code": 200, "msg": "操作成功", "data": { } } +``` + +| code | 含义 | +|---|---| +| 200 | 成功(客户端以 `code === 200` 判定) | +| 401 | 未登录 / 登录过期,msg「登录过期,请重新登录。」 | +| 702 | 账户余额不足 | +| 703 | 玩家未充值不能游玩任何游戏 | +| 500 | 业务失败,msg 直接给玩家看 | + +- **维护开关**(`sys_config`,后台「网站设置」):`websiteMaintenance=1` → 「网站维护中......」;`luckyUpgradeMaintenance=1` → 「幸运升级维护中......」。两个开关都**先于登录校验**返回。 +- **通用拦截**(`upgrade` 接口):非 主播(01)/普通(02)/白号 → 「非法的用户类型」;`status=1` → 「账号已被封禁,无法参与游戏」;`del_flag != 0` → 「账号状态异常,无法参与游戏」。 +- 幸运升级相关也有 `/api/luckyUpgrade/**` 其他接口,本文件只覆盖下表这些。 + +--- + +## 三、接口清单 + +| 接口 | 说明 | 鉴权 | 分页 | +|---|---|---|---| +| `POST /api/luckyUpgrade/getOrnamentsList` | 可升级饰品列表(含当前幸运值) | 否(未登录按普通用户口径) | 无 | +| `POST /api/luckyUpgrade/upgrade` | 拉取一次(扣款 + 判定 + 发奖) | 是 | - | +| `POST /api/luckyUpgrade/recordPage` | 记录页:全服/我的 × 当前饰品/全部饰品 | 是 | 真分页 | +| `POST /api/luckyUpgrade/recentPulls` | 某饰品位最近 50 次拉取流水 | 是 | 无(固定 50) | +| `POST /api/luckyUpgrade/myHistoryRecord` | 我的记录(全部饰品,成败都返回) | 是 | 真分页 | +| `POST /api/luckyUpgrade/getUpgradeRecord` | 通用条件查询(内部/后台复用) | 是 | 真分页 | +| `POST /api/luckyUpgrade/tenTopUpgradeRecord` | 十佳掉落榜(按实际获得价值 Top10) | 是 | 无(固定 10) | +| `POST /api/userPackSack/decompose` | 分解获得的饰品(结果页或背包) | 是 | - | + +--- + +## 四、接口详情 + +### 4.1 `POST /api/luckyUpgrade/getOrnamentsList` — 可升级饰品列表 + +**请求**(全部可选,无分页、无排序参数,返回按 `use_price` 升序) + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | Integer | 可直接取某一个饰品位(`tt_upgrade_ornaments.id`) | +| type | String | 饰品类型数字字典码(1匕首 2手枪 3步枪 4微型冲锋枪 5重型武器 6手套 7印花 8其它);空串按不传处理 | +| itemName | String | 饰品名称模糊匹配 | +| priceMin / priceMax | BigDecimal | 按价格区间过滤(**只在传了 `priceMax` 时生效**;不传 `priceMin` 默认 0) | +| userType | String | **后端注入,客户端不要传**(用登录用户身份决定返回哪个池的幸运值) | + +```json +{ "type": "3", "itemName": "", "priceMax": 5000 } +``` + +**响应**:`data` 为数组,仅返回 `status='0'`(启用)的饰品位: + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | Integer | **可升级饰品ID**(后续 `upgrade.upgradeId`、`recordPage.upgradeId`、`recentPulls.upgradeRecordId` 都传它) | +| ornamentId | Long | 饰品ID(`tt_ornament.id`) | +| name | String | 饰品全名(优先展示) | +| itemName | String | 名称(与 name 同源字段) | +| usePrice | BigDecimal | 实时价格(= 计算消耗、展示目标金额的基准) | +| imageUrl | String | 饰品图片 | +| shortName / typeName / rarityName / exteriorName / levelImg | String | 简称 / 类型 / 稀有度 / 磨损度 / 品质图 | +| luckyValue | Integer | **当前幸运值**(按登录身份取对应池:普通/主播/白号) | + +### 4.2 `POST /api/luckyUpgrade/upgrade` — 拉取一次 + +**请求** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| upgradeId | Integer | 是 | 可升级饰品ID(`getOrnamentsList` 的 `id`) | +| probability | Integer | 是 | 本次选择的成功率,1~100;**判定按 min(概率,75),扣款按传入值** | + +```json +{ "upgradeId": 25, "probability": 10 } +``` + +**响应**:`data` = 本条升级记录(`tt_upgrade_record`),字段如下: + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | Long | 升级记录ID | +| userId / nickName / userType / userAvatar | - | 玩家信息(昵称是升级时快照) | +| probability | Integer | 本次选择的成功率(原始概率快照) | +| luckyValue | Integer | 本次升级时的幸运值快照(中奖为 0) | +| amountConsumed | BigDecimal | 本次实际消耗 | +| amountRequired | BigDecimal | 本轮目标金额快照 = 饰品价 × fullV% | +| amountInvested / effectiveAmountInvested | BigDecimal | 本轮已投金额(实际扣款口径 / 真实计算口径)快照 | +| effectiveAmount | BigDecimal | 真实计算金额 = 饰品价 × min(概率,75)% | +| targetUpgradeId | Integer | 可升级饰品ID | +| targetOrnamentId | Long | 目标饰品ID | +| targetOrnamentPrice | BigDecimal | 目标饰品价格 | +| isVictory | Boolean | 成败:true=升级成功 | +| gainOrnamentsPrice | BigDecimal | 本次获得饰品总价值(成功=目标饰品价,失败=安慰奖总价,可能为 0) | +| gainOrnamentList | Array | **本次实际获得物**(JSON 数组,不是字符串),每项:`id`=背包记录ID(`tt_box_records.id`)、`ornamentId`、`ornamentName`、`ornamentHashName`、`ornamentPrice`、`ornamentImgUrl`、`ornamentLevelId`、`ornamentLevelImg`、`ornamentNumber`;无获得时为 `[]` | +| openTime | String | 升级时间 `yyyy-MM-dd HH:mm:ss` | + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "id": 93, "userId": 34052018, "probability": 10, "luckyValue": 0, + "amountConsumed": 181.95, "amountRequired": 1819.52, "isVictory": true, + "gainOrnamentsPrice": 1819.52, + "gainOrnamentList": [ + { "id": 11530, "ornamentId": 1312995578, "ornamentName": "AK-47 | 表面淬火 (略有磨损)", + "ornamentPrice": 1819.52, "ornamentImgUrl": "/uploads/xxx.png", "ornamentLevelImg": "/profile/xxx.png" } + ], + "openTime": "2026-09-11 18:24:01" + } +} +``` + +**失败返回示例**:`{"code":500,"msg":"系统繁忙,请稍后重试。"}`(同池并发抢锁失败,客户端可直接重试);`{"code":703,"msg":"玩家未充值不能游玩任何游戏"}`;`{"code":702,"msg":"账户余额不足"}`;`{"code":500,"msg":"该饰品未在幸运升级开放。"}`。 + +**结果页立即分解**:取 `gainOrnamentList[i].id` 作为 `packSackIds`,调 4.8 接口(`decomposeType="game"`)。不分解就等于饰品留在背包里。 + +### 4.3 `POST /api/luckyUpgrade/recordPage` — 记录页(主力接口) + +**请求**(二维组合,共 4 种场景,均需登录) + +| 字段 | 类型 | 必填 | 默认 | 说明 | +|---|---|---|---|---| +| scope | Integer | 否 | 0 | 0=全服(不过滤用户) 1=我的 | +| itemScope | Integer | 否 | 0 | 0=全部饰品 1=当前饰品 | +| upgradeId | Long | 条件必填 | - | 可升级饰品ID;`itemScope=1` 时必填,为空返回 500;`itemScope=0` 时传了也忽略 | +| page | Integer | 否 | 1 | 页码(≥1) | +| size | Integer | 否 | 10 | 每页条数(1~20,超限参数错误) | + +```json +{ "scope": 1, "itemScope": 1, "upgradeId": 25, "page": 1, "size": 10 } +``` + +**响应**:`data` 为分页对象 `{ records, total, current, size, pages }`,`records` 按 `openTime` 倒序: + +| 字段 | 类型 | 说明 | +|---|---|---| +| userId / avatar / nickName / vipLevel | - | 玩家信息(昵称是升级时快照) | +| probability | Integer | 成功率(原始概率,如 89 表示 89%) | +| luckyValue | Integer | 幸运值快照(按该玩家所属池;白号是白号池的值) | +| amountConsumed | BigDecimal | 消耗 | +| gainOrnamentsPrice | BigDecimal | 获得饰品总价值 | +| isVictory | Boolean | 成败:true=升级成功(前端胜负样式用) | +| openTime | String | 升级时间 | +| targetOrnamentId | Long | 目标饰品ID | +| name / imageUrl / usePrice / exteriorName / levelImg | - | **本次实际获得的饰品**:成功=目标饰品,失败=安慰奖饰品(`gainOrnamentList` 首件);未配安慰奖的失败记录会回退成目标饰品且金额为 0,**胜负请用 isVictory 判断,不要看图片** | + +- 全服(scope=0)不过滤白号、机器人、封禁用户,白号可正常上榜。 +- 旧的 `recentPulls` / `myHistoryRecord` 场景都被本接口覆盖,可继续用但建议逐步切到本接口。 + +### 4.4 `POST /api/luckyUpgrade/recentPulls` — 某饰品位最近 50 次拉取流水 + +**请求**:`{ "upgradeRecordId": 25 }` + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| upgradeRecordId | Long | 是 | ⚠️ 字段名有历史误导性:**实际语义是「可升级饰品ID」`tt_upgrade_ornaments.id`**(`getOrnamentsList` 的 `id`)。为空返回 500「upgradeRecordId(升级饰品ID)不能为空」 | +| 其他字段 | - | 否 | `ornamentId`/`userType`/`userId`/`page`/`size`/`limit` 全部忽略 | + +- **无分页**:固定最近 **50** 条(SQL `LIMIT 50`),没有 `page`/`total`。 +- **不过滤**:全服所有人(含白号、机器人、封禁/删除用户),成功与失败都返回。 +- `nickName`/`avatar`/`vipLevel` 可能为 `null`(该记录的用户已不在 `tt_user`),客户端要能容错。 + +**响应**:`data` 直接是数组(不是分页对象): + +| 字段 | 类型 | 说明 | +|---|---|---| +| avatar / nickName / vipLevel | - | 拉取人信息 | +| probability | Integer | 本次拉取**选择的成功率**(不是中奖率) | +| luckyValue | Integer | 本次拉取时的**幸运值快照**(累计原始概率;成功为 0),与 `recordPage` 同口径 | +| isVictory | Boolean | 成败:true=升级成功 | +| name / imageUrl / usePrice / exteriorName / levelImg | - | **目标饰品**("谁在拉这个饰品",不是获得物) | +| openTime | String | 拉取时间 | + +### 4.5 `POST /api/luckyUpgrade/myHistoryRecord` — 我的记录(全部饰品) + +| 字段 | 类型 | 必填 | 默认 | 说明 | +|---|---|---|---|---| +| page | Integer | 否 | 1 | 页码 | +| size | Integer | 否 | 10 | 1~20 | + +**响应**:`data` 为分页对象,`records` 是 `tt_upgrade_record`(字段同 4.2 的响应,含 `gainOrnamentList`),仅当前登录用户、不含他人记录、成败都返回、按时间倒序;昵称/头像会补齐。 + +### 4.6 `POST /api/luckyUpgrade/getUpgradeRecord` — 通用条件查询 + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| page / size | Integer | **必传** | 不传会 500(内部直接用于构造分页对象) | +| upgradeRecordId | Long | 否 | 按记录ID精确查 | +| ornamentId | Long | 否 | 按目标饰品ID过滤 | +| userType | String | 否 | 按用户类型过滤(01/02/06…) | +| userId | Integer | 否 | 按用户过滤 | + +**响应**:`data` 为分页对象(`records` 为 `tt_upgrade_record`,`userAvatar` 已补齐,`gainOrnamentList` 空值兜底为 `[]`)。主要用于内部/后台复用,客户端记录页请用 `recordPage`。 + +### 4.7 `POST /api/luckyUpgrade/tenTopUpgradeRecord` — 十佳掉落榜 + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| isVictory | Boolean | 否 | `true` 只看成功、`false` 只看失败;不传则全部记录一起排 | + +**响应**:`data` 为分页对象(固定第 1 页、10 条),按 **`gainOrnamentsPrice`(实际获得价值)倒序**取 Top10 —— 即"实际掉落榜",不是目标饰品价值榜。 + +### 4.8 `POST /api/userPackSack/decompose` — 分解饰品(结果页/背包) + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| packSackIds | Long[] | 条件必填 | 背包记录ID(`tt_box_records.id`,即 `gainOrnamentList[i].id`);`isAll=true` 时可不传 | +| isAll | Boolean | 是 | 是否全选 | +| decomposeType | String | 否 | `game`=游戏结果页立即分解(按饰品来源记 `GOLD_DEVICE_REWARD(41)`);`backpack`=背包分解(记 `DECOMPOSE_ORNAMENT(10)`) | + +返回 `{"code":200,"msg":"成功分解N件饰品"}`。受 `websiteMaintenance` 开关拦截。 + +--- + +## 五、前端对接要点 + +1. **upgradeId 语义**:所有接口里的 `upgradeId` / `recentPulls.upgradeRecordId` 都是**可升级饰品ID**(`tt_upgrade_ornaments.id`),来自 `getOrnamentsList` 的 `id`,不是 `tt_ornament.id`,也不是升级记录ID。 +2. **扣款以服务端为准**:不要传价格;展示价格用 `usePrice × probability / 100`(与后端同公式,后端保留 2 位小数)。 +3. **概率封顶 75**:UI 上若提供 >75% 的档位,要提示"更高概率只加钱、不提升判定"。 +4. **幸运值怎么用**:主界面实时幸运值取 `getOrnamentsList.luckyValue`;拉取成功后用 `upgrade` 响应里的 `luckyValue` 立即覆盖,不必等列表刷新。记录里的幸运值是历史快照。 +5. **分池**:幸运值/幸运区间按 普通(02) / 主播(01) / 白号(06) 三套独立,白号看到的是白号池的值;同一池内进度是共享的,别人垫的进度也算在你头上。 +6. **分页**:`recordPage`/`myHistoryRecord` 的 `size` 上限 20;切换 tab(我的/全服、当前饰品/全部饰品)时 `page` 重置为 1。 +7. **失败记录展示**:以 `isVictory` 判断胜负,`name/imageUrl/...` 是"实际获得物"(失败=安慰奖);无安慰奖时金额为 0。 +8. **结果页分解**:`gainOrnamentList` 里的 `id` 就是分解入参;不调用 = 饰品留在背包。 +9. **并发**:同一饰品位 + 同一奖池同时拉取会串行,抢不到锁返回 500「系统繁忙,请稍后重试。」,可直接提示重试。 + +--- + +## 六、后台配置与调参 + +### 6.1 「幸运升级 → 饰品列表」(`tt_upgrade_ornaments`) + +| 后台列 | 字段 | 说明 | +|---|---|---| +| 普通用户 / 幸运区间 | `luck_section` | 普通用户池的 `[min, max)`,即"售价百分比区间" | +| 普通用户 / 本轮已投金额 | `amount_invested` | 普通池当前累计投入(运行时值,后台不可编辑回写) | +| 主播用户 / 幸运区间 | `anchor_luck_section` | 主播池区间 | +| 白号用户 / 幸运区间 | `blank_luck_section` | 白号池区间;为空则回退 `luck_section` | +| 白号用户 / 本轮已投金额 | `blank_amount_invested` | 白号池当前累计投入 | +| 状态 | `status` | 0=启用(=可升级、会出现在客户端列表) 1=禁用 | + +- 调难度就调"幸运区间":区间 = 玩家为该饰品付出的**价格百分比**。想更容易出就整体下调(如 `[80,280]`→`[60,200]`);**不要压到个位数**(如 `[9,10]` 等于必中,等于按 9% 价格卖货),也不要配成 `min == max`(会被后端防护强制改成 `[0,160]`)。 +- **改完区间要清缓存才立即生效**:`DEL upgrade_range:{饰品位}:{池}` + `DEL upgrade_range_fixed:{饰品位}:{池}`(池=01/02/06)。不清的话当前这一轮仍按旧空间跑,等本轮中奖后自动按新配置重建。Redis 实测在 db0,密码见 `application-*.yml`。 +- 白号是独立奖池,可与普通/主播配不同的区间;「批量填货」新增饰品位时白号区间默认取普通用户的默认值。 + +### 6.2 「幸运升级 → 失败奖励」(`tt_upgrade_fail_ornaments`) + +每个饰品位可配多件失败奖励(`ornament_name` / `ornament_price` / `ornament_number` / `ornament_level_id`),玩家升级失败即发放这些饰品;**没配就不发**(记录里 `gainOrnamentList=[]`、`gainOrnamentsPrice=0`)。 + +### 6.3 维护开关(后台「网站设置」,`sys_config`) + +| key | 值 | 效果 | +|---|---|---| +| `websiteMaintenance` | `1` | 全站维护:`upgrade` 与分解接口都拦截 | +| `luckyUpgradeMaintenance` | `1` | 只拦幸运升级:`upgrade` 返回「幸运升级维护中......」 | + +### 6.4 数据落库 + +| 表 | 说明 | +|---|---| +| `tt_upgrade_record` | 每条拉取记录(成败、消耗、获得总额、各项快照、`gain_ornament_list` JSON、`lucky_value`) | +| `tt_box_records` | 获得物入背包(`source=4` 幸运升级),`id` 即结果页分解入参 | +| `tt_user_blend_ercash` | 余额流水:消费 40、游戏内分解 41、背包分解 10 | +| `tt_upgrade_ornaments` | 饰品位配置 + 三个池的运行进度(各池幸运值/已投金额) | + +字段变更历史见 `docs/updata_sql.md`(含白号三列的 ALTER)。 diff --git a/docs/流水抽奖接口文档.md b/docs/流水抽奖接口文档.md new file mode 100644 index 0000000..fafa0d4 --- /dev/null +++ b/docs/流水抽奖接口文档.md @@ -0,0 +1,447 @@ +# 流水抽奖接口文档(Cocos 客户端对接) + +## 一、功能说明 + +玩家**无需额外付费**,仅凭当日已发生的游戏下注流水自动累计抽奖资格。 + +核心规则: + +| 规则 | 说明 | +|------|------| +| 流水来源 | 仅统计游戏消费流水,**排除** Roll 房、商城消费、福利类 | +| 兑换比例 | 每个宝箱可单独配置流水门槛(默认 3000 流水 = 1 次) | +| 过期机制 | 按自然日统计,次日零点自动清零,不累积 | +| 抽奖消耗 | **只消耗"今日流水额度",不扣余额、不扣积分** | +| 中奖发放 | 饰品直接进背包(`tt_box_records.source = 14`) | +| 余额变动 | 只有玩家在背包**分解**该饰品时才产生余额/积分变动(`source = 63` 流水抽奖奖励) | +| 同时开启 | 全站同一时间只有一个抽奖活动为开启状态 | + +参与统计的流水来源码(`TtAccountRecordSource.getTurnoverLotteryCodes()`): + +| source | 含义 | +|--------|------| +| 2 | 经典开箱消费 | +| 4 | 对战消费 | +| 40 | 幸运升级消费 | +| 38 | 疯狂转盘消费 | +| 60 | 奥林匹斯消费 | + +--- + +## 二、通用约定 + +- **Base URL**:`{host}/api/turnoverLottery` +- **请求方式**:全部为 `POST` +- **Content-Type**:`application/json` +- **鉴权**:需登录态,请求头携带 token。`userId` 由后端从 token 解析,**客户端不要传 userId** + +### 统一响应结构 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": {} +} +``` + +| code | 含义 | +|------|------| +| 200 | 成功 | +| 500 | 业务失败,提示语见 `msg` | +| 701 | 未实名认证,客户端统一弹实名认证窗口 | + +> 客户端判定成功请以 `code === 200` 为准;失败时直接向玩家展示 `msg`。 + +--- + +## 三、接口清单 + +| # | 接口 | 路径 | 用途 | +|---|------|------|------| +| 1 | 获取抽奖信息 | `POST /api/turnoverLottery/myStats` | 进入抽奖页拉取流水、次数、宝箱与奖品 | +| 2 | 执行抽奖 | `POST /api/turnoverLottery/draw` | 点击抽奖,返回中奖结果供播放动画 | +| 3 | 我的抽奖记录 | `POST /api/turnoverLottery/myRecords` | 展示玩家历史中奖记录 | +| 4 | 全服中奖记录 | `POST /api/turnoverLottery/globalRecords` | 展示全服玩家中奖公示(跑马灯/滚动列表) | + +--- + +## 四、接口详情 + +### 1. 获取抽奖信息 + +**`POST /api/turnoverLottery/myStats`** + +进入抽奖页时调用,一次性返回玩家流水统计 + 所有开启中的抽奖宝箱及其奖品。 + +#### 请求参数 + +无需参数,body 可不传(或传空对象 `{}`)。 + +#### 响应示例 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "totalTurnover": 12000.00, + "usedTurnover": 3000.00, + "availableTurnover": 9000.00, + "lotteryList": [ + { + "id": 1, + "lotteryName": "每日流水抽奖", + "status": 1, + "turnoverThreshold": 3000, + "sort": 0, + "lotteryChances": 3, + "totalValue": 8250.00, + "prizes": [ + { + "id": 6, + "ornamentName": "M9 刺刀 | 渐变之色", + "price": 8200.00, + "imageUrl": "/profile/upload/2026/08/08/xxx.png", + "ornamentLevel": 3, + "exteriorName": "略有磨损", + "levelImg": "/profile/upload/level/red.png", + "displayCount": 10, + "displayPercent": "50.00%", + "realCount": 5, + "realPercent": "45.45%", + "sort": 0 + } + ] + } + ] + } +} +``` + +#### 字段说明 + +**顶层字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `totalTurnover` | Decimal | 今日累计有效流水总额 | +| `usedTurnover` | Decimal | 今日已消耗的流水(抽奖已用掉的部分) | +| `availableTurnover` | Decimal | **当前可用流水** = `totalTurnover - usedTurnover`,不会为负 | +| `lotteryList` | Array | 开启中的抽奖宝箱列表,按 `sort` 升序 | + +**`lotteryList[]` 宝箱字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | Long | 宝箱 ID,抽奖时作为 `lotteryId` 传入 | +| `lotteryName` | String | 宝箱名称 | +| `status` | Int | 状态,始终为 1(接口只返回开启中的) | +| `turnoverThreshold` | Int | 该宝箱的流水门槛(多少流水抽 1 次) | +| `sort` | Int | 展示排序,越小越靠前 | +| `lotteryChances` | Int | **该宝箱可抽次数** = `availableTurnover ÷ turnoverThreshold` 向下取整 | +| `totalValue` | Decimal | 奖池总价值(按初始真实数量 × 单价累加) | +| `prizes` | Array | 奖品列表,按 `sort` 升序 | + +**`prizes[]` 奖品字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | Long | 奖品 ID | +| `ornamentName` | String | 饰品名称 | +| `price` | Decimal | 饰品价值 | +| `imageUrl` | String | 饰品图片 | +| `ornamentLevel` | Int | 品质等级 ID | +| `exteriorName` | String | 磨损度(崭新出厂/略有磨损/久经沙场/破损不堪/战痕累累/无涂装),可能为 `null` | +| `levelImg` | String | 品质底图,可用于饰品卡片背景 | +| `displayCount` | Int | 展示数量 | +| `displayPercent` | String | **展示爆率**,如 `"50.00%"`,用于前端展示给玩家 | +| `realCount` | Int | 当前剩余库存 | +| `realPercent` | String | 真实爆率(内部数据,**不建议展示给玩家**) | +| `sort` | Int | 展示排序 | + +> **图片路径处理**:`imageUrl` / `levelImg` 若以 `/` 开头(且非 `//`),是服务器相对路径,需拼接资源域名前缀;否则为完整 URL 直接使用。 + +> **展示建议**:转盘/宝箱格子的爆率请使用 `displayPercent`,`realPercent` 是后台真实概率,不要展示。 + +--- + +### 2. 执行抽奖 + +**`POST /api/turnoverLottery/draw`** + +玩家点击抽奖时调用。**建议点击立即请求接口,拿到结果后再播放动画**——饰品在接口返回成功时已经入背包,动画只是表现层,即使动画中途活动被关闭也不影响已发的奖。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `lotteryId` | Long | 是 | 宝箱 ID,取 `myStats` 返回的 `lotteryList[].id` | + +```json +{ + "lotteryId": 1 +} +``` + +#### 成功响应 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "prizeId": 6, + "ornamentName": "M9 刺刀 | 渐变之色", + "price": 8200.00, + "imageUrl": "/profile/upload/2026/08/08/xxx.png", + "ornamentLevel": 3, + "exteriorName": "略有磨损", + "lotteryName": "每日流水抽奖", + "costTurnover": 3000, + "msg": "抽奖成功!获得: M9 刺刀 | 渐变之色" + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `prizeId` | Long | 中奖奖品 ID,可与 `myStats` 的 `prizes[].id` 对应,用于定位动画停留格子 | +| `ornamentName` | String | 中奖饰品名称 | +| `price` | Decimal | 饰品价值 | +| `imageUrl` | String | 饰品图片 | +| `ornamentLevel` | Int | 品质等级 ID | +| `exteriorName` | String | 磨损度,可能为 `null` | +| `lotteryName` | String | 宝箱名称 | +| `costTurnover` | Int | 本次消耗的流水值 | +| `msg` | String | 中奖文案,可直接展示 | + +#### 失败响应(`code = 500`,提示语在外层 `msg`) + +| msg | 触发场景 | 客户端处理建议 | +|-----|----------|----------------| +| `抽奖活动不存在或未开启` | 活动被后台关闭或已删除 | 提示后刷新 `myStats` | +| `抽奖次数不足` | 可用流水不够一次门槛 | 提示玩家去游戏累计流水 | +| `奖品配置为空` | 该宝箱未配置奖品 | 提示"活动准备中" | +| `奖品库存不足` | 全部奖品初始数量为 0 | 提示"活动准备中" | +| `抽奖流水门槛配置错误,请联系管理员` | 门槛配置为 0 或负数 | 提示联系客服 | +| `抽奖处理中,请勿重复操作` | 同一用户并发点击被锁拦截 | **禁用按钮防连点**,稍后可重试 | +| `抽奖处理中,请稍后再试` | 同一宝箱有其他玩家正在抽 | 短暂延迟后自动重试一次 | +| `系统繁忙,请稍后重试` | 服务端异常(事务已回滚,未扣流水未发奖) | 提示重试 | + +> **重要**:抽奖接口已加**双层 Redis 分布式锁**(用户级 + 宝箱级),客户端仍需在按钮上做防连点,避免频繁触发"抽奖处理中"。 + +#### 抽奖成功后的处理 + +1. 用 `prizeId` 定位动画停留位置并播放中奖动画 +2. 动画结束后重新调用 `myStats` 刷新可用流水与剩余次数 +3. 饰品已在背包,如需"立即分解"走通用背包分解接口 `/api/userPackSack/decompose`,分解产生的余额/积分变动来源为 `63 流水抽奖奖励` + +--- + +### 3. 我的抽奖记录 + +**`POST /api/turnoverLottery/myRecords`** + +分页查询当前登录玩家的历史中奖记录,按抽奖时间倒序。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `lotteryId` | Long | 否 | 全部 | 按宝箱筛选,不传查全部 | +| `pageNum` | Int | 否 | 1 | 页码,从 1 开始 | +| `pageSize` | Int | 否 | 10 | 每页条数,**最大 100** | + +```json +{ + "pageNum": 1, + "pageSize": 20 +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "total": 35, + "pageNum": 1, + "pageSize": 20, + "rows": [ + { + "id": 128, + "userId": 688660, + "lotteryId": 1, + "prizeId": 6, + "ornamentId": null, + "ornamentName": "M9 刺刀 | 渐变之色", + "imageUrl": "/profile/upload/2026/08/08/xxx.png", + "ornamentsPrice": 8200.00, + "costTurnover": 3000, + "createTime": "2026-08-08 17:18:37" + } + ] + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `total` | Long | 总记录数,用于分页组件 | +| `pageNum` | Int | 当前页码 | +| `pageSize` | Int | 每页条数 | +| `rows` | Array | 记录列表 | +| `rows[].id` | Long | 记录 ID | +| `rows[].lotteryId` | Long | 宝箱 ID | +| `rows[].prizeId` | Long | 奖品 ID | +| `rows[].ornamentName` | String | 饰品名称 | +| `rows[].imageUrl` | String | 饰品图片 | +| `rows[].ornamentsPrice` | Decimal | 奖品价值 | +| `rows[].costTurnover` | Int | 该次消耗的流水值 | +| `rows[].createTime` | String | 抽奖时间 `yyyy-MM-dd HH:mm:ss` | + +失败时返回 `code = 500`,`msg` 为 `查询失败,请稍后重试`。 + +--- + +### 4. 全服中奖记录 + +**`POST /api/turnoverLottery/globalRecords`** + +分页查询**全服所有玩家**的中奖记录,按中奖时间倒序。用于抽奖页的中奖公示区(跑马灯、滚动榜单)。 + +> **已在服务端过滤**:机器人(`user_type='03'`)、白号(`is_blank_account=1`)、封禁用户(`status='1'`)、已删除用户(`del_flag!='0'`)的记录不会出现在结果里,客户端无需再做过滤。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `lotteryId` | Long | 否 | 全部 | 按宝箱筛选,不传查全部宝箱 | +| `minPrice` | Decimal | 否 | 不限 | 最低奖品价值门槛,只展示价值 ≥ 该值的记录(做"大奖公示"时用) | +| `pageNum` | Int | 否 | 1 | 页码,从 1 开始 | +| `pageSize` | Int | 否 | 20 | 每页条数,**最大 100** | + +```json +{ + "minPrice": 1000, + "pageNum": 1, + "pageSize": 20 +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "total": 1286, + "pageNum": 1, + "pageSize": 20, + "rows": [ + { + "id": 128, + "userId": 688660, + "nickName": "云深不知", + "avatar": "/profile/avatar/2026/08/08/aaa.png", + "lotteryId": 1, + "lotteryName": "每日流水抽奖", + "prizeId": 6, + "ornamentName": "M9 刺刀 | 渐变之色", + "imageUrl": "/profile/upload/2026/08/08/xxx.png", + "ornamentsPrice": 8200.00, + "ornamentLevel": 3, + "levelImg": "/profile/upload/level/red.png", + "exteriorName": "略有磨损", + "createTime": "2026-08-08 17:18:37" + } + ] + } +} +``` + +#### 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `total` | Long | 总记录数 | +| `pageNum` | Int | 当前页码 | +| `pageSize` | Int | 每页条数 | +| `rows` | Array | 记录列表,按中奖时间倒序 | +| `rows[].id` | Long | 记录 ID | +| `rows[].userId` | Int | 中奖用户 ID | +| `rows[].nickName` | String | **中奖用户昵称**,用于公示展示 | +| `rows[].avatar` | String | **中奖用户头像** | +| `rows[].lotteryId` | Long | 宝箱 ID | +| `rows[].lotteryName` | String | 宝箱名称 | +| `rows[].prizeId` | Long | 奖品 ID | +| `rows[].ornamentName` | String | 饰品名称 | +| `rows[].imageUrl` | String | 饰品图片 | +| `rows[].ornamentsPrice` | Decimal | 奖品价值 | +| `rows[].ornamentLevel` | Int | 品质等级 ID | +| `rows[].levelImg` | String | 品质底图,可作饰品卡片背景 | +| `rows[].exteriorName` | String | 磨损度,可能为 `null` | +| `rows[].createTime` | String | 中奖时间 `yyyy-MM-dd HH:mm:ss` | + +失败时返回 `code = 500`,`msg` 为 `查询失败,请稍后重试`。 + +> **与 `myRecords` 的区别**:`myRecords` 只返回当前登录玩家自己的记录(不含昵称/头像/品质图);`globalRecords` 返回全服记录并附带中奖者昵称、头像、品质底图,专供公示展示。 + +> **轮询建议**:公示区如需实时滚动,建议 15~30 秒轮询一次 `pageNum=1`,不要高频请求。 + +--- + +## 五、典型调用流程 + +``` +进入抽奖页 + │ + ├─→ POST /myStats + │ 拿到 availableTurnover、lotteryList + │ 渲染宝箱、奖品格子(爆率用 displayPercent) + │ 显示可抽次数 lotteryChances + │ + ├─→ 玩家点击抽奖(按钮置灰防连点) + │ POST /draw { lotteryId } + │ ├─ code=200 → 用 prizeId 定位并播放动画 + │ │ 动画结束后重新 POST /myStats 刷新 + │ └─ code=500 → 按钮恢复,展示 msg + │ + ├─→ 查看我的中奖记录 + │ POST /myRecords { pageNum, pageSize } + │ + └─→ 中奖公示区(跑马灯,15~30s 轮询) + POST /globalRecords { pageNum: 1, pageSize: 20 } +``` + +--- + +## 六、对接注意事项 + +1. **不要传 userId**:所有接口的用户身份从 token 解析,传了也会被忽略。 +2. **`myStats` 无需 body**:之前该接口曾要求传 `{}`,现已改为无参,body 传或不传都可以。 +3. **次数按宝箱各算**:`lotteryChances` 在每个宝箱内单独计算(因为门槛可能不同),不是全局共享的次数。多个宝箱的次数是"同一份可用流水"的不同换算结果,抽任一宝箱都会消耗同一份流水。 +4. **抽奖不动余额**:客户端不要在抽奖后刷新余额,抽奖不产生金币/积分变化;只有分解饰品才会变。 +5. **动画与发奖解耦**:接口返回成功即已发奖入背包,动画播放期间活动被关闭不影响结果。 +6. **爆率字段区分**:给玩家看 `displayPercent`,`realPercent` / `realCount` 属于内部数据。 +7. **奖池自动重置**:当宝箱所有奖品剩余库存耗尽时,下一次抽奖会自动按后台配置的初始真实数量重置奖池,客户端无需处理。 +8. **全服记录已过滤脏数据**:`globalRecords` 服务端已排除机器人、白号、封禁及已删除用户,客户端直接展示即可。 +9. **公示区别高频轮询**:`globalRecords` 建议 15~30 秒轮询一次,`pageSize` 上限 100。 + +--- + +## 七、相关代码位置 + +| 内容 | 文件 | +|------|------| +| 用户端接口 | `skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryUserController.java` | +| 业务实现 | `skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TurnoverLotteryServiceImpl.java` | +| 全服记录查询 | `skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.java` | +| 全服记录 VO | `skins-model/src/main/java/com/ruoyi/domain/vo/lottery/TurnoverLotteryWinVO.java` | +| 奖品实体 | `skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryPrize.java` | +| 记录实体 | `skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryRecord.java` | +| 建表 SQL | `docs/updata_sql.md`(搜索"流水抽奖") | diff --git a/docs/玫瑰女郎接口文档.md b/docs/玫瑰女郎接口文档.md new file mode 100644 index 0000000..450c926 --- /dev/null +++ b/docs/玫瑰女郎接口文档.md @@ -0,0 +1,707 @@ +# 玫瑰女郎 用户端 接口文档 + +--- + +## 一、功能说明 + +玫瑰女郎是一款**选箱下注 → 旋转 → 必得饰品**的电子游戏。核心产品逻辑: + +| 规则 | 说明 | +|------|------| +| 三种玩法模式 | `normal` 普通旋转 / `ante` 底注 / `buy_fs` 购买免费旋转,互斥 | +| 下注即得 | **开局即结算**:玩家扣费后,本轮结果与奖品在开局那一刻就已确定,饰品**直接写入背包**;前端播放动画仅是"揭晓" | +| 无损模型 | 中途刷新 / 退出 / 断线,饰品已在背包,不会丢奖 | +| 按身份差异化 | `common` 普通用户 / `anchor` 主播 / `robot` 机器人 / `white` 白号,各自独立的一套饰品桶分布与 RTP | +| 1000x 封顶 | 总倍率 ≥ 1000x 视为已封顶,`maxWinReached=true`;**单局累计到账达 1000 倍即截断终止**,保证动画总赢额 = 实际到账 | +| 自动分解 | 命中饰品默认写入背包;末帧由**前端**给玩家两个选择「放入背包 / 立即分解」。"自动分解"是前端自己的开关,开启后前端在末帧替玩家调一次分解接口即可,**后端不参与、也不回传该状态**;调不动(断网/超时)也只是饰品留在背包,玩家随时能自己分解,不会丢奖、不会重复入账 | +| 消除规则 | 棋盘 5 行 × 6 列共 30 格;**同一符号满 8 个即消除**,按 8-9 / 10-11 / 12-30 三档赔付 | +| 翻滚级联 | 消除 → 上方掉落 → 补位,可在同一旋内连锁多步,每步返回 `gridState`/`symbolLinks`(累积)/`dropData`/`fillData` | +| 炸弹(乘数球) | **只在免费旋转内出现**(normal/ante 的基础旋、含进 FS 的触发盘一律没有炸弹);每屏最多 1 颗,乘数值 2~1000(见 `multiplierDetail`),FS 内跨旋累加 | +| 免费旋转 | 基础盘 4+ SCATTER 触发,赠 15 旋(4/5/6 个 SCATTER 赔 3x/5x/100x);FS 内 3+ SCATTER 重触发 +5 旋,总上限 30 旋(到上限后不再落 3+ 个 SCATTER);**三种模式进 FS 后规则完全一致** | +| 1000x 截断 | 单局累计到账达 1000 倍即截断终止(保证动画总赢额 = 实际到账) | + +> 前端只需保证:**首次用 `boxId` 开局,之后不断调用 `继续` 逐步揭晓,直到 `state === "FINISH"`。** + +--- + +## 二、通用约定 + +- **Base URL**:`{host}/api/olympus` +- **请求方式**:全部为 `POST` +- **Content-Type**:`application/json` +- **鉴权**:需登录态,请求头携带 token(`Authorization: Bearer xxx`)。`userId` 由后端从 token 解析,**客户端不要传 userId**。 + +### 统一响应结构 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": {} +} +``` + +| code | 含义 | +|------|------| +| 200 | 成功 | +| 500 | 业务失败,提示语见 `msg`(直接向玩家展示) | +| 701 | 未实名认证,客户端统一弹实名认证窗口(**玫瑰女郎接口不会返回该码**,统一约定保留,便于前端全局处理) | +| 702 | 账户余额不足(`msg`:`账户余额不足`) | +| 703 | 玩家未充值,不能游玩任何游戏(`msg`:`玩家未充值不能游玩任何游戏`) | + +> 客户端判定成功以 `code === 200` 为准;失败时直接展示 `msg`。 + +### 玩法模式(boxType) + +| boxType | modeName | 成本倍数 costMultiplier | 备注 | +|---------|----------|------------------------|------| +| `normal` | 普通旋转 | 1x | 标准玩法,饰品桶 0x-150x;基础旋不出炸弹,4+ SCATTER 自然进 FS(进 FS 后与买 FS 一致) | +| `ante` | 底注 | 2x | SCATTER 概率提升(FS 触发 0.5%→1%),饰品桶 0x-200x;基础旋同样不出炸弹 | +| `buy_fs` | 购买免费旋转 | 50x | 保证直接进入免费旋转,饰品桶 10x-1000x | + +> 其中 `price`(单次下注实际扣费)= 箱子基础下注 `basicBet` × 该模式成本倍数。 + +--- + +## 三、符号与面值对照表 + +`gridState`、`symbolLinks`、`dropData`、`fillData` 中的符号一律是**短码**(单字母),前端按本节映射图标与文案。 + +### 3.1 普通符号(可消除) + +| 短码 | 符号 code | 中文名 | 8-9 个 | 10-11 个 | 12-30 个 | 出现权重 | +|------|-----------|--------|--------|----------|----------|----------| +| `A` | CROWN | 皇冠 | 10x | 25x | 50x | 5 | +| `B` | HOURGLASS | 沙漏 | 2.5x | 10x | 25x | 6 | +| `C` | RING | 环 | 2x | 5x | 15x | 7 | +| `D` | CHALICE | 杯赛 | 1.5x | 2x | 12x | 8 | +| `E` | RED_GEM | 红宝石 | 1x | 1.5x | 10x | 9 | +| `F` | PURPLE_GEM | 紫宝石 | 0.8x | 1.2x | 8x | 10 | +| `G` | YELLOW_GEM | 黄宝石 | 0.5x | 1x | 5x | 10 | +| `H` | GREEN_GEM | 绿宝石 | 0.4x | 0.9x | 4x | 10 | +| `I` | BLUE_GEM | 蓝宝石 | 0.25x | 0.75x | 2x | 12 | + +> 赔付以 **1 倍基础下注**为基准(接口里返回的金额字段均已再 ×bet);同一符号不足 8 个不成奖,最多计 30 个。 + +### 3.2 特殊符号 + +| 短码 | 符号 | 说明 | +|------|------|------| +| `S` | SCATTER | 不参与消除、不计入 `symbolLinks` 的消除统计;基础盘 4+ 触发免费旋转(4/5/6 个赔 3x/5x/100x,即 `scatterTrigger.scatterWin`),免费旋转内 3+ 重触发 +5 旋(总上限 30 旋,到上限后不再落 3+ 个 S) | +| `M` | BOMB | 炸弹(乘数球),**乘数值不在短码里**,见 `multiplierDetail`;不参与消除、跨 cascade 保留,免费旋转内跨旋累加(`accumulator`)。**基础旋(`spinNum=0`,含触发盘)不会出现 `M`**;进 FS 后三种模式一致,每屏最多 1 颗 | + +炸弹品质码(`multiplierDetail[].quality`): + +| quality | 颜色 | 等级 | 乘数值 | +|---------|------|------|--------| +| `GREEN` | 绿 | 1 | 2 / 3 / 4 / 5 / 6 | +| `BLUE` | 蓝 | 2 | 8 / 10 / 12 / 15 | +| `PURPLE` | 紫 | 3 | 20 / 25 / 50 | +| `RED` | 红 | 4 | 100 / 250 | +| `RAINBOW` | 彩虹 | 5 | 500 / 1000 | + +### 3.3 位置索引约定 + +`multiplierDetail[].pos`、`symbolLinks[].loc[]`、`dropData[].from`/`.to`、`fillData[].pos` 全部是**从 1 开始的格位号**,共 30 格: + +``` +pos = 行序号(0 起) × 6 + 列序号(0 起) + 1 // 1 ~ 30,行=5、列=6 +``` + +--- + +## 四、接口清单 + +| # | 接口 | 路径 | 用途 | +|---|------|------|------| +| 1 | 获取箱子列表 | `POST /api/olympus/boxes` | 进入游戏页拉取可选箱子 | +| 2 | 执行旋转(开始/继续) | `POST /api/olympus/dospin` | 开局扣费并返回第一步 / 继续揭晓下一步 | +| 3 | 查询游戏记录(我的/全服,分页) | `POST /api/olympus/records` | `scope=1` 我的战绩 / `scope=0` 全服记录,均分页;**只含已结束的局** | +| 4 | 查询进行中的游戏 | `POST /api/olympus/hasActive` | 断线重连时恢复动画 | +| 5 | 背包分解(关联) | `POST /api/userPackSack/decompose` | 末帧「立即分解」(含前端自动分解) | + +--- + +## 五、接口详情 + +### 1. 获取箱子列表 + +**`POST /api/olympus/boxes`** + +进入游戏页时调用,返回当前可选的全部箱子(后端只返回启用 `status=1` 的箱子)。 + +#### 请求参数 + +无,body 可不传(或传空对象 `{}`)。 + +#### 响应 data 结构 + +```json +{ + "list": [ + { + "boxId": 1001, + "boxName": "新手箱", + "basicBet": 10.00, + "price": 10.00, + "boxType": "normal", + "modeName": "普通旋转" + } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| boxId | Long | 箱子 ID,开局时作为 `boxId` 传入 dospin | +| boxName | String | 箱子名称 | +| basicBet | Decimal | 基础下注金额 | +| price | Decimal | 单次实际扣费 = basicBet × 模式成本倍数 | +| boxType | String | `normal`/`ante`/`buy_fs` | +| modeName | String | 模式中文名(普通旋转/底注/购买免费旋转) | + +--- + +### 2. 执行旋转(开始 / 继续) + +**`POST /api/olympus/dospin`** + +这是**核心接口**,有两种用法: + +- **开始新游戏**:body 携带非空 `boxId` → 后端校验余额/充值/身份 → 扣费 → 选回合 → 返回**第一步**动画数据。 +- **继续游戏**:body 不带 `boxId`(传 `{}` 或 `{"boxId": null}`)→ 返回**下一步**动画数据,直到 `state === "FINISH"`。 + +> ⚠️ 继续时必须传一个 JSON body(至少 `{}`)。若完全不传 body,后端可能因读取不到 boxId 而报错。 + +#### 请求参数 + +```json +{ + "boxId": 1001 +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| boxId | Long | 否 | **非空**=开始新游戏;**为空/null/不传**=继续当前游戏 | + +#### 响应 data 结构(SpinResponse) + +完整响应体(`@JsonInclude(NON_NULL)`,未出现的字段即为 `null`/未返回)。下面是**真实数据**的一步(box1 普通旋转 · 自然触发的免费旋转**第 6 旋 · 第 2 波**消除,屏上还留着一颗 2 倍炸弹;回合 `round 66551`,整局实付 125.45 倍底注、30 旋、42 步): + +```json +{ + "state": "PLAYING", + "spinNum": 6, + "tumbleNum": 2, + "globalStep": 10, + "globalTotalSteps": 42, + "gridState": [ + ["C","G","F","I","A","F"], + ["F","M","C","D","F","F"], + ["F","H","E","I","F","D"], + ["I","C","I","E","F","H"], + ["H","H","H","I","A","E"] + ], + "multiplierDetail": [{"pos": 8, "val": 2, "quality": "GREEN"}], + "symbolLinks": [ + {"symbol": "G", "count": 8, "loc": [3,5,10,13,18,25,26,30], "base_score": 0.50, "tumble": 1}, + {"symbol": "F", "count": 8, "loc": [3,6,7,11,12,13,17,23], "base_score": 0.80, "tumble": 2} + ], + "dropData": [ + {"from": 19, "to": 25, "symbol": "H"}, + {"from": 7, "to": 19, "symbol": "I"}, + {"from": 1, "to": 13, "symbol": "F"}, + {"from": 20, "to": 26, "symbol": "H"}, + {"from": 14, "to": 20, "symbol": "C"}, + {"from": 8, "to": 14, "symbol": "H"}, + {"from": 2, "to": 8, "symbol": "M"}, + {"from": 4, "to": 10, "symbol": "D"}, + {"from": 24, "to": 30, "symbol": "E"}, + {"from": 12, "to": 24, "symbol": "H"}, + {"from": 6, "to": 18, "symbol": "D"} + ], + "fillData": [ + {"pos": 1, "symbol": "C"}, {"pos": 2, "symbol": "G"}, {"pos": 3, "symbol": "F"}, + {"pos": 4, "symbol": "I"}, {"pos": 5, "symbol": "A"}, {"pos": 6, "symbol": "F"}, + {"pos": 7, "symbol": "F"}, {"pos": 12, "symbol": "F"} + ], + "winAmount": 1.30, + "spinWin": 14.55 +} +``` + +读法(这一步): +- 触发盘(`spinNum: 0`)**不会出现 `M`**:这一局基础旋摇出 4 个 S,白送 3.00 并进 FS(见下面买 FS 示例①) +- `symbolLinks` 有 **2 条** —— `tumble:1` 是第 1 波(G 符号 8 连)、`tumble:2` 是第 2 波(F 符号 8 连);**本步新增的只有 `tumble === tumbleNum(2)` 那条**,另一条是给面板累加用的 +- `winAmount = 1.30` 是**本旋(第 6 旋)累计到本步的赢额**(= 0.50 + 0.80,中间波只记基础分),**不是本步增量**;**本旋末帧才把累计倍数乘上去** —— 这一旋末帧 `winAmount` 会跳到 11.70(本旋基础分 1.30 × (累计 7 + 本旋炸弹 2)) +- `spinWin = 14.55` 是**本局累计到本步的赢额**(触发盘 3.00 + 前 5 旋 10.25 + 本旋 1.30) +- `dropData` 里 `{"from":2,"to":8,"symbol":"M"}`:**炸弹也随重力下落**(第 2 格 → 第 8 格),本旋末帧统一结算后才并入累计器(7 → 9) +- `globalStep/globalTotalSteps` 由服务端按整局排帧给出 + +> 已删除的冗余字段:`accumulatedSymbols`、`spinWinners`、`removedPositions`、`baseScore` —— 替代算法见字段表下方的说明。 + +--- + +**下面用同一局(买FS bet=1、15 旋、整局实付 66.00 倍底注、共 27 步,回合 `round 93485`)看三个典型阶段**(真实数据): + +##### ① 起始:本局第 1 步(触发盘,4 个 S 白送 3 倍) + +```json +{ + "state": "PLAYING", + "spinNum": 0, + "tumbleNum": 1, + "globalStep": 1, + "globalTotalSteps": 27, + "gridState": [ + ["B","G","S","C","G","I"], + ["I","E","H","E","G","S"], + ["D","B","G","I","S","C"], + ["G","H","E","B","F","S"], + ["B","C","E","A","C","G"] + ], + "multiplierDetail": [], + "symbolLinks": [], + "dropData": [], + "fillData": [], + "scatterTrigger": {"scatterCount": 4, "freeSpins": 15, "scatterWin": 3.00}, + "winAmount": 3.00, + "spinWin": 3.00 +} +``` +- `spinNum: 0` = 触发盘(买FS 必出 4 个 S,这一盘没有任何符号连线和炸弹) +- `winAmount = spinWin = 3.00`:触发盘这一旋的总额,也是本局累计的起点 +- `state` 仍是 `PLAYING` —— **本局还有 26 步**;`scatterTrigger` 出现在**本旋的末帧**,不是本局的末帧 + +##### ② 中间:第 4 旋 · 第 2 波(这一旋连消多波,屏上还留着一颗 2 倍炸弹) + +```json +{ + "state": "PLAYING", + "spinNum": 4, + "tumbleNum": 2, + "globalStep": 10, + "globalTotalSteps": 27, + "gridState": [ + ["G","A","G","G","G","G"], + ["I","E","G","I","C","G"], + ["D","G","D","G","I","G"], + ["I","E","I","C","M","E"], + ["D","I","E","C","B","E"] + ], + "multiplierDetail": [{"pos": 23, "val": 2, "quality": "GREEN"}], + "symbolLinks": [ + {"symbol": "G", "count": 14, "loc": [2,6,8,9,12,13,14,15,16,17,18,22,27,28], "base_score": 5.00, "tumble": 1}, + {"symbol": "G", "count": 10, "loc": [1,3,4,5,6,9,12,14,16,18], "base_score": 1.00, "tumble": 2} + ], + "dropData": [ + {"from": 7, "to": 13, "symbol": "D"}, + {"from": 1, "to": 7, "symbol": "I"}, + {"from": 21, "to": 27, "symbol": "E"}, + {"from": 3, "to": 21, "symbol": "I"}, + {"from": 10, "to": 28, "symbol": "C"}, + {"from": 4, "to": 22, "symbol": "C"}, + {"from": 11, "to": 17, "symbol": "I"}, + {"from": 5, "to": 11, "symbol": "C"} + ], + "fillData": [ + {"pos": 1, "symbol": "G"}, {"pos": 2, "symbol": "A"}, {"pos": 3, "symbol": "G"}, + {"pos": 4, "symbol": "G"}, {"pos": 5, "symbol": "G"}, {"pos": 6, "symbol": "G"}, + {"pos": 8, "symbol": "E"}, {"pos": 9, "symbol": "G"}, {"pos": 10, "symbol": "I"}, + {"pos": 12, "symbol": "G"}, {"pos": 14, "symbol": "G"}, {"pos": 15, "symbol": "D"}, + {"pos": 16, "symbol": "G"}, {"pos": 18, "symbol": "G"} + ], + "winAmount": 6.00, + "spinWin": 19.25 +} +``` +- 本步新增的连线只有 `tumble === 2` 那条(G 10 连);`tumble: 1` 那条是这一旋第 1 波的(G 14 连,5.00),继续列在这里供"获胜者面板"累加 +- `dropData` 里那条 `{"from":...,"to":...,"symbol":"M"}` 会体现炸弹随重力下落,末帧才结算倍数(本例炸弹在 `gridState` 第 23 格) +- `winAmount = 6.00` 是**第 4 旋累计**(5.00 + 1.00),`spinWin = 19.25` 是本局累计;这一旋末帧会跳到 `12.00`(基础分 6.00 × (累计 0 + 本旋炸弹 2)) + +##### ③ 最后一旋:本局第 27 步(`state = FINISH`,可结算展示) + +```json +{ + "state": "FINISH", + "spinNum": 15, + "tumbleNum": 1, + "globalStep": 27, + "globalTotalSteps": 27, + "gridState": [ + ["M","C","D","E","D","A"], + ["F","C","D","C","C","G"], + ["E","C","I","E","F","B"], + ["F","F","G","H","C","I"], + ["H","F","A","C","I","A"] + ], + "multiplierDetail": [{"pos": 1, "val": 2, "quality": "GREEN"}], + "symbolLinks": [], + "dropData": [], + "fillData": [], + "winAmount": 0.00, + "spinWin": 66.00, + "totalWin": 66.00, + "totalMul": 66.00, + "prizeInfo": { + "id": 4, + "name": "M4A4 | 皇家圣骑士", + "imageUrl": "/profile/upload/2026/07/02/TTRBNCB8IFJveWFsIFBhbGFkaW4gKEZhY3RvcnkgTmV3KQ==_20260702034203A004.png", + "grade": 4, + "exteriorName": "破损不堪", + "isFallback": false + }, + "boxRecordId": 556677 +} +``` +- 第 15 旋**没有消成**:`symbolLinks` 空、`winAmount = 0`(屏上那颗 2 倍炸弹随本旋作废,累计器不变) +- `spinWin = 66.00` = **整局实付**,与 `totalWin`/`totalMul` 一致(各旋末帧 `winAmount` 相加也等于它) +- `prizeInfo` = 本局命中的饰品(按整局赢额落区间查表,见 3.4);`boxRecordId` = 服务端开局时就写入背包的记录 ID +- 1000 倍封顶时才会多两个字段:`maxWinReached: true` + `spinsRemaining: 0`;本局没触顶,所以都不返回(`spinsRemaining` 平时**每帧都在**,封顶时被强制成 0) +- 本局第 12 旋单旋赢了 22.75 倍(≥ 买 FS 的"小"阈值 20),那一旋的末帧会带 `bigWinLevel: "small"`;最后这一旋没消成(`winAmount = 0`),所以本步没有该字段 —— 该字段按**单旋**赢额判档、只在**免费旋转**里给,达标的那一旋末帧才出现 + +##### 字段说明 + +| 字段 | 类型 | 出现 | 说明 | +|------|------|------|------| +| state | String | 每步 | `PLAYING`(本局还有后续步骤)/ `FINISH`(**本局最后一步**,可结算展示)。注意:**本旋的末帧不一定是 FINISH**(只有整局最后一步才是),本旋是否结束看下一步 `spinNum` 是否变化 | +| spinNum | Integer | 每步 | 免费旋转序号,**0 = 基础旋转** | +| tumbleNum | Integer | 每步 | 本旋内的翻滚/消除序号 | +| globalStep | Integer | 每步 | 全局第几步(从 1 开始) | +| globalTotalSteps | Integer | 每步 | 本局总步数 | +| spinsRemaining | Integer | 每步 | **剩余免费旋转次数**(已发放 − 已播完旋数):已发放 = 触发帧 `scatterTrigger.freeSpins`(15) + 已播步里的 `retriggerInfo.extraSpins`(5),封顶 30。**前端直接显示这个值倒计即可,不用自己加减**(口径见下方「剩余免费旋转」)。例:第 2 旋消完出 3 个 S → 15−2+5 = **18**。基础旋/无 FS 的局恒 0;1000x 封顶时强制 0 | +| gridState | Array\[5\]\[6\] | 每步 | 棋盘短码矩阵(5 行 × 6 列,含 `S`/`M`);**每一步都是完整的 30 格,不会有空格/null**,是渲染的权威数据(约定见本节末尾) | +| multiplierDetail | Array | 每步 | 炸弹明细 `[{"pos":1起,"val":乘数值,"quality":"GREEN/…"}]`,**未 × bet**(quality 见 3.2) | +| symbolLinks | Array | 每步 | **本旋累积**的成奖连线 `[{"symbol","count","loc":[1起格位],"base_score","tumble"}]`;`base_score` 已 × bet,`tumble` = 这条连线属于本旋内第几波消除(= 本步 `tumbleNum`)。**本波新增的连线 = `tumble` 等于本步 `tumbleNum` 的那些**;获胜者面板按 `symbol` 汇总即可(原来单独的 `spinWinners` 字段已删) | +| dropData | Array | 每步 | 掉落数据 `[{"from":1起,"to":1起,"symbol"}]`:只列**位置变了**的符号,`symbol` 是它落到目的地后的符号;新旋第一波为空数组 | +| fillData | Array | 每步 | 补位数据 `[{"pos":1起,"symbol"}]`,**只含普通符号**(不含炸弹/SCATTER);正好盖住下落之后空出来的格子,新旋第一波为空数组 | +| scatterTrigger | Object | 每步 | 基础盘触发免费旋转时返回 `{"scatterCount":4,"freeSpins":15,"scatterWin":3.00}`:`freeSpins` 恒 15 = **赠送量**(不是剩余,剩余看 `spinsRemaining`);`scatterWin` = 本盘 SCATTER 的**现金赔付**(已 × bet,4/5/6 个 S 分别赔 3x/5x/100x,已计入本旋 `winAmount`/`spinWin`),无则 null | +| winAmount | Decimal | 每步 | **本旋累计到本步的赢额**(已 × bet):该旋**末帧(`isFinal=1`)的值就是这一旋的总额**(含炸弹倍数与 SCATTER),中间步是这一旋的累计进度;**不是本步增量**(本步增量 = 与上一步相减) | +| spinWin | Decimal | 每步 | **本局累计到本步的赢额**(已 × bet):前端"总赢额跳字"直接用这个;**最后一步 = 整局实付 = `totalWin`** | +| accumulator | Decimal | 每步 | **本步后的炸弹累计倍数**(未 × bet):免费旋转内跨旋传递的累计器(基础旋恒为 0);**断线重连时用 `lastStep.accumulator` 接回累计值**,其余时候前端可按 `multiplierDetail` 自行累计 | +| retriggerInfo | Object | 每步 | 免费旋转内重触发 `{"scatterCount":3,"extraSpins":5,"scatterWin":0.00}`:`extraSpins` 恒 5 = **追加旋数**(30 旋封顶后仍会报 5 但实际不加,所以别拿它自己加减,直接用 `spinsRemaining`);`scatterWin` = 本旋 SCATTER 的**现金赔付**(已 × bet,4/5/6 个 S 分别赔 3x/5x/100x,**3 个 S 赔 0**,已计入本旋 `winAmount`/`spinWin`),无则 null | +| bigWinLevel | String | 每步 | 爆奖档位(小/中/大):`small` / `middle` / `big`,无则 null。**只有免费旋转的旋末帧才可能有值**(normal/ante 的自然 FS、buy_fs 的每一旋),基础旋(`spinNum=0`)恒为 null;判档用该旋末帧的 `winAmount`,阈值见本节末尾「爆奖档位 `bigWinLevel` 阈值」 | +| **totalWin** | Decimal | 仅 FINISH | 本局总赢额 | +| **totalMul** | Decimal | 仅 FINISH | 本局总倍率 | +| **maxWinReached** | Boolean | 仅 FINISH | 是否已达 1000x 封顶 | +| **prizeInfo** | Object | 仅 FINISH | 命中饰品详情(见下) | +| **boxRecordId** | Long | 仅 FINISH | 背包记录 ID,用于末帧「放入背包」或「立即分解」(分解时作为 `packSackIds` 传入) | + +`prizeInfo` 结构: + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | Long | 饰品(奖品)ID | +| name | String | 饰品名称 | +| imageUrl | String | 饰品图片(后台「奖品管理」的"饰品图片") | +| levelImageUrl | String | **饰品等级/品质图片**(后台「奖品管理」的"品质图片",取 `tt_ornaments_level.level_img`,按饰品 `grade` 关联);前端结果弹窗的品质图标用它(可空) | +| grade | Integer | 等级(= `tt_ornaments_level.id`) | +| exteriorName | String | 外观名(可空) | +| isFallback | Boolean | 是否命中兜底饰品(无精确区间时的 fallback) | + +> 说明:`accumulator`(本步后炸弹累计倍数,未 × bet)为**跨旋传递的累计器**,基础旋恒为 `0`。 +> 前端按 `multiplierDetail` 自行累计即可;**唯一必须依赖它的是断线重连**——`hasActive` 只回最后一步, +> 前几旋攒下的累计倍数不在 `lastStep.multiplierDetail` 里,必须用 `lastStep.accumulator` 把累计值接回来, +> 否则重连后显示的累计倍数会从这次重连的那一旋重新起算(少掉之前累计的倍数)。 +> +> **剩余免费旋转 `spinsRemaining`(每帧都返回,前端直接显示,不用自己算)**: +> 进 FS 后从 **15** 起倒计,**每播完一旋 −1**(同一旋的中间波帧不变,到该旋**末帧**才减); +> **那一旋出 3+ 个 S 时在同一帧 +5**(`retriggerInfo.extraSpins`;出 3/4/5/6 个 S 都是 +5,多出来的只是现金 `scatterWin` 3x/5x/100x,3 个 S 为 0)。 +> 所以"出 3 个 S"那一帧是 **−1 和 +5 一起结算 = 净 +4**: +> +> | 时刻 | 已发放 | 已播完 | `spinsRemaining` | +> |---|---|---|---| +> | 触发帧播完(进 FS) | 15 | 0 | **15** | +> | 第 1 旋播完 | 15 | 1 | **14** | +> | 第 2 旋·第 1/2 波(本旋还没播完) | 15 | 1 | 14(不变) | +> | **第 2 旋末帧,这旋出 3 个 S** | 15+5 = 20 | 2 | **18**(= 14−1+5) | +> | 第 3 旋播完 | 20 | 3 | **17** | +> | 第 3 旋又出 4 个 S | 20+5 = 25 | 3 | **22** | +> +> 旋数上限 **30**(15 起、每次重触发 +5、最多加 3 次)—— 生成端已加闸门:**到 30 旋后不再落 3+ 个 S**, +> 所以一局最多 3 次重触发(旧池子里还残留 650 个"报了 +5 但没有旋可加"的旧帧,重跑池子后会消失; +> 后端的 `spinsRemaining` 按旋数夹 30,碰上旧帧也不会算错)。 +> 整局 1000x 封顶时强制 `spinsRemaining = 0`;无 FS 的局(基础旋空转)恒 0。 +> +> **金额与面板字段怎么用**(都在 `steps[]` 的每一步上,金额已 × bet): +> +> | 想看什么 | 用哪个 | +> |---|---| +> | 这一步加了多少(本步增量) | `winAmount` 与上一步相减(第一步直接取 `winAmount`) | +> | **这一旋一共赢了多少** | 该旋**末帧**的 `winAmount`(不用自己求和) | +> | **本局一共赢了/正在累计多少** | `spinWin`(滚动跳字);最后一步的值 = `totalWin` | +> | 本步新消出来的连线(画连线动画) | `symbolLinks` 里 `tumble` == 本步 `tumbleNum` 的那些 | +> | 获胜者面板(本旋逐波的 符号×数量×赢额) | `symbolLinks` 按 `symbol` 汇总(`count` 累加、`base_score` 累加)即可,**已无单独的 `spinWinners` 字段** | +> | 爆奖动画(small/middle/big 小/中/大) | `bigWinLevel`(后端按**单旋赢额**判好档,直接透传,无则 null;只在免费旋转里给) | +> | 炸弹与倍数累计显示 | `multiplierDetail`(每屏炸弹 `pos/val/quality`),倍数按旋自行累加;断线重连后用 `lastStep.accumulator` 接回累计值 | +> +> 一致性:各旋末帧 `winAmount` 相加 = 整局实付 = 最后一步 `spinWin` = `totalWin`。 +> 已删除的字段(都是可从别的字段推导的冗余项):`baseScore`(= 本步 `symbolLinks[].base_score` 之和)、 +> `removedPositions`(= 上一步 `symbolLinks[].loc` 的并集)、`accumulatedSymbols`(= 本旋 `symbolLinks` 按 symbol 汇总)、 +> `spinWinners`(= 本旋 `symbolLinks` 按 symbol 汇总)。 + +> **盘面与动画数据的约定**(每一步都返回完整盘面,前端不用自己推算): +> +> 1. `gridState` 永远是**权威盘面**:5 行 × 6 列 = 30 格,格子里只有 `A`~`I` / `S` / `M`,**不会出现空格或 null**(全库 355,902 步已逐一校验:空白格 0、非法符号 0、复刻结果与实际盘面 100% 一致)。 +> 2. 同一旋内,第 N+1 波可以**精确**复刻:`上一波盘面` −(上一波 `symbolLinks` 里 `tumble == 上一波 tumbleNum` 的格子)→ 按 `dropData` 下落 → 按 `fillData` 补位 = `gridState`(`dropData` 与重力、`fillData` 覆盖的格子也逐条校验过)。 +> 3. `dropData[].symbol` / `fillData[].symbol` 一律是**这一波结束后**该位置的符号(与 `gridState` 同位置一致),所以按第 2 条落地后不会出现错符号。 +> 4. `dropData` 只列**位置发生变化**的符号(原地不动的格子不在里面,前端保留原样即可);`fillData` 正好覆盖"下落之后空出来的格子",不会漏格。 +> 5. **新旋的第一波**(`tumbleNum = 1`,`spinNum` 递增或进入本局第 1 步)`dropData` / `fillData` 都是空数组 —— 这一波是**整盘重发**,前端直接整盘替换/重新发牌,不要按下落补位处理。 +> 6. 免费旋转的"保底消除"让 8 个**刚落下/刚补位**的格子凑成一组消除:这些格子的新符号就在 `dropData` / `fillData` 里(见第 3 条),所以整条动画链能完整复刻。前端每一步结束都以 `gridState` 为准渲染,永远不会出现空格。 + +> **免费旋转的累计倍数**(生成端规则,前端显示不受影响): +> 免费旋转内,**每一旋都可能带炸弹**(每屏最多 1 颗;低倍档局只出 2~8 的小球、高倍档局可出到 1000), +> 有消除的旋会把本旋炸弹并入累计器,**本旋赢额 = 基础分 × max(1, 累计值)**,之后累计值 += 本旋炸弹和 +> —— 也就是"滚雪球":上一旋攒的倍数会作用到后面每一次消除。 +> 因为累计倍数会放大后续每一次消除,**低倍档局(10x~25x,占买FS 抽中率 76%)大约只能演 5 次左右的 +> "消除+爆"**(单个球越小、能买的次数越多);要 10 次以上的连爆,那一局本身就得是 40 倍以上的档位。 +> 表现上:15 旋里约 5 旋有消除、约 4 次炸弹并入、局末累计倍数常见 15~25(大奖局 100+)。 +> +> **normal/ante 的自然免费旋转**:基础旋(`spinNum=0`)不出炸弹,摇出 4~6 个 SCATTER 才进 FS, +> 进 FS 后走的是**和买 FS 完全相同**的规则(同样的炸弹密度、同样的累计器、同样的每旋保底消除), +> 这样一局实测约 80~120 倍底注、5 旋左右有消除。因为 15 旋带炸弹的整局必然是这个量级, +> **这两个模式的 100x / 150x / 200x 饰品桶就是靠"自然触发 FS"的回合来装的** —— +> 前端不需要区分,`spinNum` 从 0 变成 1 就是进了 FS。 + +**爆奖档位 `bigWinLevel` 阈值**(小/中/大三档,按**单旋**赢额 ÷ 基础下注判档,达标即在该旋末帧返回、同档一局可多次;**只判免费旋转**,基础旋恒为 null): + +| 情况 | 模式 | `small`(小) | `middle`(中) | `big`(大) | +|------|------|------|------|------| +| ① 自然免费旋转 | `normal` / `ante` | ≥5 | ≥25 | ≥100 | +| ② 购买免费旋转 | `buy_fs` | ≥20 | ≥60 | ≥200 | + +- 判档取值:该旋末帧的 `winAmount`(单位 = 基础下注倍数,与前端看到的一致)。 +- 前端逻辑:**只在 `bigWinLevel` 非空时播对应动画**,播完继续下一步即可;一个 FS 局里三档可以按旋依次出现。 +- 为什么两套阈值:normal/ante 的自然 FS 整局约 80~130 倍(预算/旋数完全一致,共用一套);`buy_fs` 花 50 倍买来的赢额分散在 15+ 旋里,单旋阈值整体下调。 +- 实测出现频率(当前池子 30 箱逐局统计):① 自然 FS(normal 519 局 / ante 818 局)—— "小" 91.7% / 93.5% 的局会出现(每局 2.3~2.5 次)、"中" 49.3% / 48.8%(0.6 次)、"大" 31.4% / 39.0%(0.3~0.4 次);② 买 FS(12,205 局)—— "小" 19.5% 的局出现(0.47 次)、"中" 6.3%(0.13 次)、"大" **1.5%**(0.02 次)。 + +##### 错误码 / 业务提示 + +| code | msg | 触发场景 | 前端处理 | +|------|-----|---------|---------| +| 702 | 账户余额不足 | 余额 < 单次扣费 | 提示并引导充值 | +| 703 | 玩家未充值不能游玩任何游戏 | 从未充值 | 提示先充值 | +| 500 | 网站维护中...... | 全站维护开启 | 提示维护 | +| 500 | 玫瑰女郎维护中...... | 玫瑰女郎维护开启 | 提示维护 | +| 500 | 请求频繁,请稍后再试 | 短时重复请求(Redis 锁) | 稍后重试 | +| 500 | 箱子不可用 | boxId 不存在或下线 | 刷新箱子列表 | +| 500 | 该箱子暂无可用回合 | 该身份该箱无可用回合 | 提示稍后再试 | +| 500 | 游戏已结束 | 局已播完仍继续调用 | 忽略/返回首页 | +| 500 | boxId 不能为空 | 无进行中对局,且 body 未带 `boxId` | 回到选箱页重新开局 | + +> **本接口不会返回 701**(实名认证由支付等其它模块返回,游戏链路不做实名校验)。 + +> **维护只拦"新开一局"**:当 `websiteMaintenance`/`olympusMaintenance` 开启时,只要**携带非空 `boxId`(开新局)**就会返回维护提示;**不带 `boxId`(继续当前对局)不受维护影响**,可正常播完。进行中的对局不会被强制结算。 + +--- + +### 3. 查询游戏记录(我的 / 全服,均分页) + +**`POST /api/olympus/records`** + +一个接口通过 `scope` 区分「我的记录」与「全服记录」,**两者均支持分页**。 + +- `scope=1`(默认):**我的记录**,只返回当前登录玩家已结束(播完)的记录。 +- `scope=0`:**全服记录**,返回全站玩家已结束(播完)的记录(倒序)。 +- **白号记录在两种 scope 下都会出现(可上榜)**;全服口径仅排除机器人(user_type=03)、封禁(status=1)与已删除(del_flag!=0)用户。 + +#### 请求参数 + +```json +{ + "scope": 0, + "pageNum": 1, + "pageSize": 10 +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| scope | Integer | 否 | `1`=我的记录(默认)/ `0`=全服记录 | +| pageNum | Integer | 否 | 页码,默认 `1` | +| pageSize | Integer | 否 | 每页条数,默认 `10` | + +> 请求体为 **`RecordPageRequest` 实体类**(POST + JSON),三个字段均可选;body 也可不传(或传空对象 `{}`),此时等价于 `scope=1, pageNum=1, pageSize=10`(我的记录)。 + +#### 响应 data 结构 + +```json +{ + "rows": [ + { + "nickName": "玩家昵称", + "avatar": "https://.../avatar.png", + "cost": 10.00, + "settledWin": 88.00, + "prizeImageUrl": "https://.../rose.png", + "prizeName": "黄金玫瑰 (久经沙场)", + "createTime": "2024-01-01 12:00:00" + } + ], + "total": 57 +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| nickName | String | 玩家昵称(我的记录=自己,全服记录=中奖玩家) | +| avatar | String | 玩家头像 | +| cost | Decimal | 用户此次消费价格(本局扣费) | +| settledWin | Decimal | 此次获得饰品的价格(本局结算金额) | +| prizeImageUrl | String | 饰品图片 | +| prizeName | String | 饰品全名:`名称 (磨损品质)`,无品质时仅名称(如 `黄金玫瑰 (久经沙场)`) | +| createTime | String | 时间(yyyy-MM-dd HH:mm:ss,东八区) | + +> 列表项**只返回上述 7 个字段**(我的记录/全服记录同一结构):`userId`/`boxId`/`roundId`/`settled`/`modeType`/`rewardType`/`prizeId`/`prizeLevelImg`/`settleSource`/`boxName`/`basicBet` 等字段已不再返回,客户端无需判空。 +> +> **只返回已结束的局**:开局即结算(`settled=1`)但客户端还在播动画的那一局不出现在列表里,播完末帧后(服务端把该记录 `settle_source` 由 `start` 改为 `finish`)才进入记录;中途退出/断线未播完的局,按单局缓存 TTL **30 分钟**兜底,超时后照常展示,战绩不会丢。 +> +> `total` 为**该 scope 下的真实总数**(可用于分页计算)。 +> +> 早期版本曾有 `autoDismantle` 字段,现已彻底移除(自动分解由前端开关 + 前端调分解接口,后端不记录、不返回);库里 `game_olympus_record.auto_dismantle` 列作为历史列保留,无代码读写。 + +--- + +### 4. 查询进行中的游戏(断线重连) + +**`POST /api/olympus/hasActive`** + +用户刷新页面/重连时调用,判断是否有未播完的局,若有则返回当前进度与最后一步,供前端恢复动画。**维护期间不强制结算**,进行中的对局可正常重连并播完。 + +#### 请求参数 + +无,body 可不传(或传空对象 `{}`)。 + +#### 响应 data 结构 + +**无进行中游戏:** +```json +{ "hasActive": false } +``` + +**有进行中游戏:** +```json +{ + "hasActive": true, + "boxId": 1001, + "boxType": "normal", + "bet": 10.00, + "cost": 10.00, + "stepIndex": 3, + "totalSteps": 8, + "freeSpinCount": 0, + "totalWin": 25.00, + "roundId": 1234567, + "lastStep": { "...": "与 dospin 的 PLAYING 响应同构,为最后已播的一步" } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| hasActive | Boolean | 是否有进行中局 | +| boxId | Long | 箱子 ID | +| boxType | String | 模式 | +| bet | Decimal | 基础下注 | +| cost | Decimal | 本局扣费(已扣) | +| stepIndex | Integer | 当前已播步数 | +| totalSteps | Integer | 总步数 | +| freeSpinCount | Integer | 本局免费旋转**总**次数(15 + 重触发×5,封顶 30;1000x 截断局是实际执行数)。**不是剩余** | +| spinsRemaining | Integer | **剩余免费旋转次数**(倒计值,= 最后已播一步的 `lastStep.spinsRemaining`,口径见「2. 执行旋转」的字段说明),无 FS 的局为 0 | +| totalWin | Decimal | 已播步骤累计赢额 | +| roundId | Long | 回合 ID | +| lastStep | Object | 最后已播一步的完整动画数据(PLAYING 同构);**仅当 `stepIndex ≥ 1` 时返回**,`stepIndex=0` 时该字段不出现。`lastStep.accumulator` = 这步之后的炸弹累计倍数,重连时用它接回累计 | + +> 前端恢复策略:先播 `lastStep` 的最后一帧,然后**把本地炸弹累计倍数重置为 `lastStep.accumulator`**(字段不出现时按 0),再继续调用 `dospin`(不传 boxId)走完剩余步骤,直至 FINISH。少了这一步重置,重连后累计倍数会从重连那一旋重新起算。 + +--- + +### 5. 背包分解(关联接口) + +**`POST /api/userPackSack/decompose`** + +玫瑰女郎的命中饰品默认以 `ornament` 形式写入背包(`tt_box_records.source = OLYMPUS_BOX(13)`)。末帧玩家点「立即分解」——或前端"自动分解"开关开启时代替玩家自动调用——都调这个通用背包接口把饰品兑换成金币/积分;接口没调到也只是饰品留在背包,无资金影响。 + +#### 请求参数 + +```json +{ + "packSackIds": [556677], + "isAll": false, + "decomposeType": "game" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| packSackIds | Array | 否 | 待分解的背包记录 ID 列表(单个即 `[boxRecordId]`) | +| isAll | Boolean | 是 | 是否全部分解(`false`=按 ids) | +| decomposeType | String | 否 | `backpack`=背包分解(默认)/ `game`=游戏结果页立即分解。**后端当前不读取该字段**(分解结算一律按饰品来源判定),传与不传结果相同,保留仅为兼容前端调用 | + +#### 响应 + +```json +{ + "code": 200, + "msg": "成功分解1件饰品", + "data": null +} +``` + +> **分解流水口径**:分解结算按**饰品来源**自动分组记流水(`tt_box_records.source` → 对应游戏奖励流水),与前端传来的 `decomposeType` 无关(该字段后端不读取,传与不传结果相同)。玫瑰女郎饰品来源 `OLYMPUS_BOX(13)` → **`OLYMPUS_REWARD(61) 玫瑰女郎奖励`**:无论玩家「放入背包后再分解」还是末帧「立即分解」(含前端自动分解),入账流水一律是玫瑰女郎奖励。 + +--- + +## 六、关键流程(前端友好版) + +### 6.1 进入游戏页 + +1. `POST /api/olympus/boxes` → 拿到箱子列表,展示箱子和价格。 +2. 玩家选箱子 → 记录 `boxId`。 + +### 6.2 开始新游戏 + +1. `POST /api/olympus/dospin`,body `{"boxId": X}`。 +2. 返回第一步(`PLAYING` 或直接 `FINISH`,若该局只有一步)。 + +### 6.3 逐步揭晓动画 + +- 对每步 `PLAYING` 响应,播放对应动画(`gridState`/`symbolLinks` 里 `tumble === tumbleNum` 的新增连线/`dropData`/`fillData`/`multiplierDetail`)。 + `winAmount` 是该旋累计(末帧即该旋总额),`spinWin` 是本局累计,可直接用于跳字。 +- 再调用 `POST /api/olympus/dospin`,body `{}`(不带 boxId),得到下一步。 +- 重复直止 `state === "FINISH"`。 + +### 6.4 末帧结算展示 + +1. 读到 `totalWin/totalMul/prizeInfo/boxRecordId`。 +2. **饰品已写入背包**,无需等待。 +3. 展示中奖弹窗,给玩家两个选择: + - **放入背包**:什么都不用调,饰品已在背包(`boxRecordId`)。 + - **立即分解**:调 `POST /api/userPackSack/decompose`,`packSackIds:[boxRecordId], isAll:false`,入账流水为玫瑰女郎奖励(61)。 +4. 前端"自动分解"开关为开时:末帧直接按第 3 步的「立即分解」调用,等价于替玩家点了一下,**后端无需任何额外字段或开关**。 +5. 这次调用失败(断网、超时、玩家直接关页面)都不影响对局结果:饰品本来就在背包里,等于玩家选了「放入背包」,之后在背包页随时可再分解。重试也不会重复加钱(同一饰品只会被结算一次)。 +6. **结算站内信在"这一局播完"时才发**(多步局=末帧 `FINISH`,单步局=开局):开局就发会在动画播完前把结果剧透给玩家。因此中途退出/断线再没回来的局不发站内信,饰品仍在背包、战绩仍按 30 分钟兜底展示。 + +### 6.5 重连 / 刷新 + +- 调 `POST /api/olympus/hasActive`。 +- `hasActive=false` → 正常回首页/列表。 +- `hasActive=true` → 先播 `lastStep`,再继续 `dospin`(不带 boxId)走完剩余步骤。 + +### 6.6 维护行为 + +- **开启维护(`websiteMaintenance`/`olympusMaintenance`)= 只拦"新开一局"**。 +- 玩家携带非空 `boxId` 调 `dospin`(开新局)→ 返回维护提示(`网站维护中......` / `玫瑰女郎维护中......`)。 +- **进行中的对局不受影响**:不带 `boxId` 调 `dospin`(继续)可正常播完;`hasActive` 也能正常恢复动画,不会被强制结算。 + +--- + +## 七、数据口径补充 + +- **扣费来源流水**:开局扣费写入账户流水 `source = 60`(`TtAccountRecordSource.OLYMPUS_SPIN`,玫瑰女郎消费)。 +- **奖励流水**:`source = 61`(`TtAccountRecordSource.OLYMPUS_REWARD`,玫瑰女郎奖励)。**只在玩家分解饰品时入账**:无论「放入背包后再分解」还是末帧「立即分解」(含前端自动分解),口径完全一致;开局本身不发奖励流水。不会重复入账——同一饰品记录只结算一次(`status IN (0,2)` 状态机 + `user:decompose:lock` 锁)。 +- **背包记录来源**:命中饰品写入 `tt_box_records`,`source = OLYMPUS_BOX(13)`,`box_name = "OLYMPUS"`,`status=0`(我的库存)。 +- **身份维度(audience)**:`common` 普通 / `anchor` 主播(user_type=01) / `robot` 机器人(user_type=03) / `white` 白号(is_blank_account=1)。白号优先级最高,压过 user_type。 +- **注意**:白号一般为主播测试/展示用,其玩局不注水、不扣水、不受水位约束,前端无需特殊处理。 +- **1000x 封顶**:单局总倍率 ≥ 1000 即 `maxWinReached=true`,`spinsRemaining=0`;到账同样按 1000 倍截断。 +- **游戏参数(速查)**:棋盘 5×6=30 格;同符号 ≥8 消除(8-9/10-11/12-30 三档);**炸弹只在免费旋转内出现**(每屏最多 1 颗,2~1000 倍,绿/蓝/紫/红/彩虹 5 档品质;normal/ante 的基础旋无炸弹);基础盘 4+ SCATTER 触发 15 旋免费旋转(三模式进 FS 后规则一致),FS 内 3+ 再 +5 旋,上限 30 旋(到上限后不再落 3+ 个 S)。 diff --git a/docs/疯狂转盘接口文档.md b/docs/疯狂转盘接口文档.md new file mode 100644 index 0000000..5809c91 --- /dev/null +++ b/docs/疯狂转盘接口文档.md @@ -0,0 +1,727 @@ +# 疯狂转盘 前端接口文档 + +> **版本**:v2.0 +> **更新时间**:2025-01 +> **适用前端**:Web/H5/小程序 +> **Base URL**:`{host}/api/wheel` + +--- + +## 📋 目录 + +1. [玩法概述](#一玩法概述) +2. [通用约定](#二通用约定) +3. [接口清单](#三接口清单) +4. [接口详情](#四接口详情) +5. [时间轴使用指南](#五时间轴使用指南) +6. [前端对接要点](#六前端对接要点) + +--- + +## 一、玩法概述 + +疯狂转盘是一款 **选颜色+符号 → 下注 → 转盘停 → 命中得饰品** 的真人轮盘游戏。 + +### 核心规则 + +| 项目 | 说明 | +|------|------| +| **盘面结构** | 内外圈各 24 格,索引一一对应
• 外圈符号:固定布局(A金/B红/C紫/D蓝)
• 内圈颜色:每轮随机排布(金6/红8/紫10) | +| **下注方式** | 选 1 个「颜色×符号」组合 + 1 个下注额
**每轮每人最多 50 注** | +| **下注额档位** | 10 档:5 / 10 / 20 / 50 / 100 / 200 / 500 / 1000 / 2000 / 5000 元 | +| **倍率** | 12 个倍率(3 色 × 4 符号),所有档位共用
例:gold+D 倍率=22,押 100 元中奖得 2200 元 | +| **命中判定** | 下注的 `color+symbol` == 开奖的 `resultColor+resultSymbol` | +| **中奖奖励** | 饰品价值 = **下注额 × 倍率**
直接进背包,无需选择"保留/分解" | +| **未中奖励** | 每注发 **0.01 元安慰饰品**(保证每注必有产出) | +| **决策池** | 单一真实奖池,所有人同一个落点
• `real`(真实池,参与落点/RTP 决策)= 普通(02)+主播(01)+招商(04)+卡商(05)
• `display`(展示,可下注/返奖但不入真实池)= 机器人(03)+白号(06) | + +### 一局生命周期 + +``` +newRound 创建 + ↓ +下注期 (30s) ← 玩家可调用 /dobet 下注 + ↓ +封盘期 (2s) ← 不再接受下注,倒计时"即将开奖" + ↓ +开奖 + 动画 (7s) ← 后端出结果,前端播转盘动画 + ↓ +结果展示 → 下一局 +``` + +--- + +## 二、通用约定 + +### 2.1 请求方式 + +- **协议**:HTTPS +- **Method**:全部 `POST` +- **Content-Type**:`application/json` +- **鉴权**:`Authorization: Bearer {token}`(登录后从响应头或登录接口获取) + +> ⚠️ **重要**:`userId` 由后端从 token 解析,**客户端不要传 userId 参数**。 + +### 2.2 统一响应结构 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": {} +} +``` + +| code | 含义 | 前端处理 | +|------|------|----------| +| 200 | 成功 | 正常处理 data | +| 500 | 业务失败 | Toast 显示 msg | +| 701 | 未实名认证 | 弹实名认证弹窗 | +| 702 | 账户余额不足 | 提示充值 | +| 703 | 玩家未充值,不能游玩 | 引导首充 | + +### 2.3 货币单位 + +- 所有金额字段为 **String 类型**,单位:**元**(精确到小数点后 2 位) +- 示例:`"balance": "9800.50"` 表示 9800.50 元 + +### 2.4 时间格式 + +- **时间戳**:`Long` 类型,**秒级** Unix 时间戳(如 `1738300000`) +- **时间字符串**:`yyyy-MM-dd HH:mm:ss`(东八区) + +--- + +## 三、接口清单 + +| # | 接口名称 | 路径 | 用途 | 调用时机 | +|---|---------|------|------|---------| +| 1 | ~~获取游戏配置~~ | ~~`/game_config`~~ | **已废弃**,数据已合并到 `/last_round_info` | ~~进入游戏页时调用 1 次~~ | +| 2 | 投注 | `/dobet` | 下注一笔 | 玩家点击下注按钮 | +| 3 | 获取当前局状态 | `/last_round_info` | **一次拿全**:配置+状态+开奖+下注 | **轮询**(1-2 秒间隔) | +| 4 | 我的下注记录 | `/history_bet_info` | 我的全部下注记录(中奖+未中奖,分页) | 打开"记录"页 | +| 5 | 全服中奖记录 | `/round_history_bet_info` | 全服中奖公示(只含真正命中,分页) | 打开"中奖榜"页 | +| 6 | 历史开奖结果 | `/round_results` | 每一轮的开奖符号(分页) | 打开"历史开奖"页 | + +--- + +## 四、接口详情 + +### 4.1 ~~获取游戏配置~~ (已废弃) + +> **⚠️ 已废弃**:本接口数据已全部合并到 `/last_round_info`,前端只需轮询一个接口即可。 +> 旧接口保留仅为兼容旧版前端,新前端请直接使用 `/last_round_info`。 + +--- + +### 4.2 投注 + +**`POST /api/wheel/dobet`** + +下注一笔。**每轮每人上限 50 注**。 + +#### 请求参数 + +```json +{ + "bet": 100, + "color": "gold", + "symbol": "D" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `bet` | Integer | ✅ | 下注额,必须是 10 档之一 | +| `color` | String | ✅ | 内圈色:`gold` / `red` / `purple` | +| `symbol` | String | ✅ | 外圈符:`A` / `B` / `C` / `D` | + +#### 响应示例 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "roundId": 10086, + "cost": "100.00", + "balance": "9700.50", + "credits": "1200.00" + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `roundId` | Long | 本回合 ID | +| `cost` | String | 本次扣款(= 下注额) | +| `balance` | String | 下注后余额(金币) | +| `credits` | String | 下注后积分 | + +#### 常见错误 + +| code | msg | 场景 | +|------|-----|------| +| 500 | 网站维护中...... | 全站维护 | +| 500 | 疯狂转盘维护中...... | 转盘维护 | +| 500 | 游戏不在投注期 | 已封盘/开奖中 | +| 500 | 每轮最多下注50注 | 超出上限 | +| 500 | 该下注组合未配置奖品 | 矩阵缺失(联系运营) | +| 702 | 账户余额不足 | 需充值 | +| 703 | 玩家未充值不能游玩任何游戏 | 需首充 | + +--- + +### 4.3 获取当前局状态 + +**`POST /api/wheel/last_round_info`** + +**核心接口**,轮询调用(建议 1-2 秒间隔),**一次返回全部数据**:游戏配置 + 当前回合状态 + 开奖结果 + 我的下注。 + +#### 请求参数 + +```json +{} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "roundId": 10086, + "periodNo": 202501011, + "serverTime": 1738300050, + "startTime": 1738300000, + "bettedTime": 1738300030, + "spinTime": 1738300032, + "endTime": 1738300039, + "status": "spined", + + "outerSymbols": ["A","D","C","D","B","D","C","D","B","C","D","C","A","D","C","D","C","D","B","D","C","D","C","D"], + "bets": [5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000], + "myPoolType": "real", + "rewardMatrix": [ + { + "betAmount": 5, + "color": "gold", + "symbol": "D", + "multiplier": 22, + "prizeValue": "110.00", + "prizeName": "黄金玫瑰", + "prizeGrade": 3, + "prizeExterior": "崭新出厂", + "prizeImage": "https://example.com/prize.png", + "prizeLevelImg": "https://example.com/level3.png" + } + ], + + "innerColors": ["gold","gold","red","purple","purple","red","gold","purple","red","purple","gold","purple","red","purple","gold","gold","purple","purple","red","gold","purple","red","purple"], + "resultIndex": 5, + "resultColor": "gold", + "resultSymbol": "D", + "totalWin": "2200.00", + "userBets": [ + { + "color": "gold", + "symbol": "D", + "count": 3, + "totalBet": "300.00" + }, + { + "color": "red", + "symbol": "A", + "count": 1, + "totalBet": "50.00" + } + ], + "winResult": [ + { + "color": "gold", + "symbol": "D", + "hit": true, + "boxRecordId": 123456, + "prizeName": "黄金玫瑰", + "prizeImage": "https://example.com/prize.png", + "prizeGrade": 5, + "prizeLevelImg": "https://example.com/level5.png", + "prizeExterior": "崭新出厂", + "prizeValue": "2200.00" + }, + { + "color": "red", + "symbol": "A", + "hit": false, + "boxRecordId": 123457, + "prizeName": "安慰饰品", + "prizeImage": "https://example.com/consolation.png", + "prizeGrade": 1, + "prizeLevelImg": "https://example.com/level1.png", + "prizeExterior": "崭新出厂", + "prizeValue": "0.01" + } + ] + } +} +``` + +#### 响应字段说明 + +**游戏配置字段**(原 `/game_config` 接口数据,现已合并): + +| 字段 | 类型 | 说明 | +|------|------|------| +| `outerSymbols` | Array\ | 外圈 24 格符号固定布局(`A`/`B`/`C`/`D`)
例:`["A","D","C","D",...]` | +| `bets` | Array\ | 10 档下注额 | +| `myPoolType` | String | 当前用户所属决策池
`real`:真实池(普通02/主播01/招商04/卡商05,参与落点与 RTP)
`display`:展示(机器人03/白号06,可下注/返奖但不入真实池)
`null`:不可参与(如超管99) | +| `rewardMatrix` | Array | 奖励矩阵(120 条,10 档 × 3 色 × 4 符号)
每条含 `betAmount`/`color`/`symbol`/`multiplier` 及饰品信息(名称/图片/品质图片/磨损度/价格) | + +**当局状态字段**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `roundId` | Long | 回合 ID | +| `periodNo` | Long | 全局唯一期号 | +| `serverTime` | Long | **服务器当前时间**(秒级时间戳)
用于和其他时间点对比算倒计时 | +| `startTime` | Long | 本局开始时间 | +| `bettedTime` | Long | **封盘时间点** = startTime + 下注时间
到这个时刻后不能再下注 | +| `spinTime` | Long | **开奖时间点** = bettedTime + 封盘时间
到这个时刻后开始播转盘动画 | +| `endTime` | Long | **本局结束时间** = spinTime + 开奖动画时间
动画播完、下一局即将开始 | +| `status` | String | `betting`:投注期
`spined`:封盘/开奖中 | +| `innerColors` | Array\ | **本轮内圈 24 色实际排列**(`gold`/`red`/`purple`)
例:`["gold","red","purple",...]` 用于渲染盘面 | +| `resultIndex` | Integer | 开奖格子索引(0-23)
未开奖时为 `null` | +| `resultColor` | String | 开奖颜色 `gold`/`red`/`purple`
未开奖时为 `null` | +| `resultSymbol` | String | 开奖符号 `A`/`B`/`C`/`D`
未开奖时为 `null` | +| `totalWin` | String | 当前用户本轮总返奖(饰品价值合计) | +| `userBets` | Array | **当前用户本轮下注汇总**(按颜色+符号聚合,见下表) | +| `winResult` | Array | **当前用户本轮发奖明细**(每注一条,见下表) | + +**userBets 结构**(我的下注汇总,按颜色+符号聚合): + +| 字段 | 类型 | 说明 | +|------|------|------| +| `color` | String | 下注的颜色 | +| `symbol` | String | 下注的符号 | +| `count` | Integer | 下注次数 | +| `totalBet` | String | 总下注额 | + +**winResult 结构**(我的发奖明细,每注一条): + +| 字段 | 类型 | 说明 | +|------|------|------| +| `color` | String | 下注的颜色 | +| `symbol` | String | 下注的符号 | +| `hit` | Boolean | 是否命中(`true`=中奖矩阵饰品,`false`=安慰饰品) | +| `boxRecordId` | Long | **背包记录ID**(用于调用分解/出售接口) | +| `prizeName` | String | 实际发放的饰品名称 | +| `prizeImage` | String | 饰品图片 URL | +| `prizeGrade` | Integer | 饰品品质等级 | +| `prizeLevelImg` | String | 饰品品质图片 URL | +| `prizeExterior` | String | 饰品磨损度 | +| `prizeValue` | String | 饰品价格(命中=下注额×倍率;安慰=0.01) | + +> 💡 **前端使用提示**: +> - 未开奖时 `winResult` 为空 +> - 已开奖后,`winResult` 每注一条:`hit=true` 是中奖饰品,`hit=false` 是安慰饰品(0.01元) +> - 使用 `boxRecordId` 调用分解/出售接口 + +--- + +### 4.4 我的下注记录 + +**`POST /api/wheel/history_bet_info`** + +当前登录用户自己的全部下注记录(中奖+未中奖),分页。未中奖注单(`win = 0.01` 安慰奖)的饰品字段为 `null`。 + +#### 请求参数 + +```json +{ + "pageNum": 1, + "pageSize": 10 +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "total": 25, + "rows": [ + { + "periodNo": 2026090725038, + "endTime": "2026-09-07 09:14:49", + "userId": 666299, + "nickName": "李四2", + "avatar": "/profile/upload/xxx.png", + "bet": "5.00", + "betSymbol": "D", + "win": "40.00", + "betCount": 3, + "prizeGrade": 1, + "prizeName": "某某饰品", + "prizeImage": "/profile/upload/xxx.png" + } + ] + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `total` | Long | 总中奖注数 | +| `rows` | Array | 中奖注单列表 | +| `rows[].periodNo` | Long | 完整期号 | +| `rows[].endTime` | String | 回合结束时间(yyyy-MM-dd HH:mm:ss) | +| `rows[].userId` | Long | 用户 ID | +| `rows[].nickName` | String | 用户昵称(完整,不脱敏) | +| `rows[].avatar` | String | 用户头像 | +| `rows[].bet` | String | 单注下注额 | +| `rows[].betSymbol` | String | 中奖符号 | +| `rows[].win` | String | 中奖额 | +| `rows[].betCount` | Integer | 该中奖符号下注次数 | +| `rows[].prizeGrade` | Integer | 饰品品质等级(对应 tt_ornaments_level.id:1金/2红/3紫/4蓝) | +| `rows[].prizeName` | String | 饰品名称 | +| `rows[].prizeImage` | String | 饰品图片 | + +--- + +### 4.5 全服中奖记录 + +**`POST /api/wheel/round_history_bet_info`** + +全服中奖公示(只保留真正命中 `win > 0.01` 的注单,排除 0.01 安慰奖),分页。结构与 `history_bet_info` 完全一致,只是数据范围为全服所有用户。 + +#### 请求参数 + +```json +{ + "pageNum": 1, + "pageSize": 30 +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "total": 99, + "rows": [ + { + "periodNo": 2026090725038, + "endTime": "2026-09-07 09:14:49", + "userId": 666299, + "nickName": "李四2", + "avatar": "/profile/upload/xxx.png", + "bet": "5.00", + "betSymbol": "D", + "win": "40.00", + "betCount": 3, + "prizeGrade": 1, + "prizeName": "某某饰品", + "prizeImage": "/profile/upload/xxx.png" + } + ] + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `total` | Long | 总中奖注数 | +| `rows` | Array | 中奖注单列表 | +| `rows[].periodNo` | Long | 完整期号 | +| `rows[].endTime` | String | 回合结束时间 | +| `rows[].userId` | Long | 中奖用户 ID | +| `rows[].nickName` | String | 中奖用户昵称(完整,不脱敏) | +| `rows[].avatar` | String | 中奖用户头像 | +| `rows[].bet` | String | 单注下注额 | +| `rows[].betSymbol` | String | 中奖符号 | +| `rows[].win` | String | 中奖额 | +| `rows[].betCount` | Integer | 该中奖符号下注次数 | +| `rows[].prizeGrade` | Integer | 饰品品质等级(对应 tt_ornaments_level.id:1金/2红/3紫/4蓝) | +| `rows[].prizeName` | String | 饰品名称 | +| `rows[].prizeImage` | String | 饰品图片 | + +--- + +### 4.6 历史开奖结果 + +**`POST /api/wheel/round_results`** + +获取每一轮的开奖符号(用于展示历史开奖记录,如右侧滚动的历史中奖列表)。 + +#### 请求参数 + +```json +{ + "pageNum": 1, + "pageSize": 20 +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `pageNum` | Integer | 否 | 页码(默认 1) | +| `pageSize` | Integer | 否 | 每页条数(默认 20) | + +#### 响应示例 + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "rows": [ + { + "periodNo": 2025010100086, + "date": "20250101", + "roundId": 86, + "resultIndex": 5, + "resultColor": "gold", + "resultSymbol": "D", + "endTime": "2025-01-01 12:15:39" + }, + { + "periodNo": 2025010100085, + "date": "20250101", + "roundId": 85, + "resultIndex": 12, + "resultColor": "red", + "resultSymbol": "A", + "endTime": "2025-01-01 12:15:00" + } + ], + "total": 2156 + } +} +``` + +#### 响应字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `rows` | Array | 历史开奖记录列表 | +| `rows[].periodNo` | Long | **全局唯一期号**(13位,前8位=日期yyyyMMdd,后5位=局号) | +| `rows[].date` | String | **日期**(yyyyMMdd,从 periodNo 拆分而来) | +| `rows[].roundId` | Long | **当天局号**(从 periodNo 拆分而来) | +| `rows[].resultIndex` | Integer | 开奖格子索引(0-23) | +| `rows[].resultColor` | String | 开奖颜色(gold/red/purple) | +| `rows[].resultSymbol` | String | 开奖符号(A/B/C/D) | +| `rows[].endTime` | String | 本局结束时间(yyyy-MM-dd HH:mm:ss) | +| `total` | Long | 总记录数 | + +#### 前端展示建议 + +```javascript +// 展示期号 +function formatPeriodNo(periodNo) { + const str = String(periodNo).padStart(13, '0'); + const year = str.slice(0, 4); + const month = str.slice(4, 6); + const day = str.slice(6, 8); + const round = parseInt(str.slice(8)); // 去前导零 + return `${year}-${month}-${day}-${round}`; +} + +// 渲染列表 +data.rows.forEach(item => { + // 期号:2025-01-01-86 + const periodText = formatPeriodNo(item.periodNo); + + // 开奖结果:金色箱子 × 15 + const symbolIcon = getSymbolIcon(item.resultColor, item.resultSymbol); + + renderHistoryItem(periodText, symbolIcon); +}); +``` + +--- + +## 五、时间轴使用指南 + +**核心**:后端返回的是**具体时间点**(startTime/bettedTime/spinTime/endTime),前端根据这些时间点和 `serverTime` 做倒计时和阶段切换。 + +### 一局的完整时间轴 + +``` +startTime (本局创建) + │ + ├─ 【下注期】serverTime < bettedTime + │ 前端显示:倒计时 "距封盘 X 秒",接受下注 + │ 倒计时 = bettedTime - serverTime + │ + ├─ bettedTime (封盘时刻) + │ + ├─ 【封盘期】bettedTime ≤ serverTime < spinTime + │ 前端显示:"封盘中",倒计时 "X 秒后开奖" + │ 倒计时 = spinTime - serverTime + │ + ├─ spinTime (开奖时刻) + │ + ├─ 【开奖中】spinTime ≤ serverTime < endTime + │ 前端显示:播转盘旋转动画 + │ 指针停在 resultIndex 格,显示 resultColor+resultSymbol + │ 动画时长 = endTime - spinTime(由后台"开奖动画时间"配置) + │ + ├─ endTime (本局结束) + │ + └─ 【等待下一局】serverTime ≥ endTime + 前端显示:倒计时 "下一局 X 秒后开始" + 继续轮询,拿到新 roundId 进入新一轮 +``` + +### 前端倒计时计算公式 + +| 阶段 | 判断条件 | 倒计时公式 | 显示文案 | +|------|---------|-----------|---------| +| **下注期** | `serverTime < bettedTime` | `bettedTime - serverTime` | "距封盘 X 秒" | +| **封盘期** | `bettedTime ≤ serverTime < spinTime` | `spinTime - serverTime` | "X 秒后开奖" | +| **开奖中** | `spinTime ≤ serverTime < endTime` | 播动画 | "转盘旋转中" | +| **结果展示** | `serverTime ≥ endTime` | `下一局startTime - serverTime` | "下一局 X 秒后" | + +### 关键提醒 + +1. **后端不返回时长配置**(下注时间/封盘时间/动画时间),只返回时间点。前端不需要自己算,直接用时间点差值。 +2. **时间点会随后台配置变化**:运营改"下注时间"从 30→50 秒,新局的 `bettedTime` 就会自动延后,前端倒计时自动变长。 +3. **开奖动画时长** = `endTime - spinTime`,这个窗口内播动画 + 展示结果。 + +--- + +## 六、前端对接要点 + +### 6.1 进入游戏页 + +```javascript +// 只需轮询 /last_round_info,一次拿到配置+状态 +setInterval(async () => { + const round = await getLastRoundInfo(); + + // 首次进入:用配置数据渲染静态元素 + if (firstTime) { + renderWheel(round.outerSymbols); + renderBetPanel(round.bets, round.rewardMatrix); + firstTime = false; + } + + // 每次轮询:更新动态状态 + updateRoundState(round); +}, 2000); +``` +``` + +### 6.2 轮询当前局状态 + +```javascript +// 每 1-2 秒调用一次 +setInterval(async () => { + const round = await getLastRoundInfo(); + + // 根据 status 和时间点切换 UI + if (round.status === 'betting') { + if (round.serverTime < round.bettedTime) { + // 下注期 + showCountdown('距封盘', round.bettedTime - round.serverTime); + enableBetting(true); + } else { + // 封盘期(status 可能还未切到 spined) + showCountdown('即将开奖', round.spinTime - round.serverTime); + enableBetting(false); + } + } else if (round.status === 'spined') { + // 已开奖 + if (round.resultIndex !== null) { + // 播转盘动画,指针停在 resultIndex + playSpinAnimation(round.resultIndex, round.innerColors); + // 展示发奖明细(winResult 每注一条,hit 区分中奖/安慰) + showWinResult(round.winResult, round.totalWin); + } + // 倒计时下一局 + if (round.serverTime >= round.endTime) { + showCountdown('下一局', estimateNextRound()); + } + } +}, 2000); +``` + +### 6.3 下注 + +```javascript +async function doBet(color, symbol, bet) { + try { + const res = await api.post('/api/wheel/dobet', { color, symbol, bet }); + if (res.code === 200) { + // 扣款成功,更新余额 + updateBalance(res.data.balance, res.data.credits); + // Toast 提示 + showToast('下注成功'); + } + } catch (err) { + if (err.code === 702) { + showToast('余额不足,请充值'); + } else if (err.code === 500) { + showToast(err.msg); // "游戏不在投注期" / "每轮最多50注" + } + } +} +``` + +### 6.4 展示饰品信息 + +```javascript +// winResult 每注一条,hit 区分中奖/安慰 +round.winResult.forEach(item => { + if (item.hit) { + // 中奖,展示矩阵饰品 + showPrize({ + name: item.prizeName, + image: item.prizeImage, + levelImg: item.prizeLevelImg, + exterior: item.prizeExterior, + value: item.prizeValue, + boxRecordId: item.boxRecordId + }); + } else { + // 未中,展示安慰饰品 + showConsolation(item.color, item.symbol); + } +}); +``` + +### 6.5 渲染盘面(innerColors) + +```javascript +// innerColors 是 24 个元素的数组,已经是 gold/red/purple +// 例:["gold","red","purple",...] +function renderWheel(innerColors, outerSymbols) { + for (let i = 0; i < 24; i++) { + const color = innerColors[i]; // 'gold' / 'red' / 'purple' + const symbol = outerSymbols[i]; // 'A' / 'B' / 'C' / 'D' + + // 渲染第 i 格:内圈色 + 外圈符号 + renderCell(i, color, symbol); + } +} +``` + +### 6.6 注意事项 + +| 注意项 | 说明 | +|-------|------| +| **单接口轮询** | 只需轮询 `/last_round_info`,配置数据已合并,无需单独调 `/game_config` | +| **不要自动重发 dobet** | 下注接口**非幂等**,失败时提示玩家手动重试 | +| **轮询间隔** | 建议 1-2 秒,不要过于频繁 | +| **时区** | 所有时间戳为东八区(UTC+8) | +| **饰品已入背包** | 中奖后饰品直接进背包,无需"保留/分解"抉择
要兑现走通用背包分解接口 | +| **维护模式** | 后台"游戏关闭"开关打开后:
① 新局无法下注(`dobet` 返回 500 维护提示)
② 轮询接口返回 `isMaintenance: true`,前端应弹窗并跳转
③ 进行中回合不强制结算,玩家可继续看结果 | + +--- + diff --git a/docs/麻将胡了设计方案.md b/docs/麻将胡了设计方案.md new file mode 100644 index 0000000..5870779 --- /dev/null +++ b/docs/麻将胡了设计方案.md @@ -0,0 +1,1425 @@ +# 麻将胡了(Mahjong)电子游戏 · 设计方案 v1.0 + +> 目标:把「麻将胡了」(5 轴 × 4 行、1024 路、级联消除、金色符号、乘数阶梯、免费旋转)做成**玫瑰女郎(Olympus)同一套设计模式**的兄弟游戏。 +> 参考实现:`olympus-py/`(回合池生成器)+ `skins-service/game-olympus-service/`(Java 服务)+ `ruoyi-ui/src/views/game/olympus/`(后台)+ `docs/玫瑰女郎接口文档.md`(接口契约)。 +> 状态:**方案待评审**,未动任何代码。末尾第 13 节列出必须由你拍板的 10 个问题。 + +--- + +## 0. TL;DR + +| 项 | 结论 | +|---|---| +| 复用什么 | 离线预生成回合池 + 倍率桶配额控 RTP + 服务端挑回合 + 客户端逐帧续播 + 按箱×身份奖池水位 + 开局即结算 | +| 玩法范围 | **只有一个「普通旋转」模式**(用户明确不做底注、不做购买免费旋转)→ **12 个箱子**(只按投注档分,档位见 §2.8.1)、无模式倍率、无 `need_fs` 过滤 | +| 必须重写什么 | ① 中奖判定(1024 路**前缀链**:必须从第 1 轴起 1→2→3→4→5 连续,中间断链即整笔作废,替代 8+ 聚簇)② 盘面几何(5×4=20 格替代 5×6=30 格)③ 乘数介质(阶梯倍率替代炸弹球)④ **级联的消除规则**(金色中奖格**不清空**,就地留一个百搭占位) | +| 发奖形态 | **与玫瑰女郎完全一致**:游戏内**显示现金**(「共赢得 X.XX」),但**最终赢额转成一个饰品发到背包**(`settleAtStart` 按赢额落区间查饰品 → 写 `tt_box_records`),玩家在末帧选择「放入背包 / 立即分解」。→ 所以**不做**"开户即加余额"那条近路,`prize_ornament` / `prize_range` 两张表都要 | +| 新增资产 | `mahjong-py/`(生成器)+ `game-mahjong-service`(Java 模块)+ **11 张** `game_mahjong_*` 表 + 7 个后台页(含奖品管理)+ `docs/麻将胡了接口文档.md` + Cocos 面板 | +| 工作量 | 后端(py + Java + 后台 + 文档)≈ **20~26** 人日;客户端 ≈ 15~20 人日;联调/RTP 验收 ≈ 5 人日 | +| 最大风险 | ① 1024 路天然高方差 → 靠"桶配额 + 收敛阶段 + 封顶"压;② **没有"买 FS"兜底后,100x 以上的高倍桶只能靠 normal 池自己构造**;③ py/Java 两份 `calcRounds` 历史性不同步;④ 赔付计量单位(=总额÷20)若在规则页没说清,玩家会按"倍总额"理解而产生对账投诉 | + +--- + +## 1. 先把"玫瑰女郎设计模式"拆成 12 条可照抄的规则 + +这一节是整份方案的骨架。麻将胡了必须**逐条对齐**,任何一条偷懒都会在后期以 bug 的形式回来。 + +| # | 规则 | 玫瑰女郎现状(证据) | 麻将胡了落法 | +|---|---|---|---| +| 1 | **服务端权威**:客户端不能决定任何结果 | 全部结果由后端挑出的回合决定 | 一致 | +| 2 | **离线预生成回合池**:Python 造 `round`+`step` 落库,Java 只挑不算 | `olympus-py/generate_rounds.py` 写 11.7 万回合 / 106 万步 | `mahjong-py/generate_rounds.py`,同结构 | +| 3 | **桶(倍率档)定义赔付区间**,`expect_mul` = 区间中点,区间连续无缝 | `game_olympus_ornament` 17 档 0x~1000x | `game_mahjong_bucket` 10 档(见 §3.1) | +| 4 | **桶配额 = RTP 旋钮**(不是概率硬编码) | `game_olympus_box_ornament.count`,4 身份各一套 | `game_mahjong_box_bucket`,同构 | +| 5 | **两阶段抽取**:固定阶段按 `remaining` 加权消耗库存;收敛阶段按 `desiredNextMul` 逼近目标且不消耗 | `OlympusRoundPicker.pick():44-100` | 同逻辑,`bucket` 替 `ornament` | +| 6 | **按箱 × 身份奖池水位**:`payoutCap = max(0, 水位+本局成本) × rtp`;白号 `payoutCap=null` | `OlympusRewardPoolCacheRepository:80-84` | 沿用(水位 = Σ注册成本 − Σ派彩现金 + adjust) | +| 7 | **身份分桶** common/anchor/robot/white,白号独立库存不进真实经济 | `OlympusAudience` | 直接复用(建议上移到共享模块) | +| 8 | **开局即结算 + 幂等守卫**:`settled 0→1` 的 UPDATE 守卫;`settle_source=start→finish` | `OlympusGameService.settleAtStart():279-306` | 一致,但结算动作从"写背包"改为"加余额" | +| 9 | **客户端只续播**:`dospin` 带 `boxId` = 开新局;不带 = 下一步;`state=FINISH` 才展示结算 | `OlympusGameService.continueGame():230-254` | 一致 | +| 10 | **Redis 单局缓存 30 分钟 + `hasActive` 重连**,重连要能接回累计量 | `ruoyi:olympus:{userId}` TTLL 30min | `ruoyi:mahjong:{userId}`;接回量 = 本旋波次/本局累计 | +| 11 | **一轮 cycle**:`N = 固定池/(1−收敛比)`,抽满 `resetCycle + refillFromConfig`(模板) | `OlympusCycleRepository` + `box_template` | 一致 | +| 12 | **运维面**:维护开关 + 后台 7 页 + 注水/抽水/重置(原因必填)+ 流水 source 登记 6 处聚合 | `sys_config.olympusMaintenance`、`TtAccountRecordSource:193-279` | 同名同构(见 §6.5) | + +**结论:12 条全部照抄,一条都不改。**(用户 2026-09-21 定案:麻将胡了的赢额也**要变成饰品发到背包**,所以"赢额 → 落区间 → 查饰品 → 写背包 → 末帧选择放入/分解"这条链路与玫瑰女郎一模一样,连 `settleAtStart` 的 `max(totalWin, 0.01)` 兜底都能照抄。) + +**唯一的表述差异**:规则页写着「所有赢奖以现金显示」——这是**展示口径**(游戏内「共赢得 X.XX」显示的是钱),**发放口径**仍是饰品。玫瑰女郎也是这么做的(它的 `settledWin` 就是"本局价值 = 饰品价格",前端同样显示金额)。这句话不是"直付余额"的意思。 + +--- + +## 2. 游戏规格(截图是唯一事实来源) + +### 2.1 盘面与 1024 路判定 + +- 5 轴(reel)× 4 行 = 20 格;ways = 4⁵ = **1024**。 +- **判定口径(用户 2026-09-21 明确澄清,必须逐字实现)**: + + > 中奖符号必须**从第 1 轴开始、按 1→2→3→4→5 的顺序连续出现**,中间不许断。 + > 第 1 轴是**必需**的;第 2 轴断了,后面 3/4/5 轴有再多也不能中奖。 + > 每一轴上该符号可以有好几个(最多 4 个),只要求「每轴至少 1 个」。 + + 即**前缀链(prefix run)**,不是"任意 3 轴凑齐",也不是"从第 2 轴起算": + + ```python + # 对每个付费符号 X(含百搭替代): + n = 0 + for c in range(5): # 第 1 轴 → 第 5 轴 + cnt = count(X or WILD in 轴 c) # 该轴 X 的格数(百搭计入),0~4 + if cnt >= 1: n += 1 + else: break # 一旦断链,后面全部作废 + if n < 3: 不成奖 # n 只能是 3 / 4 / 5 才有档位 + ways = Π(第 1..n 轴各自的格数) # 只乘链内的轴,链外的轴不乘 + 本笔 = 赔付表[X][n] × ways + ``` + +- **反例与正例(照这张表写单测)**: + + | 各轴该符号格数(轴1~轴5) | 结果 | 原因 | + |---|---|---| + | `2, 3, 1, 0, 0` | ✅ 3 连,ways=6 | 前缀 3 轴连续 | + | `2, 3, 1, 2, 0` | ✅ **4 连**,ways=12 | 第 5 轴才断,档位取 4 | + | `2, 0, 3, 4, 4` | ❌ 不成奖 | **第 2 轴断了**(用户举的例子),后面再多也没用 | + | `0, 4, 4, 4, 4` | ❌ 不成奖 | 第 1 轴没有 → 第 1 轴必需 | + | `1, 1, 0, 4, 4` | ❌ 不成奖 | 3 连必须是 1-2-3,不能跳到 1-2-4 | + | `1, 2, 3, 4, 4` | ✅ 5 连,ways=96 | 满链 | + | `1, 1, 1, 1, 1` | ✅ 最低 ways=1 | 每轴 1 个,共 1 路 | + +- 单笔赢额(倍基础下注)= `赔付表[X][n] × ways × 本波乘数`。 +- 多个符号各自独立成奖、**累加**(互不影响,各有各的前缀链);同一符号只按上述规则算一笔,不存在"同符号多个子路径重复叠加"。 +- 参考例子(截图):符号 `?` 三连赔 10,ways=6(各轴 1/3/2)→ 本笔 60。 + +**赔付表**(数值抄自赔付表截图,含义见下方"计量单位"): + +| 短码 | code | 中文名 | 3 连 | 4 连 | 5 连 | +|---|---|---|---|---|---| +| `A` | FAT_CHOI | 發 | 15 | 60 | 100 | +| `B` | RED_DRAGON | 中 | 10 | 40 | 80 | +| `C` | WHITE_DRAGON | 白板(紫框) | 8 | 20 | 60 | +| `D` | CHAR_8 | 八萬 | 6 | 15 | 40 | +| `E` | DOT_MULTI | 花筒(四色) | 4 | 10 | 20 | +| `F` | BAMBOO_MULTI | 索(红绿) | 4 | 10 | 20 | +| `G` | DOT_PURPLE | 筒(紫绿) | 2 | 5 | 10 | +| `H` | BAMBOO_GREEN | 索(绿) | 2 | 5 | 10 | +| `W` | WILD | 百搭(元宝) | — | — | — | **不由卷轴落下**,只能由金色符号转化产生(**每个金色格各留 1 个,无上限**),且只可能出现在第 2/3/4 轴 | +| `S` | SCATTER | 胡(夺宝符号) | — | — | — | 只触发免费旋转,不赔现金 | + +> ✅ **赔付计量单位 = 投注总额 ÷ 20(已用排除法锁死,Q0)** +> +> **两个已知事实**(均来自用户确认): +> ① 赔付表里的 `15 / 60 / 100` 是**最基础的 1 倍价**,不带任何阶梯倍率(用户 2026-09-21 原话:"这个是最基础的 1 倍的价格"); +> ② 规则页写着「游戏共有 1,024 个中奖路(固定)**并规定了 20 条基础投注**」——这句就是计量口径。 +> +> **排除法**(用真实运行截图:投注总额 ¥8.00、横幅「共赢得 **0.80**」、阶梯条高亮 **x2**): +> +> | 假设 | 推论 | 结论 | +> |---|---|---| +> | 这 ¥0.80 **已含** x2 倍率 | 基准价 = 0.80 ÷ 2 = ¥0.40 = **1 个赔付单位** | ❌ 赔付表里**没有值为 1 的档**(最小是 2),不可能 | +> | 这 ¥0.80 **未含**倍率(x1 基准价) | 若 1 单位 = ¥0.40 → 最低档 3 连(赔付值 2)× ways 1 = **¥0.80** ✅ | ✅ 成立,且 `8.00 ÷ 20 = 0.40` **正好** | +> +> → **赔付单位 = 投注总额 ÷ 20 是唯一自洽解**;同时确认「阶梯条高亮 x2」表示的是**下一波(第 2 波)即将生效的倍率**,不是本波已生效的。 +> +> 所以本游戏的完整赔付公式是: +> ``` +> 本笔赢额 = 赔付表[X][连轴数] × ways × (投注总额 ÷ 20) × 本波阶梯倍率 +> = 赔付表[X][n] × ways × (box.bet × betMul ÷ 20) × rotationMul +> ``` +> `PAY_UNIT_DIVISOR = 20` 写进 `config.py`,**不要散落在各处**。 +> +> **真机复核(30 秒,做一次就彻底放心)**:投注总额 **¥8.00** 时,若 K=20,则**所有中奖金额都必须是 ¥0.40 的整数倍**(0.80 / 1.20 / 1.60 / 2.00 …)。只要出现过 **¥0.20 / ¥0.60 / ¥1.00** 这类**不是 0.40 整数倍**的金额,就说明 K=40,回来改一个常量即可。另外:¥8.00 时最低档符号 3 连 1 路应赔 **¥0.80**。 +> +> 为什么必须搞清:如果按"× 投注总额"实现,基础旋 RTP 会到 **300%~400%**(粗算:约 40% 的旋有 3+ 前缀链,均 1.7 路、均赔付 5.2 → 期望 3.5 倍/旋),游戏根本发不出去;除以 20 后基础旋自然 RTP ≈ **17%**,剩下交给免费旋转与阶梯——这正是这类游戏该有的结构。 +> 规则页展示的赔付表数值保持原样(15/60/100…),**但必须在规则页同时写明"每条基础投注"**,否则玩家会按"倍总额"理解,产生对账投诉。 + +> 出现权重 `SYMBOL_WEIGHTS` 初值建议 `{A:5,B:6,C:8,D:9,E:11,F:11,G:13,H:13,S:2}`,最终由 §5 的 RTP 校准反推,不在方案里定死。 +> ⚠️ **权重表里没有 `W`** —— 百搭不由卷轴落下(见 §2.2),`_fill` / 新旋 / 补位都必须断言不产出 `W`。 + +### 2.2 百搭与夺宝符号(前缀链;百搭只在第 2/3/4 轴) + +- 🎯 **百搭的两条铁律(用户 2026-09-21 确认)**: + 1. **原生盘面不会有百搭**——卷轴**永远不会落下**元宝,补位也不会补出元宝;**百搭的唯一来源是"金色普通符号被消除后的转化"**。 + 2. **几个金色格被消除,就留几个百搭**(**无数量上限**)——所以一屏**可以同时有多个百搭**(示例见 §15 第 1 波:两张金色「中」→ 留下 **2 个**百搭)。 +- 🎯 **从这两条推出的关键结论(决定了整套判定的正确性)**: + + > 金色符号**只出现在第 2/3/4 轴**(§2.3),而百搭的唯一来源是金色转化 + > ⇒ **百搭只可能出现在第 2/3/4 轴,第 1 轴永远没有百搭** + > ⇒ 前缀链的第 1 轴**必然是自然符号** + > ⇒ **任何一笔中奖都至少含 1 个自然符号**,**"纯百搭链"在数学上不可能出现**。 + + 这就是 Q1 永久关闭的原因(不需要 `WILD_PAYS` 开关、也不需要"每笔至少一个自然符号"的额外特判)——**由"金色只在 2/3/4 轴"自动保证**。 +- **百搭(元宝)**:**唯一不能替代的是「胡/FS」**(用户二次确认:"FS 不能作为百搭符号,其他的符号都可以作为百搭符号")。除此之外**付费符号、金色符号统统能替代**(金色本质就是普通符号,见 §2.3),在链里**等同于该符号计数**。 + - **盘面上只有一种百搭,就是元宝(短码 `W`)**,而且**只有金色转化这一个来源**(见 §2.3)。 + - **符号权重表里没有 `W`**:生成器的卷轴权重只包含 8 个付费符号 + 胡,`_fill` / 新旋 / 补位**不允许产出 `W`**。 + - 百搭能**补链中间的断点**:`發, 百搭, 發, _, _` → 3 连成立。 + - 百搭**不能**把断链处的胡"算成"付费符号,也**不会**因为某个胡的存在而变成胡——即 `發, 胡, 發` **不成链**(第 2 轴该符号计数为 0,链在第 2 轴断掉)。 + - **不需要** `WILD_REELS` 配置(原生不下百搭)。 +- ✅ **落地规则(引擎必须实现)**: + + | 场景 | 处理 | + |---|---| + | 本波有 N 个金色格中奖 | **N 个格子各自就地留下一个百搭**(N 可以是 0、1、2…,**无上限**) | + | **卷轴补位 / 新旋开局** | **永远不产生百搭**(原生不下百搭) | + | 盘面上的百搭参与中奖 | 随那一波被消除(和普通符号同权) | + | 盘面上的百搭**没有**参与任何成链 | **不消除**,留在盘面上继续等下一波(见下方第 ⑤ 行) | + + > 由此不再有"名额/上限"这类规则,`WILD_SLOT_POLICY` 开关**取消**——每个金色格各转各的,行为单一确定。 +- ✅ **百搭的完整生命周期(实现照这张表写,最容易错的是第 ⑤ 行)**: + + | 阶段 | 规则 | + |---|---| + | ① 出现 | **只能**由"金色普通符号参与中奖被消除"时就地转化产生(原生盘面永不出现百搭) | + | ② 数量 | **每个金色格各留 1 个百搭,无上限**(同一个盘面可以有 2 个、3 个…) | + | ③ 位置 | **只可能在第 2/3/4 轴**(因为金色只在那三轴)——第 1 轴永远没有百搭 | + | ④ 作用 | **可以作为任何付费符号**(除「胡」以外),在**前缀链判定里等同于该符号计数**——能补链、能搭桥;**同一个百搭可以被多个符号同时"借用"**(每条链各算各的) | + | ⑤ **消除条件** | **必须满足消除链**:只有它**参与了某条成立的中奖链**,才会随那一波被爆炸消除;**不满足链就不消除**,继续留在盘面上等下一波(用户原话:"是可以和其他符号是可以消除的(要满足消除链)") | + | ⑥ 能和谁一起消 | 百搭**和普通符号完全同权**:它参与的那条前缀链里可以是普通符号,**也可以是金色符号**;金色符号本来就是普通符号镀金,所以**不需要任何特殊逻辑** | + | ⑦ **不跨旋** | 下一旋**整盘重发**,百搭与金色标记都不带走;**每个旋开局时盘面上 0 个百搭** | + + > 换句话说:**百搭就是一张和普通符号完全同权的牌**,只不过它"什么都能当",而且全盘限量 1 张、名额会在金色格被消除时补上。 + > 实现上**不要给"百搭 + 金色"加任何特例**——金色符号在链判定里就是它自己的自然符号,和"發 + 百搭"没有区别。 +- **胡(夺宝/SCATTER)**:不参与 1024 路(**既不计入链,也不被百搭替代**);出现在任意轴任意位置;**自身不赔现金**(截图规则只给了免费旋转,没有 scatter 现金赔付),因此 `scatterTrigger.scatterWin` 恒为 0。 +- 3 个及以上胡 → 触发免费旋转:**12 旋 + 每多 1 个额外 +2 旋**(3→12 / 4→14 / 5→16 / 6→18)。免费旋转内再 3+ 胡 → 重触发,同样 12+2n。 + +### 2.3 金色麻将符号(本游戏最独特、也最容易做错的一条) + +截图原文: +1. 「在任何旋转期间,第 2、第 3 和/或第 4 轴上的某些符号(不包括百搭符号和夺宝符号)将可能显示为金色。」 +2. 「在新符号跌落后的每一轮旋转中,上一轮赢奖中所涉及的黄色麻将符号将被转换为百搭符号。」 + +**用户补充的落地口径(本条的权威解释,2026-09-21 多次确认)**: + +> 百搭符号 **就是"金色的普通符号"**——它是普通符号(比如「中」)镀了金色,**判定与赔付都按它的自然符号算**。 +> 这个符号**被消除之后,该位置会留下一个真正的百搭符号**(就是元宝 `W`)——这是**百搭在整个游戏里唯一的来源**(原生盘面不会有元宝); +> 之后它就按百搭的规则参与后续波的成奖与消除。**每个金色格各留一个百搭、无上限**(见 §2.2)。 + +所以转化时机不是"被消除后、下一波再补成百搭",而是**本波消除的那一刻就地变成百搭**。引擎按下面这条时序实现: + +``` +第 n 波结算(判定与赔付都用金色符号的"自然符号"身份,例如当「中」用): + 1. 算出本波所有中奖格(含金色格,金色格按它的自然符号计入 ways) + → ★ 这些格子**全部"被消除"**(客户端全部播爆裂动画,含金色格)。 + "消除"不等于"清空":消除之后该格才分成两类—— + 2. 非金色的中奖格 → 置为**空洞**,参与重力下落 + 顶部补新符号 + 3. 金色的中奖格 → 该符号被消耗;**若盘面上当前没有百搭,取其中 1 个就地写入 W** + (这个格子**不是空洞**、不补新符号;同波其余金色格按第 2 类处理,照常清空) + 4. 重力下落 + 顶部补位(这一步里 W 已经是普通盘面符号,见下方"锚定问题") + 5. 生成第 n 波的帧:grid_state 已经是转换后的盘面,并带 wild_converted=[pos,...] 供客户端播"金色→百搭"动画 +第 n+1 波: + W 按百搭规则替代任意付费符号(可搭桥、可补链,但**不能替代胡**),若它参与成奖则随该波被消除 +``` + +> 一句话记:**中奖格全部"被消除"(全爆),区别只在消除之后变成什么** —— 非金色格变成"空洞→补新牌",金色格变成"百搭"。这是和玫瑰女郎 `_collapse` 唯一的语义差别。 +> **统计口径也要注意**:说"本波消除了 4 个符号"包含金色格;说"本波有 3 个空洞要补牌"不含金色格。两个数不一样,别混。 + +**必须定死的三个实现细节**: + +- 🎯 **百搭的两条铁律**(用户 2026-09-21 补充,见 §2.2)。这两条**直接改写了第 3 步**: + - **原生盘面永远没有百搭**:卷轴不落元宝、补位也不补元宝,**百搭的唯一来源就是金色转化**; + - **每个金色格各留一个百搭**:同波有几个金色格中奖,就留下几个百搭(无上限)。卷轴永不落百搭。 + - 副作用(好副作用):**"纯百搭链"在数学上不可能出现**,原本要拍板的 Q1 自动消失,不需要 `WILD_PAYS` 开关。 +- ✅ **重力下落(Q2 已定案:不钉住)**。用户 2026-09-21 确认:"**符号是可以下落的,下面符号消除之后,上面符号会往下掉落**"——百搭也是盘面符号,**一视同仁跟着落**,不锚定。 + 列内重力按标准写法:**中奖格清空 → 该列幸存符号(含刚转化的百搭)保持相对顺序整体压到底 → 顶部补入新符号**。 + - 百搭**下方**的格被清空 → 百搭跟着掉下去; + - 百搭**上方**的格被清空 → 百搭留在原地不动,上方的牌落到它头上。 + - 如果把它做成"钉在空中",**它下方被清空的格就落不进任何符号**(上方的牌被它挡住),会出现悬空空洞、客户端与服务端对不上账 —— 所以**不能钉**。 + - 客户端要注意:**百搭会移动位置**,下落动画必须用帧里的 `dropData`(`from → to`)驱动,**不能假设它原地不动**;"金色→百搭"的变形动画(`wildConverted`)播完之后,紧接着就可能是它的下落动画。 +- ⚠ **"涉及中奖"的判定**:金色格被计入某笔中奖的 `ways`(无论它是作为该符号的自然符号,还是被当作其它符号的百搭替代计入)都算"涉及"。**金色格只做自己那一种符号的替代**——它显示为金色「中」时,对「中」是自然符号、对其它符号是百搭替代。 + > 注意:由于最多只有 1 个百搭,且金色格本身是自然符号,**"金色格被别人当百搭替代"的情况只可能由盘面上那个唯一的百搭产生**,不会成群出现。 + +**其余约定**: + +- 每旋开局(基础旋与每个 FS 旋)对**第 2/3/4 轴**上的普通符号按概率 `GOLDEN_CELL_PROB`(建议 0.10,逐格独立)打金色标记,不动 `W`/`S`;新补入的符号也可以带金色标记。 +- 转化后的 `W` **不再回填金色标记**(每格只转化一次)。 +- 金色符号在**波次判定时用的是自然符号**,所以"金色「中」链"就是「中」的链;转化后下一波起它才变成通用百搭。 +- 金色格也受"百搭同样会被消除"约束(§2.2 边界二):本波转成 W 之后,若在后面的波里参与成奖,就随那一波被消除。 + +### 2.4 乘数阶梯(奖金翻倍) + +- **赔付表是"1 倍基准价"**(用户 2026-09-21 确认:"这个是最基础的 1 倍的价格")——表里的 15/60/100 不含任何阶梯倍率,阶梯是**在它之上再乘**的。 +- 本旋内**每一波(rotation,即一次消除结算)用一个乘数**;该波所有 1024 路赔付 × 该乘数。 +- **基础旋转(用户原话)**:一次消除 x1、两次消除 x2、三次消除 x3、**四次消除 x5**(第 4 波及以后都是 x5)→ `LADDER_BASE = (1, 2, 3, 5)`。 +- **免费旋转**:`LADDER_FS = (2, 4, 6, 10)`(截图里 FS 模式下卷轴上方的 x2 x4 x6 x10)——**FS 的第 1 波就是 x2**。 +- 第 n 波取 `ladder[min(n, 4)]`。由于"第 n 波存在"本身就等价于"第 n−1 波有中奖",无需额外判断递进条件。 +- **每旋重新从第一档开始**(基础旋从 x1、FS 旋从 x2),不跨旋累计。 + +**完整算式串起来(一张表看懂)**: + +| 环节 | 值 | +|---|---| +| 赔付表基准值 | `赔付表[X][连轴数]`(如 發 3 连 = 15) | +| × 路数 | `ways`(各轴个数的乘积) | +| × 每单位金额 | `投注总额 ÷ 20`(Q0) | +| × 本波阶梯 | 基础旋 `1/2/3/5`;FS 旋 `2/4/6/10` | +| = 本笔赢额 | `赔付值 × ways × (bet×betMul÷20) × rotationMul` | + +> 由此可推出**本局最小中奖**(基础旋第 1 波、最低档符号、1 路):`2 × 1 × (bet÷20) × 1 = bet ÷ 10` —— 这条直接约束了下注档下限(见 §2.8.2)。 +- 截图里那条「1024 路中奖组合」横条下方的 `x1 x2 x3 x5`(当前档位金色高亮)就是这条阶梯的 HUD。 + +> 实现提示(来自 olympus 经验):Olympus 的"炸弹累计器"(`accumulator_before/after`,FS 内跨旋传递)机制**原封不动复用**——麻将胡了虽然乘数不跨旋累计,但"本步乘数 / 本波序号"需要每帧下发,字段名可直接沿用 `accumulator`(语义改为"本波乘数")以免客户端大改。**建议改名为 `rotationMul`**,避免误解。 + +### 2.5 级联(tumble) + +每一波:结算 → 中奖符号(含已转化的百搭)爆炸消除 → 该轴上方符号整体下落 → 该轴顶部补入新符号 → 进入下一波。直到本波无任何成奖为止。下一旋开始时盘面是完全新的一屏。 + +### 2.6 免费旋转 + +- 触发:3+ 胡(任意位置)。 +- 赠送:12 + 2×(胡数−3);重触发同式。 +- 免费旋转内乘数阶梯为 `[x2, x4, x6, x10]`。 +- 可重复触发;**总旋数上限 = 30 旋**(Q3 定案)。到顶后不再落 3+ 胡(生成端保证:一旦已发旋数到 30,后续 FS 旋不再摇出胡)。 +- 免费旋转内投注大小/倍数与触发时一致。 + +### 2.7 封顶 + +- 单局总倍率封顶 **500x**(Q4 定案)。到达封顶即截断:剩余旋数作废、`maxWinReached=true`、`spinsRemaining=0`,保证"动画总赢额 = 实际到账"。 +- 生成端在造回合时就按封顶截断并写 `win_capped` 标记(对齐 olympus `step.win_capped`)。 + +### 2.8 投注模型与下注档设计(推荐方案 A) + +原版:投注大小 0.02~50.00 × 投注倍数 1~10,另有现金钱包与自动旋转。 + +**方案 A(推荐)**:`箱子 = 投注档`,请求可带 `betMul(1~10)`: +- `price(实际扣费) = box.bet × betMul × costMultiplier`(本游戏 costMultiplier ≡ 1) +- **回合池里存的全是倍率**(`actual_mul`),与 `bet`/`betMul` 无关 → 调档/调倍数**不需要重生成池子**(这正是玫瑰女郎能做到"改配比不动池子"的根因)。 +- 奖池水位按 `(boxId, audience)` 聚合,`betMul` 只影响单局金额,不影响池结构。 + +**方案 B(不推荐)**:给每个 `bet × betMul` 组合建一个箱子 → 100 个箱子、池子碎片化、水位口径混乱。 + +#### 2.8.1 设计多少个下注额、哪几个、能多大? + +**先分清两件事,因为成本差一个数量级:** + +| 加什么 | 池子要不要动 | 成本 | +|---|---|---| +| **加档位**(多一个箱子) | **要**:每档一个独立回合池 ~4500 回合 / 8 万帧 | 贵(生成时间 + 40 条配比 + 一套水位) | +| **加 betMul 上限** | **不用**:池子只存倍率(`actual_mul`),与下注额无关 | **几乎为零** | + +→ 所以"想把数额做大"有两条路:**改档位表**(贵)或**抬高倍数上限**(免费)。 + +**推荐:12 档,覆盖到 ¥1000**(在玫瑰女郎 10 档基础上加 500 / 1000 两档),betMul 保持 1~10: + +| boxId | box.bet | × betMul(1~10) 实际扣费 | +|---|---|---| +| 1 | 1 | 1 ~ 10 | +| 2 | 3 | 3 ~ 30 | +| 3 | 5 | 5 ~ 50 | +| 4 | 10 | 10 ~ 100 | +| 5 | 20 | 20 ~ 200 | +| 6 | 30 | 30 ~ 300 | +| 7 | 50 | 50 ~ 500 | +| 8 | 100 | 100 ~ 1000 | +| 9 | 200 | 200 ~ 2000 | +| 10 | 300 | 300 ~ 3000 | +| **11** | **500** | **500 ~ 5000** | +| **12** | **1000** | **1000 ~ 10000** | + +- 成本增量很小:12 箱 → 5.4 万回合 / ~96 万帧 / 生成 20~30 分钟 / **480 条**配比行(比 10 档只多 20%)。 +- **单注上限 ¥10,000 仍在平台既有范围内**:玫瑰女郎 `buy_fs` 最高一注就是 `300 × 50 = ¥15,000`,所以这不是新量级的敞口。 +- 备选(想零成本扩大):**保持 10 档,把 betMul 放到 1~20** → 实际覆盖 1~6000,池子一行不动。 + +**⚠️ 做大之前必须先定的两件事** + +1. **单局最大赔付口径(风控)** —— **已定案:封顶 500x**: + +| 组合 | 单局最大赔付 | +|---|---| +| 玫瑰女郎现状(1000x × bet 300) | ¥300,000 | +| **麻将胡了(500x × bet 10000)** | **¥5,000,000** | +| ~~麻将胡了(1000x × bet 10000)~~ | ~~¥10,000,000~~(已否) | + + 运行期还有奖池水位 `payoutCap` 兜底(水不够就出不了大奖),但 500x 这条线是玩家预期与风控口径,**代码里写死为 `MAX_WIN_CAP_MUL = 500`**。 +2. **冷门高档位会水位饥饿**:¥500 / ¥1000 这两档玩家天然少,水位长期接近 0 → `payoutCap` 把它们压到出不了奖。**上线时必须给这两档单独注一笔初始水位**,或干脆先只放 10 档、等有需求再开高档(**加档不需要改客户端,只需加箱 + 生成池子**)。 + +**为什么不是更多档** + +1. **每多一档 = 多一整套成本**(这才是决定档位数量的真正约束): + - 多一个**回合池**:约 4500 回合 / 8 万帧,池子必须按箱独立生成; + - 多 40 条**配比行**(10 桶 × 4 身份)要配、要重算、要维护模板; + - 多一套**奖池水位**(4 个身份各一条)要运营盯。 + 10 档 → 4.5 万回合 / 80 万帧 / 生成 15~25 分钟 / 400 条配比行,正好是玫瑰女郎已经在跑的量级。 +2. **档位越多,冷门档越容易"饿死"**:奖池水位是**按箱独立**的,没人玩的档水位长期为 0 → `payoutCap = max(0, 水位+成本) × rtp` 被压到接近 0 → **那个档几乎出不了奖**,只能靠运营注水救。所以档位不是越多越好。 +3. **玩家侧足够**:1~1000 的 12 档 + 1~10 倍 = 实际覆盖 1~10000,比原版 0.02~50 宽两个数量级,选注习惯也够密。 +4. **对齐玫瑰女郎**:前 10 档与玫瑰女郎一字不差,玩家已熟悉,运营报表/返利阈值/消费排行榜口径统一。 + +**不推荐的做法** +- ❌ 直接抄原版的 0.02~50.00:那是社娱场(social casino)面额,本平台是金币场,玫瑰女郎最低档是 ¥1;而且 §2.8.2 会证明 **0.02 档下最小中奖会丢分**(显示成 +0.00)。 +- ❌ 为了"档位够密"铺 20~30 档:池子生成与后台维护成本线性上涨,且冷门档水位饥饿问题成倍放大。 +- ❌ 一次把最高档拉到 ¥5000 / ¥10000:单局最大赔付会到千万级,且这两档几乎没人玩、水位必饿死。 + +**两个附带决策** +- **档位表里 3 / 30 保留**(它们是整数,只是不是 10 的整数倍;玫瑰女郎就这么定的,玩家点"+"往上加时体验更顺),不改成 2 / 25 之类。 +- **投注倍数默认 1**,只作为"快速加倍/加注"的细调;若实测玩家几乎不用,可后续在需求里砍掉(**表结构与接口预留,砍掉不用改池子**)。 + +> ⚠️ **上线前必须做的一件事**:新游戏的奖池水位从 0 开始,而 `payoutCap = max(0, 水位 + 本局成本) × rtp` —— 意味着**开局头几局玩家最多只能赢 0.8 倍下注**,大奖要等水位被玩起来才出得来。所以**上线前要按箱 × 身份人工注水**(后台奖池水位页,原因必填),否则前几天的"爆奖概率"会低到不像这个游戏。玫瑰女郎也是这么运营的。 + +#### 2.8.2 下注档不是随便定的:它被**赔付表**和**饰品区间表**两头夹住 + +用户问"是不是要根据赔付表来设计下注额"——**是,而且还有一个来自饰品表的约束**。这两条是硬约束,不是审美偏好。 + +**约束一:来自赔付表 —— 最低档下注的最低中奖不能小于 ¥0.01** + +赔付公式里最小的那一笔是:最低档符号(`G`/`H`,3 连赔付值 **2**)× ways **1** × 第 1 波阶梯 **x1**: + +``` +最小中奖 = 2 × (投注总额 ÷ 20) = 投注总额 ÷ 10 +``` + +| 投注总额 | 最小中奖 | 结论 | +|---|---|---| +| ¥0.02(原版最低档) | ¥0.002 | ❌ 金额字段是 `DECIMAL(10,2)`,**显示成 +0.00**,玩家会认为"中了不给钱" | +| ¥0.10 | ¥0.01 | ⚠️ 理论下限,刚好不丢分 | +| **¥1(建议最低档)** | **¥0.10** | ✅ 留 10 倍余量 | + +→ **`box.bet ≥ 0.10` 是硬下限**;这是"不能照抄原版 0.02"的**可证明理由**(不是我拍脑袋说社娱场面额)。 + +**附带的一个好性质**:档位全部取整数时,`unit = bet ÷ 20` 必是 **0.05 的整数倍**(1→0.05、3→0.15、5→0.25、20→1.00、300→15.00),所以**任何"赔付值 × ways"都是整分**,不会出现 0.0333 这种除不尽的数。`betMul` 取整也一样(3×3=9 → 0.45)。建议档位表**只放整数**,把这条性质保住。 + +**约束二:来自饰品区间表 —— 最低档饰品的价格决定"每局保底返奖"** + +"必得饰品"意味着**空转局也会发一个最低档饰品**(`win = max(totalWin, 0.01)` 兜底),而这个饰品的价格就是那一局的返奖。空转率按基础旋 30%~40% 估: + +``` +每局保底返奖 ≈ 空转率 × 最低档饰品价格 +低档位下注的 RTP 下限 ≈ 空转率 × 最低档饰品价格 ÷ 投注总额 +``` + +| 最低档饰品价 | 空转率 | 投注 ¥1 时的保底 RTP | +|---|---|---| +| ¥0.01 | 40% | 0.4% ✅ | +| ¥0.05 | 40% | 2% ✅ | +| ¥0.50 | 40% | **20%** ⚠️ 光保底就吃掉 1/4 的目标 RTP | + +→ **最低档饰品的返奖区间下限要压在 ¥0.01~0.05 量级**;饰品区间表(`prize_range`)在导入/配置时要和最低下注档一起检查,别直接搬一套为"高档位"设计的区间表过来。 +→ 对账口径也要注意:水位里的"派彩"应当用 **`record.settled_win`(饰品价)**,而不是 `total_win`(游戏内赢额)——否则这两者对不上时,水位会漂。 + +**结论**:下注档表要同时满足 +1. `bet ≥ 0.10`(赔付表约束,实际取 ¥1 起) +2. 档位取整数(保证 unit 是整分) +3. 最低档饰品价 ≤ ¥0.05 量级(饰品表约束) + +模式:**只有一个「普通旋转」**(用户 2026-09-21 明确:底注、购买免费旋转都不做)。 + +| boxType | 名称 | costMultiplier | 语义 | +|---|---|---|---| +| `normal` | 普通旋转 | **1x** | 唯一的玩法:自然触发免费旋转(3+ 胡) | + +> 砍掉 `ante` / `buy_fs` 带来的连锁简化(都要落地,别留半套): +> - `MahjongModeType` 只留 `normal` 一个枚举值,`costMultiplier` 恒为 1,`price = box.bet × betMul`; +> - **没有 `need_fs` 过滤**了(那是 `buy_fs` 专用),挑回合只按桶; +> - 箱数从 30 降到 **10**(只按投注档分箱)→ 池子规模、生成时间、后台配置量全部降到 1/3; +> - 高倍桶(100x 以上)**只能靠自然触发 FS 的回合供给**,没有"买 FS"那批高倍回合兜底 → 生成端要用 bias 主动**构造**高倍局,不能靠碰运气(见 §5.4 风险)。 + +--- + +## 3. RTP 与桶设计(数学核心) + +### 3.1 倍率桶(10 档) + +替代玫瑰女郎的"饰品桶"。约定:区间连续无缝、`expect_mul = (low+high)/2`、顶桶受封顶影响"匹配区间 ≠ 发放上限"。 + +| id | 名称 | range_low | range_high | expect_mul | 说明 | +|---|---|---|---|---|---| +| 1 | 空 | 0 | 0 | 0.00 | 完全未中 | +| 2 | 微 | 0.01 | 0.50 | 0.25 | | +| 3 | 小 | 0.50 | 2.00 | 1.25 | | +| 4 | 中 | 2.00 | 5.00 | 3.50 | | +| 5 | 中大 | 5.00 | 15.00 | 10.00 | | +| 6 | 大 | 15.00 | 40.00 | 27.50 | 自然触发的 FS 多落这里 | +| 7 | 很大 | 40.00 | 100.00 | 70.00 | | +| 8 | 巨 | 100.00 | 250.00 | 175.00 | | +| 9 | 超级 | 250.00 | 400.00 | 325.00 | | +| 10 | 封顶 | 400.00 | 700.00 | **500.00** | 区间上界故意开到 1.4× 封顶(对齐玫瑰女郎"匹配区间 ≠ 发放上限"),**实际发放截断 500** | + +> 因为封顶是 **500x**(Q4),顶桶的"名义期望 500"在池内实测均值会明显低于 500(被截断的局都堆在这里),配比计算时要按池内 `AVG(actual_mul)` 走,必要时用 `PAYOUT_EXPECT_OVERRIDE`(玫瑰女郎顶桶就是这么用 835 顶 1000 的)。 + +### 3.2 RTP 恒等式 + +``` +Σ (share_i × expect_mul_i) = rtp × costMultiplier # 本游戏 costMultiplier ≡ 1 +``` + +| 模式 | costMultiplier | 目标 RTP | 需要的期望倍率 | +|---|---|---|---| +| normal(唯一) | 1 | 0.80 | **0.80** | + +因为只有 1x 模式,**所有箱、所有身份的目标期望倍率都是同一个数**(只由 rtp 决定),收敛阶段 `targetMul = rtp × cm = rtp`——比玫瑰女郎三模式简单得多。 + +### 3.3 配比从哪来(`calcRounds` 算法) + +两份实现必须一致:`mahjong-py/_recalc_boxes.py::calc_rounds` 与 `MahjongDistributionService::calcRounds`。 + +1. 基础权重 `30 × 0.6^k`(k = 桶序号,低倍桶权重高); +2. 二分搜索 `power` 使加权均值命中 `targetRtp × cm`(误差 <1%); +3. 按权重把固定池分配到各桶,并施加单调约束 `count[i] ≥ count[i+1]`(低倍桶条目多于高倍桶); +4. 400 次"任意两桶搬 1 个"微调,逼到 `|delta| < 0.01`; +5. **期望值取该箱池内实测 `AVG(actual_mul)`**(样本 ≥1),无样本才回退名义 `expect_mul`/封顶修正值(玫瑰女郎顶桶用 835 而非 1000 就是这个原因)。 + +### 3.4 一轮(cycle)与收敛比例 + +``` +fixedSum = Σ count # 固定池 +N = round(fixedSum / (1 - cr)) # 一轮总抽数(写进 box.total_rounds) +fixedRounds= round(N × (1 - cr)) +收敛阶段: desiredNextMul = max(0, (N × targetMul − 已派彩) / max(1, N − drawn)) +``` + +- `cr`(`rtp_converge_ratio`)默认 0.15,线上玫瑰女郎用 0.20,夹在 `[0, 0.9]`。 +- 固定阶段消耗 `used`,收敛阶段**不消耗库存**——这是"RTP 自愈"的来源。 +- 抽满 `drawn ≥ N` → `resetCycle` + 按模板 `refillFromConfig`。 + +### 3.5 奖池水位(与玫瑰女郎同口径) + +``` +水位(box, audience) = Σ(已结算 record.cost) − Σ(已结算 record.total_win) + adjust +payoutCap = max(0, 水位 + 本局成本) × box.rtp # 白号 = null +``` + +- bet/win **以 DB 为真相**(`game_mahjong_record` 按身份聚合),Redis 只做 5 秒短缓存;`adjust` 是纯 Redis 的人工注水/抽水值。 +- 收敛/固定两阶段都用 `payoutCap` 过滤"期望赔付超上限"的桶,捞不到就退最小桶。 +- 白号(`is_blank_account=1`)`payoutCap=null`:纯概率、不受水位约束、消耗自己的 white 库存,不进真实经济。 + +### 3.6 身份与目标 RTP(**后台可配**,不是拍板项) + +各身份的 RTP **不在代码里写死**,和玫瑰女郎一样由后台配置,两层: + +1. **箱子级**:`game_mahjong_box.rtp`(目标返奖率)+ `rtp_converge_ratio`(收敛比例)—— 在后台「箱子配置」页改; +2. **身份级**:`game_mahjong_box_bucket` 里**每个身份一套桶配比**(`count`),配比变了,该身份的实际 RTP 就变了 —— 在后台「箱子配置 → 桶配比」弹窗改,或用「一键重算」按目标 RTP 反算。 + +所以"各身份 RTP 定多少"**不是一个需要在方案阶段拍板的数**,而是运营参数:先全部按 **0.80** 建库,上线后按身份在后台调(玫瑰女郎就是这么跑的)。 + +| audience | 判定 | 建库默认 RTP | 说明 | +|---|---|---|---| +| common | 其余全部 | 0.80 | | +| anchor | `user_type='01'` | 0.80 | 四身份互斥并集 = 全部 | +| robot | `user_type='03'` | 0.80 | | +| white | `is_blank_account=1`(最高优先) | 0.80 | 白号不进奖池:`payoutCap=null`、不注水不抽水,**配比即爆率**,给主播用 | + +### 3.7 免费旋转出现率与"悬念盘"(生成端必须显式约束) + +- 唯一模式 `normal`:目标**自然触发率 ≈0.5%**(每 200 局 1 次),由 3+ 胡的落点概率控制。 +- 触发局整局倍率自然落在 30x~150x 区间 → 天然归入 6~8 号桶。**生成器要保证 6~8 号桶里存在足够比例的"含 FS 回合",且这些回合在箱内占比 ≈0.5%**,否则"自然触发免费旋转"会变成空话。 +- **悬念盘(near-miss)**:用户描述的"出现 2 个胡时最后一列一直有放大效果、很刺激"——这是**为了钓出第 3 个胡的期待感**。虽然表现是客户端动画(见 §9),但**盘面得先存在**: + - 生成端要保证一定比例的**基础旋**恰好落 **2 个胡且第 5 轴无胡**(`NEARMISS_2SCATTER_RATE`,建议 **1%~2%** 的基础旋); + - 玫瑰女郎有现成的同类配置可参照(`olympus-py/olympus/config.py:181` 的 `BASE_NEARMISS_SCATTER`); + - 少了这条,客户端那段"最后一列放大"的动画**永远不会被触发**(纯自然概率太低),爽点白做。 + +--- + +## 4. 回合池数据模型(11 张表) + +前缀 `game_mahjong_`,DDL 权威文件 `mahjong-py/schema.sql`(照抄 `olympus-py/schema.sql` 骨架)。 + +| # | 表 | 对应 olympus 表 | 关键列 | 差异 | +|---|---|---|---|---| +| 1 | `game_mahjong_box` | `game_olympus_box` | box_id, box_type, bet, price, total_rounds, rtp, rtp_converge_ratio, status | `total_ornaments`→`total_rounds` | +| 2 | `game_mahjong_bucket` | `game_olympus_ornament` | id(1-10), expect_mul, name, range_low, range_high | 10 档倍率桶 | +| 3 | `game_mahjong_box_bucket` | `game_olympus_box_ornament` | box_id, bucket_id, audience, count, used | 同构,唯一键 `(box_id,bucket_id,audience)` | +| 4 | `game_mahjong_round` | `game_olympus_round` | round_id, box_id, bucket_id, actual_mul, free_spin_count, total_rotations, has_fs, near_miss | 去 `total_cascades` 语义改为 `total_rotations`(波数);`need_fs` 不需要了(没有买 FS 模式);加 `near_miss` 标记 2 胡悬念盘 | +| 5 | `game_mahjong_step` | `game_olympus_step` | 见 §4.1 | 字段大改(ways/golden/阶梯) | +| 6 | `game_mahjong_record` | `game_olympus_record` | user_id, box_id, round_id, cost, bet_mul, total_win, settled, settle_source, audience, **prize_id / prize_name / prize_image_url / prize_level_img / settled_win / reward_type** | **保留全部 prize_* 列**(发饰品要走区间匹配),只多一个 `bet_mul` | +| 7 | `game_mahjong_user` | `game_olympus_user` | user_id, total_spins, total_cost, total_win, max_win_mul | 去 `total_free_spins` 或保留 | +| 8 | `game_mahjong_box_template` | `game_olympus_box_template` | box_id, audience, name, total_rounds, items_json, is_active | 同构 | +| 9 | `game_mahjong_reward_log` | `game_olympus_reward_log` | box_id, audience, type, amount, balance_before/after, pool_balance, operator, remark | 同构(原因必填由前端保证) | +| 10 | **`game_mahjong_prize_ornament`** | `game_olympus_prize_ornament` | id, game_type, name, image_url, grade, exterior_name, sort, status, is_fallback | **必须有**:赢额 → 饰品 | +| 11 | **`game_mahjong_prize_range`** | `game_olympus_prize_range` | prize_id, win_low, win_high | **必须有**:赢额落区间 | + +**可选**:`game_mahjong_win`(日报表)。若后台"每日统计"要独立日表则加回,否则按 `record` 聚合(Q8)。 + +### 4.1 `game_mahjong_step` 字段(每帧一行) + +```jsonc +{ + "id": 1, "round_id": 1001, + "reel_spin_id": 0, // 0=基础旋/触发盘, >0=第N次免费旋转 + "rotation_seq": 1, // 本旋内第几波(= 乘数阶梯序号) + "grid_state": [["A","W","S","D","H"], ...], // 4 行 × 5 列 = 20 格短码 + "golden_detail": [{"pos":7,"symbol":"B"}], // 本帧盘面上仍是"金色普通符号"的格(1 基格位) + "wild_converted": [{"pos":7,"fromSymbol":"B"}],// 本波"金色→百搭"就地转化的格(客户端播变形动画) + "ways_detail": [{"symbol":"A","reelCount":3,"counts":[1,2,1,0,0], + "loc":[1,6,11,7], // 本笔中奖的全部格位(1起),含金色格 —— 客户端按它播爆裂动画 + "ways":2,"payBase":15,"rotationMul":2,"win":3.00}], + // win = 15 × 2 × (bet÷K) × 2,K 见 §2.1 计量单位 + "drop_data": [{"from":12,"to":17,"symbol":"H"}], + "fill_data": [{"pos":2,"symbol":"G"}], + "scatter_trigger": {"scatterCount":3,"freeSpins":12,"extraSpins":0}, // 或 null + "retrigger_info": null, + "rotation_mul": 2, // 本波乘数(阶梯档) + "win_amount": 120.00, // 本旋累计到本步(末帧 = 本旋总额),不是增量 + "spin_win": 340.00, // 本局累计到本步 + "win_capped": 0, + "bigwin_level": "small", // 本旋末帧才有 + "is_final": 1 +} +``` + +**只落库不下发**:`pay_base`、`counts`(留档/对账用)。接口下发字段见 §7。 + +--- + +## 5. Python 生成器(`mahjong-py/`) + +### 5.1 目录结构(对拷 `olympus-py`) + +``` +mahjong-py/ +├── schema.sql # 11 张表 DDL + 10 档桶种子(改前缀与种子) +├── mahjong_db.py # 建表 + 批量写(executemany,死锁重试 5 次) +├── mahjong/ +│ ├── config.py # ★ 唯一事实来源:符号/赔付表/桶/模式/身份/RTP/FS 参数 +│ ├── models.py # frozen dataclass(Bomb → RotationMul) +│ └── engine.py # 结算状态机骨架 +├── mahjong_simulator.py # ★ 核心:单回合模拟 + bias 反解 + FS 保底配额 +├── generate_rounds.py # 批量落库(箱编号/池子分配/id 段切分/并行) +├── simulate_cycle.py # 整轮 RTP 仿真验收(几乎原样拷贝) +├── _recalc_boxes.py # 按目标 RTP 重算配比(calc_rounds 一行不改) +├── _make_templates.py # 12 箱 × 4 身份 = 48 条生效模板 +├── _regen.py # check/snapshot/clear/restore/verify +├── _reset_boxes.py +├── README.md # 结构与 runbook(照抄并改写游戏规则段) +└── tests/ + ├── test_engine.py # ways 判定 / 阶梯 / 金色转化 / 封顶 + └── test_simulator.py # 级联几何复刻 / FS 触发 / 池内 RTP +``` + +### 5.2 必须重写的四处引擎逻辑 + +1. **`_winners` → 1024 路前缀链判定**(`_MIN_WIN_COUNT=8` 语义作废) + ```python + def ways_win(grid): # grid: 4行 × 5列 + for sym in PAYING: + run, ways = 0, 1 + for c in range(5): # 第1轴 → 第5轴,断了就停 + cnt = count_sym_or_wild(grid, c, sym) + if cnt == 0: break + run += 1; ways *= cnt + if run < 3: continue # 1-2-4 / 2-3-4 / 第1轴为空 一律不成奖 + yield Win(sym, reelCount=run, ways=ways, pay=PAYTABLE[sym][run] * ways) + ``` + **必须写成前缀链(`break` 就停),不是"5 轴里有 3 轴有就算 3 连"** —— 后者会把用户举的 `1,0,3,4,4` 判成中奖,方向性错误,且会系统性抬高 RTP。 + 注意:`连轴数 = run`(3/4/5),档位从第 1 轴起算;`ways` **只乘链内的轴**,链外轴的个数不参与。 + **不需要**再写"纯百搭链不成奖"的特判 —— **百搭只可能出现在第 2/3/4 轴**(金色只在那三轴),第 1 轴必然是自然符号,所以纯百搭链不可能出现(§2.2)。生成端与校验端加一条断言:**`W` 只出现在第 2/3/4 轴**。 +2. **几何**:`GRID_ROWS=4 / GRID_COLS=5 / GRID_SIZE=20`;`_force_win` 的 `random.sample(free, cnt)`、`_force_small_win` 的可用格数、`_place_scatter` 的保护集都要跟着改。 +3. **乘数阶梯替换炸弹**: + - 删掉 `BOMB_CELL_PROBABILITY` / `BOMB_VALUES` / `BOMB_QUALITIES` / `FS_SCREEN_BOMB_PROB` 一整组; + - 新增 `LADDER_BASE = (1,2,3,5)` / `LADDER_FS = (2,4,6,10)`,第 n 波乘数 = `ladder[min(n,4)]`; + - `_force_win` 的目标反解改为"基础组合 × 本波阶梯乘数",即给定目标倍率 T 时,需要的基础分 = `T / ladder[n]`; + - **累计器机制不删**:`accumulator_before/after` 改成 `rotation_mul`,语义从"跨旋累加"变为"本波档位"。 + +4. **级联的"消除"不再是"全部清空"**(金色符号就地转百搭,替换 olympus 的纯消除): + ```python + def settle_rotation(grid, golden): # golden: {pos: natural_symbol} + wins = prefix_chain_wins(grid) # 判定时金色格按 natural_symbol 计数 + if not wins: return None + converted = [] + for pos in win_cells(wins): + if pos in golden: + grid[pos] = WILD # ★ 就地改写,不清空、不放新符号 + converted.append({"pos": pos, "fromSymbol": golden.pop(pos)}) + for pos in win_cells(wins) - {c["pos"] for c in converted}: + grid[pos] = None # 其余中奖格照常清空 + drop_and_fill(grid) # W 作为幸存符号一起下落(§2.3 Q2) + return converted + ``` + - 这一处**必须写对**:olympus 是"中奖格全清 + 补新符号",本游戏是"金色中奖格变成 W 留在盘上",会直接改变级联的密度与 RTP,也是本游戏最独特的爽点来源。 + - `_collapse` 仍可复用(列式下落方向不变),只是入参从"清空后的盘面"变成"金色格已改写为 W 的盘面"。 + - 落库/下发都要带 `wild_converted`(§4.1 / §7.1)。 + +其余可原样拷贝:`_collapse`(5 轴列式下落,方向不变)、`_fill`(去掉炸弹参数)、`fill_distribution`(阶段1 自由生成 + 阶段2 按缺口补桶 + `_calibrate_bias` 饥饿重校准)、FS 保底配额机制(`FS_SPIN_WIN_PROB` / `FS_CASCADE_CONTINUE_PROB` / `FS_GUARANTEE_COMBOS` 换成本游戏的"保底最少中奖形状")。 + +### 5.3 生成流水线(顺序敏感,照抄) + +```bash +cd mahjong-py +python mahjong_db.py # 建 11 张表 + 10 档桶种子(幂等) +python generate_rounds.py --per-bucket 20 # 干跑,不写库 +python generate_rounds.py --all --alloc config --write --workers 4 # 12 箱写库(只有 normal) +python _make_templates.py # 12 箱 × 4 身份 = 48 条生效模板 +python _recalc_boxes.py --mode normal # 重算配比 + 回写 total_rounds +python simulate_cycle.py --box 1 --cycles 5 --audience common --seed 2026 # RTP 验收 +python _regen.py check|snapshot|clear|restore|verify # 体检/快照/清池 +python -m pytest tests -q # 回归 +``` + +**顺序不可颠倒**:重建池子后必须 `_recalc_boxes` → 清 Redis `ruoyi:mahjong:cycle:*` → `_make_templates`,否则 Java 端拿的是过期配比与旧账本(这是玫瑰女郎踩过的坑)。 + +### 5.4 参数与工作量预估 + +| 参数 | 值 | 依据 | +|---|---|---| +| `ALLOC_K` | 1.5 | 池子 = 配比 × 1.5,留抽样多样性 | +| `ALLOC_FLOOR` | 60 | 每桶保底条数 | +| `BOX_STEP_BAND` | 200000 | 每箱 step id 预留段(只有 normal,比 olympus 的三模式好估) | +| 每箱池子 | 4000~5000 条回合 | 因为没有"买 FS"那批高倍回合兜底,**高倍桶要靠 normal 池自己凑**,所以单箱池子要比玫瑰女郎更大 | +| 总规模 | **12 箱** × ~4500 = **5.4 万回合**,每回合 8~40 帧 → **~96 万 step**(与 olympus 的 106 万同一量级) | +| 生成耗时 | 4 进程约 **15~25 分钟**(本机 i3-10105F 只 4 物理核,**workers 必须 ≤4**,再高反而更慢) | +| 风险点 | **高倍桶(8/9/10 号桶)没有买 FS 兜底**:1000x 的局只能靠"基础旋自然触发 FS + 长级联 + 阶梯叠满"构造出来,靠近乎碰运气是凑不齐的 → 必须用 `_force_win` 主动构造,并接受"构造局的高倍桶可能长时间饿死"(玫瑰女郎已有 `_calibrate_bias` 饥饿重校准可复用) | + +> 若 80 万 step 的写入时间不可接受,回落方案:把"未中奖的回合"(桶 1、桶 2)合并成少数几条模板回合,靠 Java 端 `copyFromSameBucket` 复制(olympus 已有此兜底,`:179-231`)。 + +--- + +## 6. Java 服务(`skins-service/game-mahjong-service`) + +### 6.1 模块接线(3 处,易漏) + +1. `skins-service/game-mahjong-service/pom.xml`(拷 olympus,改 artifactId,parent = `skins-service`); +2. `skins-service/pom.xml` 的 `` 加一行; +3. `ruoyi-admin/pom.xml` 加依赖。 + > 无需 `@ComponentScan`/`@MapperScan`:包路径 `com.ruoyi.*` + `mapperLocations: classpath*:mapper/**/*Mapper.xml` 自动生效(`ruoyi-admin/src/main/resources/application.yml:70-72`)。 + +### 6.2 包与类清单(对照 olympus 35 个类) + +``` +com.ruoyi.game.mahjong +├── controller/MahjongClientController # /api/mahjong +├── controller/MahjongAdminController # /admin/mahjong +├── service/MahjongGameService # ★ dospin 主链路(开局事务/结算/续播/重连) +├── service/MahjongRoundPicker # ★ 两阶段挑桶 +├── service/MahjongDistributionService # 配比计算/补池/RTP 仿真 +├── repository/MahjongCacheRepository # ruoyi:mahjong:{userId} TTL 30min +├── repository/MahjongCycleRepository # ruoyi:mahjong:cycle:{boxId}:{audience} +├── repository/MahjongRewardPoolCacheRepository # ruoyi:mahjong:reward:pool +├── model/PickedRound +├── model/cache/MahjongGameCache(内嵌 CachedStep)/ MahjongRewardPoolCache +├── domain/(11 张表实体) +├── mapper/(9 个接口)+ resources/mapper/mahjong/*.xml +├── enums/MahjongModeType +└── contract/request/{SpinRequest,RecordPageRequest} + response/{SpinResponse,BoxResponse,RecordItemResponse} +``` + +`MahjongAudience` **建议直接复用 `OlympusAudience` 并上移到共享模块**(`skins-model`),避免第 4 份身份判定逻辑漂移(`TtRechargeRecordController` 里已有一份按 audience 聚合的 SQL)。 + +### 6.3 开局事务(与玫瑰女郎的唯一实质差异) + +``` +startNewGame(userId, boxId, betMul = 1..10): + 校验箱子/维护/充值(703)/余额(702) + audience = resolve(player); isBlank = audience.isWhite() + bet = box.bet × betMul // 没有模式倍率(只有 normal) + cost = bet // 实际扣费 = 下注总额 + payoutCap = isBlank ? null : scale2(max(0, 水位(box,aud) + cost) × box.rtp) + picked = roundPicker.pick(boxId, audience, bet, payoutCap) + totalMul = picked.round.actualMul ; totalWin = totalMul × bet + ── 事务 ──────────────────────────────────────────────── + updateUserAccount(userId, -cost, MAHJONG_SPIN(68)) + insert game_mahjong_record(settled=0, settle_source=null, bet_mul, audience) + if (!picked.converge) boxBucketMapper.incrUsed(boxId, bucketId, audience) + settleAtStart(): // 幂等守卫 + 发饰品(与玫瑰女郎逐行同构) + win = scale2(max(totalWin, 0.01)) // ★ 空转局也要给最低档饰品,兜底 0.01 照抄 + prize = findPrizeWithFallback(win) // 精确区间 → is_fallback=1 → 最低区间 → 最高区间 + guard = UPDATE record SET settled=1, total_win=?, settled_win=win, reward_type='ornament', + prize_id/name/image_url/level_img=?, settle_source=(单步?finish:start) + WHERE id=? AND settled=0 + if guard == 0 → 已结算,清缓存返回 + insert tt_box_records(source = MAHJONG_BOX(15), status=0, is_show=1, box_name='MAHJONG') // 写背包 + → 回填 cache.boxRecordId + upsert game_mahjong_user 累计 + ── 提交后(全部 try/catch 吞异常,绝不外抛 500)──────── + cycleRepository.recordDraw(...) + if drawn >= cycleN → resetCycle + refillFromConfig + firstIsFinal ? clearCache : setCache(30min) + // 单步局开局即播完 → 此刻发站内信;多步局等末帧(continueGame) +``` + +**硬约束(照抄玫瑰女郎)**: +- **先扣费、后结算、同一事务**;提交后的 Redis/账本/补池失败必须吞掉,否则玩家看到 500 会重开一局、再扣一次钱。 +- **开局就写背包**:麻将胡了允许空转(没中奖),但**空转局照样发最低档饰品**(`win = max(totalWin, 0.01)` + `findPrizeWithFallback` 的最低区间兜底)——这就是玫瑰女郎的"必得饰品",也是"无损模型"(中途退出/断线饰品已在背包)成立的前提。 +- **奖励流水 `MAHJONG_REWARD(69)` 不在开局写**:和玫瑰女郎一样,只在玩家**分解饰品**时才由通用背包接口写入(`tt_box_records.source = MAHJONG_BOX(15) → MAHJONG_REWARD(69)`)。 +- `settle_source` 语义不变:开局 `start`(动画还在播,战绩列表不显示)→ 末帧 `finish`。 + +### 6.4 续播 / 重连 / 战绩可见性(照抄) + +- `dospin` 带 `boxId` = 开新局;不带 = 下一步;越界 = "游戏已结束"并清缓存。 +- `COMMON_LOCK + userId`(复用平台通用用户锁,与其它游戏互斥),失败 → "请求频繁,请稍后再试"。 +- 末帧:`settle_source=finish` + 返回 FINISH(`totalWin`/`totalMul`/`maxWinReached`/`prizeInfo`/`boxRecordId`)+ 清缓存;**站内信在末帧才发**(`sendWinNotice`,防剧透)。 +- 末帧客户端给玩家两个选择「**放入背包 / 立即分解**」,分解走通用接口 `POST /api/userPackSack/decompose {packSackIds:[boxRecordId], isAll:false, decomposeType:"game"}`;"自动分解"是前端自己的开关,后端不参与、不回传。 +- `hasActive`:返回 `stepIndex/totalSteps/spinsRemaining/rotationMul/lastStep`,客户端用 `lastStep.rotationMul` 接回当前波次(否则重连后阶梯会从第一档重算)。 +- 维护开关:**只拦开新局**(带 boxId 才拦),进行中的局永远放行播完。 +- **战绩列表(records)**:`settled=1 AND (settle_source != 'start' OR create_time < cutoff)`,`cutoff = now − 30min`。**cutoff 必须在 Java 侧算,不能用 SQL `NOW()`** —— DB 会话时区是 UTC,而 `create_time` 存的是本地时间,用 `NOW()` 会差 8 小时直接失效(玫瑰女郎踩过的坑)。 +- `scope=0` 全服口径排除机器人(`user_type='03'`)、封禁(`status='1'`)、已删除(`del_flag!='0'`),**白号保留上榜**。 +- 响应体用 `@JsonInclude(NON_NULL)`:未出现的 key 就是 null,客户端无需判空。 + +### 6.5 平台共享文件的改动(9 处,漏一处就少一块统计) + +| # | 文件 | 改动 | +|---|---|---| +| 1 | `skins-model/.../TtAccountRecordSource.java` | 新增 `MAHJONG_SPIN(68, OUTPUT, "麻将胡了消费")` / `MAHJONG_REWARD(69, INPUT, "麻将胡了奖励")`;并加进 `getWelfarePrizeCodes():193`、`getGameConsumeCodes():203`、`getGameBetCodes():212`、`getGameRewardCodes():224`、`isNewGameConsumeForCommission():250`(新游戏返佣口径)、`getTurnoverLotteryCodes():271` —— **共 6 处聚合** | +| 2 | **`skins-model/.../TtboxRecordSource.java`** | **新增 `MAHJONG_BOX(15, "麻将")`**(现有 13=玫瑰女郎、14=流水抽奖)—— 背包来源,漏了就写不进背包 | +| 3 | **`skins-service/service-user/.../ApiUserPackSackServiceImpl.java:612-645`** | `mapDecomposeSource` 加 `MAHJONG_BOX(15) → MAHJONG_REWARD(69)`;**漏了就会走 `DECOMPOSE_ORNAMENT(10)` 默认口径,报表全错** | +| 4 | `skins-service/service-admin/.../TtRechargeRecordController.java:155-163,253-281` | 报表 sources 列表 + `appendPoint` 单列该游戏(可选) | +| 5 | `skins-service/service-admin/.../config/RedisConstants.java` | 加 3 个键:`ruoyi:mahjong:` / `ruoyi:mahjong:cycle:` / `ruoyi:mahjong:reward:pool` | +| 6 | `skins-model/.../MessageCategoryEnum.java` | 加 `GAME_MAHJONG("GAME_MAHJONG","麻将胡了")`(前缀 `GAME_` 才进"游戏"Tab)—— **本游戏要发结算站内信,必加** | +| 7 | `sys_config` | `mahjongMaintenance` 维护开关(0 正常 1 维护) | +| 8 | `sys_menu` | 7 个后台菜单(见 §8) | +| 9 | `docs/updata_sql.md` | 新增一节「麻将胡了模块建表 / 服务接入」,格式照 1144-1225 行;**返奖饰品种子**照 1187-1200 行从糖果整套导入(`DELETE` + `INSERT ... SELECT`,保留原 id 以维持区间映射) | +| 10 | 亏损返利 / 消费排行榜 / VIP 流水返利 | 这些模块按 source 码聚合,登记进第 1 项后自动生效;**必须实测一次**别只看代码 | + +> **总览:10 处改动里,第 1/2/3/6 项是"漏了必出事故"的**——漏 1 少流水统计、漏 2 写不进背包、漏 3 分解口径错、漏 6 站内信发不出去。 + +### 6.6 封顶与"匹配/发放分离" + +- 回合池里桶 10 的 `actual_mul` 一律 **= 1000**(发放值),但配比计算时期望用 **835**(实测池内均值,可配 `PAYOUT_EXPECT_OVERRIDE`),否则 RTP 会系统性偏低。 +- 运行期 `payoutCap` 过滤 + 收敛阶段 `capMul = payoutCap / bet` 双保险。 +- FINISH 时 `totalMul ≥ 1000` → `maxWinReached=true`、`spinsRemaining=0`。 + +--- + +## 7. 接口协议(草案,最终产物是 `docs/麻将胡了接口文档.md`) + +Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 + +| # | 接口 | 用途 | +|---|---|---| +| 1 | `POST /boxes` | 箱子列表(含 `bet`、`price`;**没有 `modeName`**,只有一个普通旋转模式) | +| 2 | `POST /dospin` | 开局(`{boxId, betMul}`)/ 续播(空 body `{}`) | +| 3 | `POST /records` | `scope=1` 我的 / `scope=0` 全服,分页,只含已结束局 | +| 4 | `POST /hasActive` | 断线重连恢复 | +| 5 | `POST /api/userPackSack/decompose` | **复用玫瑰女郎那个通用背包接口**,末帧「立即分解」(含前端自动分解) | + +> `dospin` 开局请求:`{"boxId": 1001, "betMul": 1}`;`betMul` 缺省按 1 处理,范围 1~10,服务端夹取。 + +### 7.1 帧字段(`SpinResponse`) + +| 字段 | 出现 | 说明 | +|---|---|---| +| `state` | 每帧 | `PLAYING` / `FINISH` | +| `reelSpinNum` | 每帧 | `0`=基础旋,`>0`=第 N 次免费旋转 | +| `rotationNum` | 每帧 | 本旋内第几波(级联序号) | +| `globalStep` / `globalTotalSteps` | 每帧 | 整局第几步 / 总步数 | +| `gridState` | 每帧 | **4 行 × 5 列**短码矩阵,20 格全满(权威渲染数据) | +| `goldenDetail` | 每帧 | `[{"pos":1起,"symbol":"B"}]` 本帧仍为"金色普通符号"的格(它按 `symbol` 的自然符号成奖) | +| `wildConverted` | 每帧 | `[{"pos":1起,"fromSymbol":"B"}]` 本波**就地转成百搭**的格:该格 `gridState` 已是 `W`、不清空不补新符,客户端播"金色→百搭"变形动画 | +| `waysDetail` | 每帧 | `[{"symbol","reelCount","counts":[..5],"loc":[格位…],"ways","payBase","rotationMul","win"}]`:`loc` = **本笔中奖的全部格位(1 起,含金色格)**,客户端按它播爆裂动画;`win` = `payBase × ways × (bet÷K) × rotationMul`(**K 见 §2.1 计量单位**) | +| `dropData` / `fillData` | 每帧 | 与玫瑰女郎同构(`from/to/symbol`、`pos/symbol`) | +| `rotationMul` | 每帧 | 本波乘数(阶梯档位),重连时用它接回档位 | +| `winAmount` | 每帧 | **本旋累计到本帧**(末帧 = 本旋总额),不是增量 | +| `spinWin` | 每帧 | 本局累计到本帧;末帧 = 整局实付 | +| `scatterTrigger` | 有则 | `{"scatterCount":3,"freeSpins":12,"extraSpins":0}` | +| `retriggerInfo` | 有则 | 免费旋转内重触发 | +| `spinsRemaining` | 每帧 | 剩余免费旋转次数(服务端算好,前端直接倒计) | +| `bigWinLevel` | FS 旋末帧 | `small` / `middle` / `big` | +| `maxWinReached` | FINISH | 是否触顶 | +| `totalWin` / `totalMul` | FINISH | 总赢额(**显示用现金数字**)/ 总倍率 | +| `prizeInfo` | FINISH | `{id,name,imageUrl,grade,exteriorName,isFallback}` —— **本局命中的饰品**(赢额落区间匹配所得) | +| `boxRecordId` | FINISH | 背包记录 ID:末帧「放入背包 / 立即分解」都靠它(分解时作为 `packSackIds` 传入) | +| `recordId` | FINISH | 本局记录 ID(战绩用) | + +**位置约定**:`pos = 行序号(0 起) × 5 + 列序号(0 起) + 1`,1~20。 +**短码**:`A~H` 付费符号 / `W` 百搭 / `S` 胡。 + +> 前端只需保证:**首次用 `{boxId, betMul}` 开局,之后不断调 `dospin({})` 逐步揭晓,直到 `state === "FINISH"`**,然后在末帧用 `boxRecordId` 让玩家选「放入背包 / 立即分解」。 + +### 7.2 错误码(照抄) + +702 余额不足 / 703 未充值 / 500 维护中、请求频繁、箱子不可用、该箱子暂无可用回合、游戏已结束、boxId 不能为空。**本接口不返回 701。** + +--- + +## 8. 后台管理(7 页) + +| 菜单 | 路径 | component | perms | +|---|---|---|---| +| 麻将胡了(目录) | `mahjong` | — | | +| 箱子配置 | `box` | `game/mahjong/box/index` | `game:mahjong:box` | +| 倍率桶 | `bucket` | `game/mahjong/bucket/index` | `game:mahjong:bucket` | +| 回合管理 | `round` | `game/mahjong/round/index` | `game:mahjong:round` | +| 奖池水位 | `rewardPool` | `game/mahjong/rewardPool/index` | `game:mahjong:pool` | +| 玩家记录 | `record` | `game/mahjong/record/index` | `game:mahjong:record` | +| 每日统计 | `dailywin` | `game/mahjong/dailywin/index` | `game:mahjong:win` | +| **奖品管理** | `prize` | `game/mahjong/prize/index` | `game:mahjong:prize` | **本游戏要有**(赢额→饰品、返奖区间编辑、品质图) | + +菜单 SQL 照抄 `docs/updata_sql.md:1165-1186` 的写法(`SET @game_root := (SELECT parent_id FROM sys_menu WHERE component='game/sweet/box/index' LIMIT 1)` 动态挂到"游戏管理"下,再 `LAST_INSERT_ID()` 取目录 id 插 7 个子菜单)。**注意两个仓库现状**: +- `sql/` 目录是空的,全量 DDL 历史只在 `docs/updata_sql.md`(追加式)和生成器的 `schema.sql` 里,别去 `sql/` 找; +- 后台页面**没有任何 `v-hasPermi` 指令**,路由也**不在 `router/index.js` 里静态注册**——能否访问完全由 `sys_menu` 行决定(`component='game/mahjong//index'`)。菜单 SQL 漏一条,页面就是白屏而不是报错。 + +**箱子配置页**:箱列表(bet/目标 RTP/固定池/收敛比例)+ 桶配比弹窗(改 `count`、看 `remaining`/`convergeCount`/计算 RTP)+ 模板管理 + 一键重算/批量重算。 +**奖池水位页**:四身份 Tab + 说明面板 + 逐箱水位与实时 RTP + 批量注水/抽水/重置(比例 + **调整原因必填**,空或纯空格不提交——这是用户已裁决的口径)+ 调整历史分页。 +**奖品管理页**:返奖饰品列表(名称/品质/图片/品质图/磨损/兜底/返奖区间)+ 编辑(`prize/save` 同步写 `game_mahjong_prize_range`)——照抄 `olympus/prize/index.vue`(121 行)。 +**玩家记录页**:列里带**饰品名+图**与「播放状态 start/finish」标签(和玫瑰女郎一致)。 +**必做校验**:`calcRounds` 回写时同时更新 `total_rounds`,否则 `cycleN` 与实际池子对不上。 + +--- + +## 9. 客户端(Cocos,`D:\changzhou-cocos\changzhou_cocos\changzhou_cocos`) + +| 文件 | 作用 | 与玫瑰女郎面板的关系 | +|---|---|---| +| `assets/Scripts/Pages/MahjongPanel/MahjongPanel.ts` | 主面板(5×4 盘面、级联动画、阶梯条、FS 模式、自动旋转) | 结构照 `CandySloterPanel.ts`(186KB),**盘面改为 4 行 5 列、去掉炸弹、加金色转化动画** | +| `MahjongBigWin.ts` | 爆奖三档 | 拷贝 | +| `MahjongHistory.ts` / `MahjongRules.ts` / `MahjongSettings.ts` | 战绩 / 规则 / 设置(含投注大小与倍数) | 拷贝改文案 | +| `MahjongSfx.ts` / `MahjongSpineUtil.ts` | 音效 / 骨骼工具 | 拷贝 | + +**主界面 HUD 布局(按用户给的真实运行截图逐块对齐)**: + +| 位置 | 元素 | 数据来源 | +|---|---|---| +| 顶部横条 | 「1024 路中奖组合」标题条(两侧金环装饰) | 静态 | +| 标题条下方 | **乘数阶梯行** `x1 x2 x3 x5`(FS 内 `x2 x4 x6 x10`):**当前档金色高亮+放大,未到的档压暗** | `rotationMul` | +| 中部 | **5 列 × 4 行**牌面;**金色符号 = 普通牌 + 金色底/描边**;**本波中奖牌 = 红色边框** | `gridState` / `goldenDetail` / `waysDetail` | +| 牌面下方 | 「共赢得 X.XX」金边横幅(本局累计,跳字) | `spinWin` | +| 横幅下方三格 | 余额 ¥ / 投注 ¥ / 赢额 ¥ | 余额接口 / `bet×betMul` / 本局赢额 | +| 底部按钮 | 闪电(加速)/ − / 大 Spin / + / 自动旋转,右上角 ✕ 退出 | 静态 + 投注档联动 | + +> 截图实测细节:投注 ¥8.00、阶梯高亮在 **x2**(本旋第 2 波)、横幅「共赢得 0.80」→ 说明"**每波结算后 HUD 立刻按当前波次刷新阶梯、并按本局累计刷新横幅**",不是等整旋播完才更新。这两处要按帧驱动,别做成一旋只刷一次。 + +动画要点: +1. 每波消除后"金色格 → 百搭"要有明确视觉反馈(用 `wildConverted` 驱动,**不要靠 diff 盘面猜**); +2. 阶梯条随波次金色高亮递进(见上表),用 `rotationMul` 驱动; +3. **悬念(near-miss)表现**:基础旋逐列落地时,若前 4 列已出现**恰好 2 个胡**,第 5 列(最后一列)要播放**放大/发光/慢转**效果吊期待——这是用户点名要的"很刺激"的爽点。判据完全在客户端(数 `gridState` 里的 `S`),服务端只负责让这种盘面**真的存在**(§3.7 的 `NEARMISS_2SCATTER_RATE`)。 +4. FS 触发/重触发弹窗;剩余旋数直接显示 `spinsRemaining`,前端不要自己加减; +5. **末帧结算交互**(和玫瑰女郎一致):先播 `totalWin` 落袋动画 → 弹「**放入背包 / 立即分解**」(用 `prizeInfo` 展示饰品名与图)→ 分解走 `POST /api/userPackSack/decompose`;设置里的「自动分解」开关只是前端开关,开启后末帧自动替玩家调一次分解接口,调不动就留在背包(不会丢奖)。 + +--- + +## 10. 分期实施计划 + +| 阶段 | 交付物 | 验收标准 | 人日 | +|---|---|---|---| +| **P0 数学与仿真** | `mahjong-py` 的 `config.py` / `engine.py` / `mahjong_simulator.py` / `tests/` | ① 前缀链判定逐条单测通过(§2.1 七个正反例);② 金色就地转百搭、阶梯乘数、级联几何三项有专门单测;③ 自然 1000 万旋仿真:基础旋命中率 20~30%、**FS 触发率 ≈0.5%**、悬念盘占比 1~2%、总 RTP 落在设计区间 | 6~8 | +| **P1 回合池** | `schema.sql` / `mahjong_db.py` / `generate_rounds.py` / `_recalc_boxes.py` / `_make_templates.py` / `simulate_cycle.py` / README | ① **12 箱**池子生成成功,无饥饿桶(尤其 8/9/10 号高倍桶);② `simulate_cycle` 整轮 RTP 偏差 ±3pt 内(目标 ±1pt);③ 池体检通过(每桶有条目、封顶局存在、含 FS 回合占比 ≈0.5%) | 4~5(比三模式省 1~1.5 天) | +| **P2 Java 服务** | `game-mahjong-service` 全套 + 10 处共享文件改动 | ① `mvn -pl skins-service/game-mahjong-service -am compile` 通过;② 开局/续播/重连/封顶/空转局五条链路离线单测;③ 扣费与发奖同一事务、`settled` 幂等可重放、饰品开局即入背包、分解流水口径正确 | 6~8(含饰品/背包/站内信链路) | +| **P3 后台 + SQL 文档** | 7 个 Vue 页面(含奖品管理)+ `docs/updata_sql.md` 新节 + `docs/麻将胡了接口文档.md` | ① 菜单挂载正确、权限生效;② 一键重算与注水/抽水(原因必填)可用;③ 返奖饰品与区间可编辑;④ 接口文档与真实响应逐字段对齐 | 4~5 | +| **P4 客户端** | Mahjong 面板全套 | ① 逐帧复刻服务端盘面(含金色转化、阶梯高亮、**2 胡悬念放大**);② 断线重连回到正确波次;③ 末帧「放入背包/立即分解」+ 自动分解开关 | 15~20 | +| **P5 联调与上线** | 测试服部署 + 对账脚本 + 上线清单 | ① 真实 30 分钟对账:`game_mahjong_record.settled_win` 之和 = 背包记录数 × 对应饰品价,分解后 = `MAHJONG_REWARD(69)` 流水;② RTP 抽样 ≥1000 局落 ±5pt;③ 流水抽奖 / 消费排行 / VIP 返利三项回归通过 | 4~5 | + +**总计**:后端 20~26 人日、客户端 15~20 人日、联调 4~5 人日。P0→P1 强串行,P4 可在 P2 接口冻结后并行。 + +--- + +## 11. 测试与验收清单 + +**Python** +- `test_engine.py`: + - **前缀链判定逐条覆盖 §2.1 的 7 个正反例**(`2,3,1,0,0` ✅ / `2,3,1,2,0` ✅ 4 连 / `2,0,3,4,4` ❌ / `0,4,4,4,4` ❌ / `1,1,0,4,4` ❌ / `1,2,3,4,4` ✅ ways=96 / `1,1,1,1,1` ✅ ways=1)——**这是全游戏最容易写错的一处,必须单测钉死**; + - 百搭补链(`發,百搭,發` 成立)、**原生盘面不落百搭**(`_fill`/新旋/补位断言不产出 `W`)、**每个金色格各留一个百搭(无上限)且只出现在第 2/3/4 轴**、**百搭不能替代胡**(`發,胡,發` 不成链)、**断言 `count_wild(grid) ≤ 1`**; + - **金色符号就地转百搭**:① 金色「中」参与中奖后,该格 `gridState` 变 `W` 且 `wildConverted` 有它;② 该格**没有**被清空/没有新符号落进去;③ W 在下一波能成功搭桥(例如让 `發,發,_,...` 变成 3 连);④ 该 W 在更后面的波参与成奖时**被正常消除**;⑤ 未被中奖涉及的金色格**不转化**、保持原样; + - 赔付表 8×3 全档、**阶梯逐波取值(1次=x1 / 2次=x2 / 3次=x3 / 4次+=x5)**、封顶截断、FS 触发/重触发旋数(3→12 / 4→14 / 5→16)、**百搭不能替代胡**(`發,胡,發` 不成链)、**悬念盘**(恰好 2 胡且第 5 轴无胡的盘面占比落在设计值)。 +- `test_simulator.py`:级联几何复刻(上一波盘面 − 本波中奖格 + drop/fill = 本波盘面,且**金色格以 W 留在盘上**,玫瑰女郎有 `TestTumbleGeometry` 可照抄)、池内每桶 `actual_mul` 落在区间内、无"饥饿桶"(**重点查 8/9/10 号高倍桶**——没有买 FS 兜底,最容易饿死)。 +- `simulate_cycle.py`:整轮 RTP 与设计的偏差;白号池独立。 + +**Java** +- `RoundPicker`:固定阶段只消耗 `used`、收敛阶段不消耗;`payoutCap` 过滤生效;白号走纯概率分支。 +- 幂等:同一 `recordId` 重复 `settleAtStart` 只入账一次、**只写一条背包记录**。 +- 封顶局:`maxWinReached=true`、`spinsRemaining=0`、`totalWin = 500×bet`(**500x 封顶,Q4 定案**)。 +- **空转局(`totalWin=0`)**:仍按 `max(totalWin,0.01)` 命中**最低区间饰品**、照样写背包、`settle_source` 照常 `finish`;**开局不写任何 `MAHJONG_REWARD` 流水**。 +- **奖励流水时机**:`settleAtStart` **不写** 69;只有调 `decompose` 分解后才写 69(source 15 → 69 的映射生效),未分解就永远没有 69。 +- 站内信:单步局开局发、多步局末帧发、**中途弃局不发**(对齐玫瑰女郎)。 +- 并发:同一用户并发 `dospin` 只有一个能开局(`COMMON_LOCK+userId`)。 + +**对账 / 回归** +- `Σ record.settled_win`(已结算局) == `Σ` 对应饰品价 × 条数 == 背包里 `tt_box_records.source=15` 的条数口径。 +- 分解后:`MAHJONG_REWARD(69)` 流水之和 == 已分解饰品价之和。 +- 今日总投注/总赢奖、消费排行榜、VIP 流水返利、流水抽奖四项把新 source 计入后数值正确(改 `TtAccountRecordSource` 后必须实测,不能只看代码)。 +- 断线重连:播到第 3 旋第 2 波刷新,重连后阶梯档位与累计赢额与刷新前一致;**饰品此时已在背包**(无损模型)。 + +--- + +## 12. 风险与对策 + +| # | 风险 | 对策 | +|---|---|---| +| 1 | **1024 路天然高方差**(免费旋转 + 10x 阶梯叠加,单局理论可爆几千倍) | 桶配额 + 收敛阶段 + **500x 封顶**(把尾部直接切掉);单箱池子 ≥3000 条;上线前用 `simulate_cycle` 跑 5 轮以上看尾部 | +| 2 | **金色符号"就地转百搭"改变了级联密度**(中奖格不再全清 → 盘面更容易连续中奖 → RTP 与旋内波数都会上升) | 这是规则本身的效果,不是 bug;但必须在 P0 用仿真量化(对比"金色格照常清除"的口径),再据此调桶配比;Q2 的"锚定/不锚定"用同一套仿真验证 | +| 3 | **step 表体量**(~96 万帧,12 箱) | 无中奖桶(1/2)合并成模板回合 + Java `copyFromSameBucket` 复制;必要时分表 | +| 4 | **py / Java 两份 `calcRounds` 不同步**(玫瑰女郎已因此返工) | 定死"先改 py,再对齐 Java";P2 加一条对照单测(同输入同一配比) | +| 5 | **免费旋转"演不动"**(FS 是白送的 12 旋,预算铺不开时动画会空转) | 沿用 FS 保底配额机制(每 N 旋必补一次中奖),并把预算按"保底预算 + 一旋大奖"拆分 | +| 6 | ~~现金直付与平台既有"游戏必给饰品"体验差异~~ | **已定案:走饰品**(用户 2026-09-21)。代价是 P2 多 1~2 天(区间匹配 + 背包 + 站内信 + 分解口径),客户端末帧多一步"放入背包/分解" —— 已计入工期 | +| 7 | **客户端工作量大**(玫瑰女郎面板是 186KB 单文件) | 直接复制改造,**不做公共 slot 基类抽象**(两个游戏的盘面/动画差异大于共性) | +| 8 | **生成耗时**(本机 i3-10105F,4 物理核) | `--workers` 上限 4;分箱分批跑;把重建写进 runbook 而不是临时起意 | +| 9 | **母本代码有文档漂移**(`olympus-py/README.md:459` 声称 RoundPicker 有 `ruoyi:olympus:seen:*` 不重发去重,**实际代码里不存在**) | 新游戏的 README 只写"代码里真的有的东西";照抄前先 grep 确认 | +| 10 | **高倍桶没有"买 FS"兜底**(只有 normal 模式,100x+ 的局全靠自然触发 FS + 长级联构造) | 生成端用 `_force_win` **主动构造**而不是等运气;接受高倍桶 `count` 低、并保留 `_calibrate_bias` 饥饿重校准;上线前专门查 8/9/10 号桶有没有条数 | + +--- + +## 13. 全部定案(**无待拍板项**) + +> 2026-09-21 全部澄清完毕。下表保留完整决策链,方便回溯"这条为什么这么定"。 + +| # | 问题 | 结论 | +|---|---|---| +| ~~Q0~~ | ~~赔付值乘的基数是哪个?~~ | ✅ **投注总额 ÷ 20**(赔付表是 1 倍基准价 + 规则页"20 条基础投注" + 截图排除法,见 §2.1) | +| ~~Q1~~ | ~~纯百搭链(第1/2/3轴全百搭)算什么?~~ | ✅ **由"金色只在第 2/3/4 轴 + 百搭只能由金色转化"自动消解**——百搭永远不在第 1 轴,链的第 1 轴必然是自然符号 ⇒ 每笔中奖天然含自然符号,**纯百搭链数学上不可能出现**,不需要单独规则、也不需要 `WILD_PAYS` 开关(见 §2.2) | +| ~~Q2~~ | ~~金色→百搭的那个百搭是否"钉住不动"?~~ | ✅ **不钉住**("符号是可以下落的,下面符号消除之后,上面符号会往下掉落") | +| ~~Q3~~ | ~~免费旋转总旋数上限~~ | ✅ **30 旋** | +| ~~Q4~~ | ~~单局封顶倍率~~ | ✅ **500x** → `MAX_WIN_CAP_MUL = 500`;配 ¥1000 档 ×10 倍,单局最大赔付 ¥5,000,000 | +| ~~Q5~~ | ~~是否要做 `ante` / `buy_fs`~~ | ✅ **都不做,只保留普通旋转** → 12 箱、无 `need_fs`、`costMultiplier≡1` | +| ~~Q6~~ | ~~各身份目标 RTP~~ | ✅ **不是拍板项**:后台配置(箱子级 `rtp` + 身份级桶配比),建库统一 0.80,上线后按身份调,见 §3.6 | +| Q7 | 投注档数量与档位表 | 🔧 **技术默认:12 档** `1/3/5/10/20/30/50/100/200/300/500/1000` × betMul 1~10(实际 1~10000)。零成本备选:10 档 + betMul 1~20,见 §2.8.1 | +| Q8 | 每日统计要不要独立日表 | 🔧 **技术默认:不要**,后台按 `record` 聚合 | +| ~~Q9~~ | ~~奖励形态:现金直付 vs 饰品入背包~~ | ✅ **赢额变成一个饰品发到背包** → 区间匹配 → `tt_box_records`(source=15) → 末帧放入/分解 → 分解时写 `MAHJONG_REWARD(69)` | +| Q10 | 后台"回合管理"要不要暴露金色/阶梯明细 | 🔧 **技术默认**:只读展示 `actual_mul / 旋数 / 波数 / 是否含 FS / 悬念盘`,不展示内部帧 | +| **新** | **百搭:来源 / 数量 / 位置 / 消除条件** | ✅ **已确认**:① 原生盘面**不会**出现百搭(卷轴不落、补位不补),**唯一来源是金色转化**;② **几个金色格被消除就留几个百搭,无上限**(一屏可同时有多个);③ 因此**百搭只可能在第 2/3/4 轴**,第 1 轴永远没有 ⇒ **纯百搭链不可能**(Q1 永久关闭);④ 百搭**可作为任何付费符号**,**被消除也要满足消除链**;⑤ 可与普通符号、也可与金色符号连着消除(同权,无特例) | +| ~~新~~ | ~~转化边界的策略开关~~ | ❌ **已取消**:既然"每个金色格各留一个、无上限",就不存在"已有百搭时该不该转化"的边界,`WILD_SLOT_POLICY` 不需要了 | + +--- + +## 14. 完整规则汇总(规则页文案版 —— 可直接交客户端 / 策划 / QA) + +> 本节把 §2 的游戏规格**按玩家视角重排**,既是客户端「规则」「赔付表」两个面板的文案来源,也是 QA 的验收清单。 +> 文案口径:**玩家能看懂**;参数口径:见每节末尾的「开发参数」。 + +### 14.1 一句话 + +麻将胡了是一款 **5 轴 × 4 行**的视频老虎机游戏,共 **1024 个中奖路**,编入了**百搭符号**、**金色麻将符号**以及**奖金乘数递增的免费旋转**。 + +### 14.2 投注与结算 + +| 项 | 规则 | +|---|---| +| 投注构成 | 「**投注大小**」(12 档)× 「**投注倍数**」(1~10)= **投注总额**,开局即扣 | +| 投注大小档位 | `1 / 3 / 5 / 10 / 20 / 30 / 50 / 100 / 200 / 300 / 500 / 1000` | +| 游戏单位 | 金额以 ¥ 为单位 | +| 现金钱包 | 显示当下玩家可用的现金余额 | +| 自动旋转 | 可按玩家选择的回合数量自动进行游戏 | +| 中奖派发 | 按「**赔付表**」执行;不同中奖路上的奖金**叠加** | +| 结算形态 | 本局赢额**按区间转换为一个饰品发到背包**;末帧可选择「放入背包 / 立即分解」;开启「自动分解」则末帧自动分解为金币 | + +> **开发参数**:`box.bet ∈ 上表 12 档`;`price = box.bet × betMul`(`costMultiplier ≡ 1`);饰品按 `game_mahjong_prize_range` 的赢额区间匹配;开局即 `settleAtStart` 写背包(无损模型)。 + +### 14.3 中奖判定(1024 路 = 前缀链) + +- 赔付表上的数值是「**每条基础投注**」的单价:**1 条基础投注 = 投注总额 ÷ 20**(游戏固定 20 条基础投注)。 +- 一条中奖路必须**从最左(第 1 轴)开始,按 1 → 2 → 3 → 4 → 5 的顺序连续出现**该符号; + - 第 1 轴是**必需**的; + - **中间断了就整笔作废**——例如 1、2、4 有、3 没有,不算中奖;2、3、4 有、1 没有,也不算。 +- 每一轴上该符号可以有**好几个**(最多 4 个),只要求「每轴至少 1 个」。 +- **路数** = 链内各轴该符号个数的**乘积**。 +- **本笔赢额 = 赔付值 × 路数 × (投注总额 ÷ 20) × 本波乘数**。 +- 中奖路奖金**从最左至右卷轴的顺序派发**;不同符号各自独立成奖、**互相叠加**。 +- 参考例子:`1 × 3 × 2 = 6` 路;赔付值 `10 × 6 = 60`(基础注),说明见游戏内「赔付表」。 + +> **开发参数**:`PAY_UNIT_DIVISOR = 20`;判定实现见 §5.2(`break` 就停,禁止写成"任意 3 轴凑齐")。 + +### 14.4 符号与赔付表 + +赔付值(每条基础投注,**1 倍基准价**): + +| 符号 | 5 连 | 4 连 | 3 连 | +|---|---|---|---| +| 發 | 100 | 60 | 15 | +| 中 | 80 | 40 | 10 | +| 白板(紫框) | 60 | 20 | 8 | +| 八萬 | 40 | 15 | 6 | +| 花筒(四色) | 20 | 10 | 4 | +| 索(红绿) | 20 | 10 | 4 | +| 筒(紫绿) | 10 | 5 | 2 | +| 索(绿) | 10 | 5 | 2 | +| **百搭(元宝)** | — | — | — | +| **胡(夺宝符号)** | — | — | — | + +### 14.5 百搭符号(元宝) + +- **百搭符号可替代除「夺宝符号(胡)」以外的所有符号**。 +- **百搭不会从卷轴上出现**:它**只能由金色普通符号被消除后转化而来**(见 14.6),而且**一个盘面上最多只会出现 1 个**。 +- 百搭能**补链中间的断点**(`發, 百搭, 發` 成立),但它**不能替代胡**——`發, 胡, 發` 不成链。 +- **百搭被消除也要满足消除链**:它和普通符号完全同权——**只有它参与了某条成立的中奖链,才会随那一波一起被消除**;不满足链就留在盘面上继续用,不会凭空消失。 +- 百搭被消除之后,才可能再由金色符号转化出新的百搭;**下一旋整盘重发,百搭不带走**。 + +### 14.6 金色麻将符号 + +**官方规则原文(游戏内「金色麻将符号」页)**: + +1. 「在任何旋转期间,第 2、第 3 和/或第 4 轴上的**某些符号(不包括百搭符号和夺宝符号)**将可能显示为金色。」 +2. 「在**新符号跌落后**的每一轮旋转中,**上一轮赢奖中所涉及的**黄色麻将符号将被**转换为百搭符号**。」 + +**人话版(4 条,这是要写进规则页的文案)**: + +| # | 规则 | +|---|---| +| 1 | **谁能变金**:只有**第 2、第 3、第 4 轴**上的**普通符号**会显示为金色(**百搭和胡不会变金**) | +| 2 | **金色是什么**:就是那个普通符号**镀了一层金**,判定与赔付**仍然按它的自然符号**算(金色「中」照「中」的赔付表赔) | +| 3 | **中奖后怎么变**:这个金色符号**参与中奖、被消除时**,它原来的位置**不会空掉、也不会补新牌,而是留下一个真正的百搭符号**(元宝)——**每个金色格各留一个**(同波几个金色格中奖就留几个,无上限) | +| 4 | **之后怎么用**:留下的百搭和盘面上直接落下的百搭**完全一样**——可以替代除「胡」以外的所有符号、可以补链搭桥、参与中奖时同样被消除 | + +**两个最容易搞错的点**: + +- 官方原文说"**在新符号跌落后**的每一轮旋转中……被转换为百搭"——这是**动画时序**的描述(视觉上要等新牌落下来才看到它变百搭);**实现上是"该格被消除的那一刻就地写入百搭"**,两者不冲突。 +- 金色符号**本身就是普通符号**,所以它照样**随重力下落**(下面被消除就往下掉),**不会钉在空中**。 + +**一个连贯的例子**: + +| 步骤 | 盘面 | 发生了什么 | +|---|---|---| +| 第 1 波 | 金色「中」参与「中」的 3 连 | 按「中」赔付;结算后该格**留下一个百搭** | +| 第 2 波 | 掉落后形成 `發, 百搭, 發 …` | 百搭**搭桥**,「發」3 连成立 | +| 第 3 波 | 这个百搭又参与了一笔中奖 | 它**随这一波被消除**(百搭也是一张普通牌) | + +> **开发参数**:每旋开局对第 2/3/4 轴的普通符号按 `GOLDEN_CELL_PROB`(建议 0.10,逐格独立)打金色标记,新补入的符号也可能带金色;本波中奖涉及的金色格在消除时**不补新符号、就地写 `W`**,并随重力下落(不锚定);每格只转化一次,转化后的 `W` 不再回填金色。判定细节见 §2.3。 + +### 14.7 乘数阶梯(奖金翻倍) + +- **在任何旋转期间,所有赢奖都将乘以卷轴上方所显示的乘数值**。 +- **普通旋转**:第 1 次消除 **x1** → 第 2 次 **x2** → 第 3 次 **x3** → 第 4 次及以后 **x5**。 +- **免费旋转**:乘数分别增加至 **x2、x4、x6、x10**。 +- **每旋重新从第一档开始**,不跨旋累计。 + +> **开发参数**:`LADDER_BASE = (1,2,3,5)`、`LADDER_FS = (2,4,6,10)`;第 n 波取 `ladder[min(n,4)]`。 + +### 14.8 级联(翻滚) + +- **每轮游戏结束并发放奖金后,所有赢奖符号都将爆炸消除,让其上方的符号向下跌落**,以进行新一轮的旋转。 +- 顶部补入新符号后进入下一波,**直到没有其他中奖组合**为止。 +- **额外的中奖组合都将计入每次旋转的赢奖中**。 + +### 14.9 免费旋转模式 + +- **当 3 个夺宝符号(胡)出现在卷轴的任意位置时,免费旋转模式将被触发,并获得 12 次免费旋转**。 +- **每个额外的夺宝符号都将触发多 2 次的免费旋转**:3 个 → 12 次,4 个 → 14 次,5 个 → 16 次,6 个 → 18 次。 +- **免费旋转模式期间,卷轴上方的乘数将分别增加至 x2、x4、x6 和 x10**。 +- **免费旋转可以被重复触发**(免费旋转内再出现 3+ 个胡,同样按"12 + 每个额外胡 +2"追加)。 +- **免费旋转的总旋数上限为 30 旋**(到顶后不再出现 3+ 个胡)。 +- 免费旋转期间投注大小与投注倍数、以及触发免费旋转模式时的参数**保持一致**。 + +> **开发参数**:`FS_AWARD = 12 + 2 × (scatterCount − 3)`;重触发同式;**总旋数上限 30**(Q3 定案);到顶后不再落 3+ 胡。 + +### 14.10 悬念表现(2 个胡) + +- 当卷轴上已出现 **2 个夺宝符号**时,**最后一列会播放大/发光效果**,吊起"再来一个胡"的期待感。 + +> **开发参数**:客户端按 `gridState` 里的 `S` 计数驱动;生成端用 `NEARMISS_2SCATTER_RATE`(1%~2%)保证这种盘面真实存在。 + +### 14.11 爆奖与封顶 + +- **单局封顶**:总倍率到达封顶即截断,剩余旋数作废,保证"动画总赢额 = 实际到账"。 +- **爆奖档位**:小 / 中 / 大(按单旋赢额判档,只在免费旋转内播)。 +- **所有赢奖以现金显示**(游戏内显示金额),**最终以饰品形式发放到背包**。 + +> **开发参数**:**封顶 500x**(Q4 定案)——`totalMul ≥ 500` 即 `maxWinReached=true`、`spinsRemaining=0`、按 500 倍截断发放;爆奖阈值见 §12 对应条目。 + +### 14.12 参数速查(给开发 / QA) + +| 参数 | 值 | +|---|---| +| 盘面 | 5 轴 × 4 行 = 20 格 | +| 路数 | 4⁵ = 1024 | +| 赔付单位 | 投注总额 ÷ 20(`PAY_UNIT_DIVISOR = 20`) | +| 判定 | 前缀链(第 1 轴起连续,断链作废,`ways` 只乘链内轴) | +| 百搭 | **只能由金色符号转化产生**(原生盘面不落);**每个金色格各留一个(无上限)**,**只可能在第 2/3/4 轴**;可替代除「胡」外全部;满足消除链才被消除 | +| 金色符号 | 只在第 2/3/4 轴;按自然符号成奖;消除后就地留百搭 | +| 普通旋阶梯 | `1 / 2 / 3 / 5`(第 4 波及以后 x5) | +| FS 阶梯 | `2 / 4 / 6 / 10` | +| 免费旋转 | 3+ 胡 → 12 旋,每个额外胡 +2,可重触发,**上限 30 旋** | +| 悬念盘 | 2 个胡 → 第 5 轴放大;生成端占比 1%~2% | +| 投注档 | 12 档:1/3/5/10/20/30/50/100/200/300/500/1000 | +| 投注倍数 | 1~10 | +| 封顶 | **500x** | +| 发奖 | 赢额 → 区间匹配饰品 → 背包 → 可分解 | + +--- + +## 15. 完整消除示例(逐波走一遍,用来校验规则理解) + +> 场景:**普通旋转**,投注大小 ¥100 × 投注倍数 1 → **投注总额 ¥100**,故 **1 条基础投注 = 100 ÷ 20 = ¥5.00**。 +> 本旋共 **4 波消除**,覆盖:两个符号同时中奖(叠加)、**两个金色格各留一个百搭**、百搭随重力下落、百搭参与成链后自己也被消除、3/4/5 连三种档位、x1→x2→x3→x5 全阶梯。 +> 图例:`A`=發 `B`=中 `C`=白板 `D`=八萬 `E`=花筒 `F`=索(红绿) `G`=筒(紫绿) `H`=索(绿) `W`=百搭(元宝) `S`=胡 **加粗带 `*` = 金色**。 +> ⚠️ 金色格只能出现在**第 2/3/4 轴**(本示例的两张金色都在轴2、轴3)。 + +### 第 1 波(基础旋 · 阶梯 x1) + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | B | C | B | E | S | +| 行2 | B | **B\*** | **B\*** | D | F | +| 行3 | D | C | C | G | E | +| 行4 | C | E | H | F | G | + +**判定**(前缀链;两张金色格都按自然符号「中」算): + +| 符号 | 轴1 | 轴2 | 轴3 | 轴4 | 轴5 | 结果 | +|---|---|---|---|---|---|---| +| **中 B** | 2(行1、行2) | 1(行2金) | 2(行1、行2金) | 0(E/D/G/F) | — | **3 连,ways = 2×1×2 = 4** | +| **白板 C** | 1(行4) | 2(行1、行3) | 1(行3) | 0 | — | **3 连,ways = 1×2×1 = 2** | +| 其余 6 个 | 轴2 里都没有它们 | — | — | — | — | 轴 2 断,不成奖 | + +> 本波**两个符号同时中奖**,奖金**叠加**。 + +**赔付**: + +| 中奖 | 赔付值 | ways | 基础注 | 阶梯 | 赢额 | +|---|---|---|---|---|---| +| 中 3 连 | 10 | 4 | 40 | x1 | **¥200** | +| 白板 3 连 | 8 | 2 | 16 | x1 | **¥80** | +| | | | | 小计 | **¥280** | + +**本波共 9 个符号被消除**(9 格全播爆裂动画): + +| 格子 | 消除后 | +|---|---| +| **中链**:(行1,轴1)、(行2,轴1)、(行1,轴3) | 空洞 → 下落 + 补新牌 | +| **中链**:(行2,轴2)**金**、(行2,轴3)**金** | **每张金色格各留一个百搭** → 两格**都**就地变 `W`(**本波留下 2 个百搭**) | +| **白板链**:(行4,轴1)、(行1,轴2)、(行3,轴2)、(行3,轴3) | 空洞 → 下落 + 补新牌 | + +> ⚠️ 两条口径一起记: +> ① **"消除 ≠ 清空"**——9 格全部"被消除"(全爆),区别只在消除**之后**:金色格写 `W`,其余格置空待补; +> ② **"几个金色格中奖,就留几个百搭"**——不是每波只留 1 个。本波留 **2 个**。 + +**重力 + 补位**(列内幸存符号保持顺序压到底,顶部补新牌): + +| 轴 | 本波消除 | 该列变化 | 补位后(上→下) | +|---|---|---|---| +| 1 | 行1、行2、行4 | 只剩 D(行3) → 落到 **行4** | 顶部补 D、D、D → **D / D / D / D** | +| 2 | 行1、行3 | 幸存 `W`(行2)、E(行4) → **`W` 落到行3**、E 到行4 | 顶部补 F、G → **F / G / W / E** | +| 3 | 行1、行3 | 幸存 `W`(行2)、H(行4) → **`W` 落到行3**、H 到行4 | 顶部补 A、B → **A / B / W / H** | +| 4、5 | 无 | — | 原样不动 | + +#### 第 1 波 → 第 2 波的盘面演变(两步) + +> ⚠️ **说明**:消除之后符号**立刻**落下来(存活符号下移 + 新牌从列顶落下),“整列空洞”只是一个瞬时中间帧、不是一个静止状态,所以下面只展示**两个**关键画面:**消除瞬间** 和 **落定后**。 + +**步骤 ① 消除瞬间**(9 格全爆;**两张金色格就地变 `W`**,其余中奖格变空洞): + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | ▢ | ▢ | ▢ | E | S | +| 行2 | ▢ | **W** | **W** | D | F | +| 行3 | D | ▢ | ▢ | G | E | +| 行4 | ▢ | E | H | F | G | + +> `▢` = 空洞(待补)。两张金色格**各自**变成了 `W`,盘面上现在**同时有 2 个百搭**。 + +**步骤 ② 符号落定(存活符号下移 + 新牌从列顶落下)→ 这就是「第 2 波消除之前的盘面」**: + +| 轴 → | 1 | 2 | 3 | 4 | 5 | 这一列发生了什么 | +|---|---|---|---|---|---|---| +| 行1 | **D** | **F** | **A** | E | S | 轴1 整列被清空 → **落进来 4 张新牌** | +| 行2 | **D** | **G** | **B** | D | F | 轴2:`W` 与 E 下移一格 → 顶部落进 2 张新牌 | +| 行3 | D | **W** | **W** | G | E | 轴3:`W` 与 H 下移一格 → 顶部落进 2 张新牌 | +| 行4 | D | E | H | F | G | 轴4、轴5 没被消除 → 不动 | + +> **粗体** = 本步落进来的牌。两个 `W` **都从行2 落到了行3**;轴1 因为四格全爆、列内没有存活符号,所以落进来的**全是新牌**(4 张)——这就是"消除完符号掉落下来"的那一步。 + +### 第 2 波(阶梯 x2)——**两个百搭一起帮忙成链,然后一起被消除** + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | D | F | A | E | S | +| 行2 | D | G | B | D | F | +| 行3 | D | **W** | **W** | G | E | +| 行4 | D | E | H | F | G | + +| 符号 | 轴1 | 轴2 | 轴3 | 轴4 | 轴5 | 结果 | +|---|---|---|---|---|---|---| +| **八萬 D** | 4(行1~行4) | **1(百搭当八萬)** | **1(百搭当八萬)** | 1(行2) | 0(S/F/E/G) | **4 连,ways = 4×1×1×1 = 4** | +| F / G / A / B | 轴1 没有 | — | — | — | — | 轴1 断,不成奖 | + +> 本波盘面**只有轴 1 是纯八萬**(补位恰好都补成 D),所以只有八萬成链。这也顺带说明了:**有百搭的那几轴对所有符号都成立**,链能不能成完全取决于"没有百搭的那些轴"。 + +**赔付**:八萬 **4 连**赔付值 **15** × ways **4** = 60 基础注 = **¥300** × 阶梯 **x2** = **¥600** + +**本波共 7 个符号被消除**:轴1 行1~行4 的 4 张 D、**(行3,轴2) W**、**(行3,轴3) W**、**(行2,轴4) D**。 +- **两个百搭都参与了成立的中奖链** → 按"满足消除链才消除" → **两个 `W` 一起被消除**(盘面回到 0 个百搭) +- 本波中奖格**没有金色** → **不产生新百搭** + +**重力 + 补位**: + +| 轴 | 该列变化 | 补位后(上→下) | +|---|---|---| +| 1 | 4 格全消除 → 无幸存 | 顶部补 4 张 → **E / H / C / F** | +| 2 | 消除行3 → 幸存 F,G,E 落到行2~4 | 顶部补 C → **C / F / G / E** | +| 3 | 消除行3 → 幸存 A,B,H 落到行2~4 | 顶部补 **E\***(**这张是金色**) → **E\* / A / B / H** | +| 4 | 消除行2 → 幸存 E,G,F 落到行2~4 | 顶部补 A → **A / E / G / F** | +| 5 | 无 | 原样 **S / F / E / G** | + +#### 盘面演变(第 2 波 → 第 3 波) + +**① 消除瞬间**(7 格全爆,两个 `W` 都被消除;本波无金色格,不产生新百搭): + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | ▢ | F | A | E | S | +| 行2 | ▢ | G | B | ▢ | F | +| 行3 | ▢ | ▢ | ▢ | G | E | +| 行4 | ▢ | E | H | F | G | + +**② 符号落定(存活符号下移 + 新牌从列顶落下)→ 「第 3 波消除之前的盘面」**: + +| 轴 → | 1 | 2 | 3 | 4 | 5 | 这一列发生了什么 | +|---|---|---|---|---|---|---| +| 行1 | **E** | **C** | **E\*** | **A** | S | 轴1 整列清空 → 落进 4 张新牌 | +| 行2 | **H** | F | A | E | F | 轴2:F/G/E 下移一格 → 顶部落进 1 张 | +| 行3 | **C** | G | B | G | E | 轴3:A/B/H 下移一格 → 顶部落进 1 张(**金色**) | +| 行4 | **F** | E | H | F | G | 轴4:E/G/F 下移一格 → 顶部落进 1 张;轴5 没被消除 | + +> 轴1 四格全爆 → 没有存活符号 → **落进来的全是新牌**;两个 `W` 被消除,本波不产生新百搭。 + +### 第 3 波(阶梯 x3)——**一张金色格中奖,留 1 个百搭** + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | E | C | **E\*** | A | S | +| 行2 | H | F | A | E | F | +| 行3 | C | G | B | G | E | +| 行4 | F | E | H | F | G | + +| 符号 | 轴1 | 轴2 | 轴3 | 轴4 | 轴5 | 结果 | +|---|---|---|---|---|---|---| +| **花筒 E** | 1(行1) | 1(行4) | **1(行1金)** | 1(行2) | 1(行3) | **5 连,ways = 1×1×1×1×1 = 1** | +| 索绿 H | 1(行2) | 0(C/F/G/E) | — | — | — | 轴2 断 | +| 白板 C | 1(行3) | 1(行1) | 0(E/A/B/H) | — | — | 轴3 断 | +| 索红绿 F | 1(行4) | 1(行2) | 0 | — | — | 轴3 断 | +| 其余 | 轴1 没有 | — | — | — | — | 不成奖 | + +**赔付**:花筒 **5 连**赔付值 **20** × ways **1** = 20 基础注 = **¥100** × 阶梯 **x3** = **¥300** + +**本波共 5 个符号被消除**:(行1,轴1)E、(行4,轴2)E、**(行1,轴3) 金**、(行2,轴4)E、(行3,轴5)E——5 格全爆。 +其中 4 格变空洞;金色格 (行1,轴3) **原地留百搭 `W`**(本波开始时盘面 0 个百搭 ✅,本波只有 1 张金色格中奖 → **留 1 个**)。 + +**重力 + 补位**: + +| 轴 | 该列变化 | 补位后(上→下) | +|---|---|---| +| 1 | 消除行1 → 幸存 H,C,F 落到行2~4 | 顶部补 G → **G / H / C / F** | +| 2 | 消除行4 → 幸存 C,F,G 落到行2~4 | 顶部补 A → **A / C / F / G** | +| 3 | **该列无空洞**(金色格变 `W`) | 原样 → **W / A / B / H** | +| 4 | 消除行2 → 幸存 A,G,F 落到行2~4 | 顶部补 G → **G / A / G / F** | +| 5 | 消除行3 → 幸存 S,F,G 落到行2~4 | 顶部补 C → **C / S / F / G** | + +#### 盘面演变(第 3 波 → 第 4 波) + +**① 消除瞬间**(5 格全爆;金色格 (行1,轴3) 就地变 `W`): + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | ▢ | C | **W** | A | S | +| 行2 | H | F | A | ▢ | F | +| 行3 | C | G | B | G | ▢ | +| 行4 | F | ▢ | H | F | G | + +**② 符号落定(存活符号下移 + 新牌从列顶落下)→ 「第 4 波消除之前的盘面」**: + +| 轴 → | 1 | 2 | 3 | 4 | 5 | 这一列发生了什么 | +|---|---|---|---|---|---|---| +| 行1 | **G** | **A** | **W** | **G** | **C** | 轴1:H/C/F 下移一格 → 顶部落进 1 张 | +| 行2 | H | C | A | A | S | 轴2:C/F/G 下移一格 → 顶部落进 1 张 | +| 行3 | C | F | B | G | F | **轴3 没有空洞**(金色格变 `W`)→ 整列不动 | +| 行4 | F | G | H | F | G | 轴4:A/G/F 下移一格 → 顶部落进 1 张;轴5:S/F/G 下移 → 落进 1 张 | + +> `W` 停在原处(轴3 那列没被清空),其余列各自下移一格并补新牌。 + +### 第 4 波(阶梯 x5,第 4 波及以后都是 x5)——**一个百搭被两个符号同时借用,本旋结束** + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | G | A | **W** | G | C | +| 行2 | H | C | A | A | S | +| 行3 | C | F | B | G | F | +| 行4 | F | G | H | F | G | + +| 符号 | 轴1 | 轴2 | 轴3 | 轴4 | 轴5 | 结果 | +|---|---|---|---|---|---|---| +| **筒 G** | 1(行1) | 1(行4) | **1(百搭当筒)** | 1(行1、行3 → 2) | 1(行4) | **5 连,ways = 1×1×1×2×1 = 2** | +| **索红绿 F** | 1(行4) | 1(行3) | **1(百搭当索红绿)** | 1(行4) | 1(行3) | **5 连,ways = 1×1×1×1×1 = 1** | +| 白板 C | 1(行3) | 1(行2) | 0(W/A/B/H) | — | — | 轴3 断 | +| 索绿 H | 1(行2) | 0(A/C/F/G) | — | — | — | 轴2 断 | +| 發 A | 轴1 没有 | — | — | — | — | 不成奖 | + +> 同一个百搭这波**同时被筒和索红绿两个符号借用**,两条链都成立 → **两笔并赔**。 + +**赔付**: + +| 中奖 | 赔付值 | ways | 基础注 | 阶梯 | 赢额 | +|---|---|---|---|---|---| +| 筒 5 连 | 10 | 2 | 20 | x5 | **¥500** | +| 索红绿 5 连 | 20 | 1 | 20 | x5 | **¥500** | +| | | | | 小计 | **¥1000** | + +**本波共 9 个符号被消除**(筒链 5 格 ∪ 索红绿链 5 格,重叠在 `W` 与部分格): +(行1,轴1)G、(行4,轴1)F、(行4,轴2)G、(行3,轴2)F、**(行1,轴3) W**、(行1,轴4)G、(行3,轴4)G、(行4,轴4)F、(行3,轴5)F、(行4,轴5)G—— +`W` 参与了两条链 → **被消除**;本波中奖格无金色 → **不产生新百搭**。 + +#### 盘面演变(第 4 波 → 本旋结束) + +**① 消除瞬间**(10 格全爆:筒链 6 格 ∪ 索红绿链 5 格,重叠在 `W`): + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | ▢ | A | ▢ | ▢ | C | +| 行2 | H | C | A | A | S | +| 行3 | C | ▢ | B | ▢ | ▢ | +| 行4 | ▢ | ▢ | H | ▢ | ▢ | + +**② 符号落定(存活符号下移 + 新牌从列顶落下)→ 本旋结束时的盘面**: + +| 轴 → | 1 | 2 | 3 | 4 | 5 | 这一列发生了什么 | +|---|---|---|---|---|---|---| +| 行1 | **E** | **G** | **E** | **C** | **A** | 轴1:H/C 下移两格 → 落进 2 张 | +| 行2 | **F** | **D** | A | **D** | **B** | 轴2:A/C 下移两格 → 落进 2 张 | +| 行3 | H | A | B | **E** | C | 轴3:A/B/H 下移一格 → 落进 1 张 | +| 行4 | C | C | H | A | S | 轴4:A 落到底 + 落进 3 张;轴5:C/S 下移 + 落进 2 张 | + +→ 复核这个盘面:轴1 有 E/F/H/C,其中只有 C 能在轴2 对上,而**轴3 没有 C** → **无任何中奖组合,本旋结束(整局共 4 波)**。 + +### 本旋总账 + +| 波 | 中奖 | 赔付值 | ways | 基础注 | 阶梯 | 本波赢额 | +|---|---|---|---|---|---|---| +| 1 | 中 3 连 | 10 | 4 | 40 | x1 | ¥200 | +| 1 | 白板 3 连 | 8 | 2 | 16 | x1 | ¥80 | +| 2 | 八萬 4 连 | 15 | 4 | 60 | x2 | ¥600 | +| 3 | 花筒 5 连 | 20 | 1 | 20 | x3 | ¥300 | +| 4 | 筒 5 连 | 10 | 2 | 20 | x5 | ¥500 | +| 4 | 索红绿 5 连 | 20 | 1 | 20 | x5 | ¥500 | +| | | | | | 合计 | **¥2180** | + +- 投注总额 ¥100 → **本局总倍率 = 2180 ÷ 100 = 21.80x**(未触及 500x 封顶) +- 本旋只有 **1 个胡**(一直挂在轴5)→ **不触发免费旋转**(需 3+) + +### 百搭轨迹(这张表是重点:**几个金色格中奖,就留几个百搭**) + +| 时刻 | 盘上百搭数 | 发生了什么 | +|---|---|---| +| 旋开局 | **0** | 原生盘面不落百搭 | +| 第 1 波结束 | 0 → **2** | 同波有**两张**金色「中」中奖 → **各留一个百搭**(轴2、轴3 各一个)。两个 `W` 随后**都随重力从行2 掉到行3** | +| 第 2 波结束 | 2 → **0** | **两个 `W` 都参与了八萬链**(满足消除条件)→ 一起被消除;本波无金色中奖,不补 | +| 第 3 波结束 | 0 → **1** | 本波只有 **1 张**金色「花筒」中奖 → 留 **1 个** `W`;该列没有空洞,所以它**原地不动** | +| 第 4 波结束 | 1 → **0** | `W` 同时参与筒链与索红绿链 → 被消除;本波无金色中奖,不补 | +| 旋结束 | 0 | 无后续中奖;下一旋整盘重发 | + +> **百搭数量没有上限**(第 1 波就同时有 2 个),但**只可能出现在第 2/3/4 轴**——因为金色格只在那三轴,而百搭的唯一来源是金色转化。 +> 这条推论很关键:**第 1 轴永远不会有百搭**,所以前缀链的第 1 轴必然是自然符号 ⇒ **任何一笔中奖都至少含 1 个自然符号** ⇒ **"纯百搭链"在数学上不可能出现**(Q1 因此永久关闭,不需要额外规则)。 + +> 全程每一波盘面上的百搭数都 **≤ 1** ✅,且**永远只由金色转化产生** ✅。 +> 本示例**注意**:第 1 波两张金色格各留一个百搭 → **盘面同时有 2 个百搭**,第 2 波它们被一起消除。这与"每个金色格各留一个、无上限"的口径一致。 + +--- + +## 附录 A:与玫瑰女郎的字段/表名映射速查 + +| 玫瑰女郎 | 麻将胡了 | +|---|---| +| `game_olympus_box.total_ornaments` | `game_mahjong_box.total_rounds` | +| `game_olympus_ornament`(17 档饰品桶) | `game_mahjong_bucket`(10 档倍率桶) | +| `game_olympus_box_ornament.ornament_id` | `game_mahjong_box_bucket.bucket_id` | +| `game_olympus_round.ornament_id` | `game_mahjong_round.bucket_id` | +| `game_olympus_step.symbol_links` | `game_mahjong_step.ways_detail` | +| `game_olympus_step.multiplier_detail`(炸弹) | **删除**,改为 `golden_detail` + `rotation_mul` | +| `game_olympus_step.accumulator_*` | `game_mahjong_step.rotation_mul`(语义:本波档位,不跨旋) | +| `game_olympus_step.free_spin_id` / `cascade_seq` | `reel_spin_id` / `rotation_seq` | +| `game_olympus_prize_ornament` / `_prize_range` | `game_mahjong_prize_ornament` / `_prize_range`(**要,赢额→饰品**) | +| `TtboxRecordSource.OLYMPUS_BOX(13)` | `TtboxRecordSource.MAHJONG_BOX(15)`(写背包) | +| `OLYMPUS_SPIN(60)` / `OLYMPUS_REWARD(61)` | `MAHJONG_SPIN(68)` / `MAHJONG_REWARD(69)` | +| `ruoyi:olympus:*` | `ruoyi:mahjong:*` | +| `olympusMaintenance` | `mahjongMaintenance` | + +## 附录 B:关键代码坐标(新游戏要对齐的现有实现) + +| 主题 | 文件:行 | +|---|---| +| 两阶段挑桶 | `game-olympus-service/.../service/OlympusRoundPicker.java:44-100` | +| 水位仓储 | `.../repository/OlympusRewardPoolCacheRepository.java:80-84` | +| 开局事务 + 提交后吞异常 | `.../service/OlympusGameService.java:173-220` | +| 开局结算 + 幂等守卫 | `.../service/OlympusGameService.java:279-306` | +| 续播 / 末帧 / 战绩可见性 | `.../service/OlympusGameService.java:230-254` | +| 封顶判定 | `.../service/OlympusGameService.java:426-429`(`MAX_WIN_CAP_MUL`) | +| 配比算法(py) | `olympus-py/_recalc_boxes.py:52-129` | +| 配比算法(Java) | `.../service/OlympusDistributionService.java:100-207` | +| 生成端补桶/饥饿重校准 | `olympus-py/olympus_simulator.py:740-805` | +| 级联几何复刻测试 | `olympus-py/tests/test_simulator.py::TestTumbleGeometry` | +| 平台流水 source 聚合 | `skins-model/.../TtAccountRecordSource.java:193-279` | +| 建表 + 菜单 SQL 写法 | `docs/updata_sql.md:1144-1225` | +| 接口契约模板 | `docs/玫瑰女郎接口文档.md`(706 行) | + +## 附录 C:待新增的运维 runbook(对齐 `olympus-py/README.md`) + +1. **首次上线**:`mahjong_db.py` → `generate_rounds.py --all --alloc config --write` → `_make_templates.py` → `_recalc_boxes.py --mode normal` → **给 12 箱 × 4 身份注水**(水位从 0 起,不注水则前几局 payoutCap ≈ 0.8×下注,大奖出不来;**¥500/¥1000 这两档必须单独注**)。 +2. **改配比(不重建池子)**:后台"一键重算"或 `_recalc_boxes.py --mode X` → 清 `ruoyi:mahjong:cycle:*` → `_make_templates.py`。 +3. **重建池子**:`_regen.py snapshot` → `generate_rounds.py --clear --write` → 重算 → 清 Redis → 刷模板 → `_regen.py verify`。 +4. **调水位**:后台奖池水位页按身份逐箱注水/抽水(**原因必填**)或批量按"一轮花费基数 × 比例%"。 +5. **日常体检**:`_regen.py check`(每桶有条目 / 无封顶缺失 / 池内 RTP)+ `simulate_cycle.py` 抽样。 diff --git a/olympus-py/README.md b/olympus-py/README.md new file mode 100644 index 0000000..79146f3 --- /dev/null +++ b/olympus-py/README.md @@ -0,0 +1,516 @@ +# Olympus 电子游戏 — Python 端结构文档 + +> match-3 消除类电子游戏(皇冠/宝石主题)的**规则核心 + 离线回合生成器**。 +> Python 端负责:可执行的规则规范(引擎)、回合数据预生成、落库。 +> 在线播放接口、结算发奖由后续的 Java 服务端消费本模块产出的 `round/step` 数据实现。 + +--- + +## 一、目录结构 + +``` +olympus-py/ +├── olympus/ 规则核心包(纯逻辑,无第三方依赖) +│ ├── __init__.py 包导出:引擎/模型/关键配置 +│ ├── config.py 全部规则配置 + 3 个查询函数 +│ ├── engine.py OlympusEngine 结算状态机(规则的可执行规范) +│ └── models.py 不可变数据模型(Bomb/Elimination/Cascade/Spin/Session) +├── tests/ +│ ├── __init__.py +│ └── test_engine.py 25 项单元测试(规则回归) +├── olympus_simulator.py 回合生成器核心(网格/级联/炸弹/免费旋转/落桶/截断) +├── generate_rounds.py 生成入口(--per-box 每箱总回合 / box_id 从1顺序) +├── olympus_db.py 建表 + 批量写库(连接参数走环境变量) +├── schema.sql 全量 DDL(12 张表)+ 饰品桶种子数据 +└── pyproject.toml 包元信息(name=olympus, py>=3.9) +``` + +> 辅助脚本:`_reset_boxes.py`(重置——清空 box/box_ornament/round/step/box_template)、`_create_reward_log.py`(建奖池调整日志表 game_olympus_reward_log)。 + +分两层: +- **规则核心 `olympus/`**:定义"游戏应该怎么算",被引擎和生成器共同依赖,是唯一事实来源。 +- **生成/落库工具(根目录)**:调用规则核心批量产出回合数据并写入 MySQL。 + +--- + +## 二、核心游戏规则总览 + +| 规则 | 值 / 说明 | +|---|---| +| 网格 | 5 行 × 6 列 = 30 格 | +| 中奖门槛 | 屏幕任意位置同符号 **≥8 个**即赔付(pay-anywhere) | +| 普通符号 | 9 种:皇冠/沙漏/环/杯赛/红/紫/黄/绿/蓝宝石,各有 **8-9 / 10-11 / 12-30** 三档区间赔率(12 个及以上至全屏 30 格归最高档) | +| SCATTER | 4/5/6 个赔 3x/5x/100x,且 **≥4 个触发免费旋转** | +| 免费旋转 | 首次赠送 **15 次**;FS 中 **≥3 个 SCATTER 重触发 +5 次**(4/5/6 个同时赔 3x/5x/100x 并入本旋赢额;恰好3个只+5不赔),上限 30,**到上限后不再落 3+ 个 SCATTER**(否则会写出"extraSpins=5 但加不上旋"的 retrigger_info) | +| 炸弹(乘数符号) | 值 2x~1000x,分 5 种品质色;**只在免费旋转内出现**(normal/ante 的基础旋、含进 FS 的触发盘一律不出) | +| 消除→引爆 | 一次旋转内多波消除,**全部消完后末帧统一引爆炸弹** | +| 单局封顶 | 累计赢奖 **≤ 1000 倍**基础下注,超出截断 | + +--- + +## 三、三种游戏模式(互斥) + +| 模式 | code | 扣费倍率 | 实际扣费档位 | 底注效果 | 可购买 FS | 保证进 FS | +|---|---|---|---|---|---|---| +| 普通旋转 | `normal` | 1× | 1,3,5,10,20,30,50,100,200,300 | ✗ | ✓ | ✗ | +| 底注 | `ante` | 2× | 2,6,10,20,40,60,100,200,400,600 | ✓ | ✗ | ✗ | +| 购买免费旋转 | `buy_fs` | 50× | 50,150,250,500,1000,1500,2500,5000,10000,15000 | ✗ | ✗ | ✓ | + +**互斥关系**(双向):底注开启 → 禁止购买免费旋转;购买免费旋转 → 强制关闭底注效果。三种模式基础注档位完全一致(均 10 档)。 + +- `normal` vs `ante`:ante 花双倍钱,换 **FS 触发概率翻倍**(0.5%→1%)+ SCATTER 出现率更高 + 桶顶更高(150x→200x)。 +- `buy_fs`:花 50 倍直接进 15 次免费旋转,是累计器滚雪球的主要场景。**首盘固定 4 个 SCATTER**(直接赔 3x),且该盘不出炸弹(炸弹只在免费旋转内出现,触发盘炸弹本来也不带入累计器)。 +- **normal/ante 也能进 FS**:基础旋摇出 4~6 个 SCATTER 就免费进 15 旋,进 FS 后与 buy_fs **完全同一套规则**(炸弹、累计器、每旋保底消除),见第三点五 / 第四节。 + +--- + +## 三点五、基础旋为什么没有炸弹(normal/ante) + +> 规则:**炸弹只存在于免费旋转内**。`_spin()` 里 `if not is_free_spin: allow_bomb = False` —— 基础旋(含进 FS 的触发盘、含补桶植入的大奖旋)整盘不会有 `M`。 + +随之而来的物理限制:没有炸弹,基础旋最多只能凑到 `A12(50) + B10(10) ≈ 60x`(自然多组 12 连到 75x 已属极罕见),**100x / 150x / 200x 这些桶基础旋根本赔不出来**。所以: + +| 目标倍率 | 谁来装这个桶 | +|---|---| +| < 80x(`FS_ONLY_TARGET_MUL`) | 基础旋(`--alloc config` 补桶时用 `_plan_base_only` 植基础组合) | +| ≥ 80x(100x/150x/200x 桶) | **自然触发 FS 的回合**:`generate_round(box, bias≥80)` 直接生成"4~6 S 触发盘 + 15 旋 FS",FS 预算 `= 桶目标 − 触发盘赢额` | + +这正是这两个模式里"能进免费旋转"的现实来源:玩家在后台配比里抽中 100x/150x(/200x) 桶时,看到的就是一次免费旋转(box1:桶 12/13 的池子 100% 是 FS 回合)。 + +实测(2026-09 用当前生成器重跑全部 30 箱之后;全库 39,431 回合 / 358,234 步): + +| 检查项 | 结果 | +|---|---| +| 盘面 | 5×6、30 格全满、只有 A~I/S/M —— **空白格 0、非法格 0** | +| 掉落复刻 | 每一步都能用「上一波盘面 − 上一波新增连线 → `drop_data` 下落 → `fill_data` 补位」**精确**复刻(不符 0 条) | +| 金额 | 连线 `base_score` = 赔付表、单旋赢额 = 基础分 × max(1, 累计+本旋炸弹) + scatter、累计器、`spin_win` 连续 —— 全对(只有 2 步在 1000x 封顶截断局里 `accumulator_after` 记的是未并入值,该字段已下发为接口 `accumulator`) | +| 免费旋转多样性 | 13,542 个 FS 回合,**整局盘面完全重复 = 0** | +| 基础旋炸弹 | normal/ante 基础旋炸弹格 **0**;触发盘 S 数:normal 519 局 / ante 818 局都是 4~6,买 FS 12,205 局固定 4 | +| 100x+ 桶 | normal/ante 的桶 12/13/14 池子 200/200/200 条**全部**是 FS 回合(每箱 49~55 / 76~88 个 FS 回合) | +| RTP | 30 个箱**后台配比反推 = 池内实际口径 = 79.99%~80.03%**(见第六点五;重算后不再有 ±1.7pt 偏差) | +| 池子覆盖率 | 池内回合数 / 配比 count 最小 **1.36x**(≥1 → 一个 cycle 内不会重复发) | +| 抽回合模拟 | `simulate_cycle.py` 复刻 Java 链路(含 payoutCap/水位/抽干按模板重填)各 3~4 个 cycle 都收敛到 **80.0%** | + +--- + +## 四、炸弹累计器(关键机制) + +> **累计器只存在于免费旋转模式**,基础旋转没有累计器。三种模式只要进入 FS(normal/ante 摇出 4+S 或 buy_fs 购买)均启用累计器,与模式无关。 + +| 场景 | 炸弹行为 | 累计器 | +|---|---|---| +| 基础旋转(normal/ante 未进 FS) | **整盘不出炸弹**(`allow_bomb=False`) | 不存在(`multiplier_after` 不变) | +| 免费旋转(三模式一致:≥4 S 进入 / buy_fs 购买) | 每屏最多 1 颗(`FS_SCREEN_BOMB_PROB=0.08` 每空格);有消除时并入累计器,跨旋传递 | **从 0 起累**,永不归零直到 FS 结束 | + +**计算公式**(`engine.end_spin`): +- 基础旋转:`本旋赢额 = 基础分 × max(1, 本旋炸弹和)` +- 免费旋转:`本旋赢额 = 基础分 × max(1, 累计值 + 本旋新炸弹)`,之后 `累计值 += 本旋炸弹和` + (**累计倍数作用到每一次消除** —— 0.4×2 + 0.5×4 = 2.8 这种滚雪球,这是这个玩法的核心爽点) + +三条性质:① 免费旋转内累计器只涨不跌;② 无消除的旋转炸弹作废、累计值保持;③ 触发盘(进 FS 那一旋)的炸弹**不带入** FS 累计器。 + +> **推论(决定了"每局能演几次消除")**:n 次"消除+炸弹"的累计花费 ≈ `0.125 × 平均炸弹值 × n²` 倍底注, +> 所以**炸弹越小,买得起的消除次数越多**(单个球值 b、n 次连爆 ≈ 0.125·b·n² ≤ 该局 FS 预算)。 +> 实测(买FS 售价 50 倍、RTP 80%、桶 7~17): +> +> | 桶(抽中率) | FS 预算 | 每局消除旋数 | 每局炸弹并入 | 局末累计中位 | +> |---|---|---|---|---| +> | 10x(35.6%) | 7 | 5.3 旋 | 3.92 次 | 15 | +> | 16x(23.4%) | 13 | 5.2 旋 | 3.97 次 | 16 | +> | 25x(16.1%) | 22 | 5.3 旋 | 4.21 次 | 20 | +> | 40x(9.2%) | 37 | 4.9 旋 | 3.62 次 | 16 | +> | 100x(3.8%) | 97 | 4.7 旋 | 3.44 次 | 17 | +> | 150x~1000x(4.5%) | 147+ | 3~6 旋 | 2~5 次 | 26~258 | +> +> **炸弹值按档位配**(`FS_SMALL_BOMB_MAX=8` / `FS_BIG_BOMB_TARGET=100`):低倍档(<100x)只出 2~8 的小球 +> ——大球一合并,后面每次消除都被放大几十倍,低倍档预算立刻见底;高倍档(≥100x)用全量 2~1000, +> 所以"大炸弹"照样存在,只是它出现的那些局自然落在高倍桶。 +> 每屏最多 1 颗(`FS_MAX_BOMBS_PER_SPIN`),玩家才看得出"消一次爆一颗、倍数一颗颗涨"。 +> > 2026-09 试过一版"按桶分档"(低倍局只用 2~4、自然路径放开到 1000):低倍桶消除 3.65→4.29 次/局, +> > 但高倍桶出现大球后**消除次数反而掉**(1000x 桶 13→8.7 次/局)—— 一局赔付 ≈ 系数×k²、桶目标固定, +> > 大球把后面每次消除都放大,同样的钱只够买更少的消除。用户定调"消除多优先",故已回退; +> > 实测数据与折中做法(如"每局最多一颗大球""大球只给 300x+")留在 `docs/玫瑰女郎炸弹倍数分档方案.md`。 + +**推论(改生成器时必须知道)**:累计器只在"本旋有消除"时才吃炸弹,而炸弹均值 ≈25 倍(`w = 1/√v` 权重)→ **一次带炸弹的消除就把累计倍数推到几十上百**,此后任何一次小消除都被放大成 7.5~50。所以"某局 FS 能容纳多少次消除"完全由该局的原始分预算决定: + +| buy_fs 桶 | 触发盘白送 | FS 可用预算 | 保底上限 | +|---|---|---|---| +| 4x(3.00~5.00) | 3.00 | ~1.0 | 约 2~4 次消除 | +| 6.5x(5.00~8.00) | 3.00 | ~3.5 | 约 8~14 次 | +| 10x(8.00~12.00) | 3.00 | ~7.0 | 每旋都能消 + 连消 | + +> **buy_fs 的可用桶已改为 10x~1000x(桶 7~17)**:4x/6.5x 桶整局预算只有 1~3.5 倍底注, +> 演不出"15 旋里十来旋都在消 + 炸弹累计",玩家看到的一直是空转动画。 +> 改动点:Java `OlympusModeType.getOrnamentRange()` 的 `BUY_FS` → `{7,17}`; +> py `MODE_ORNAMENT_IDS['buy_fs']` → `range(7,18)`;后台「一键重算」重配 count +> (py 侧可跑 `python _recalc_boxes.py --mode buy_fs` 复刻同一算法)。 +> 实测观感(box21,玩家加权):每局 **4.5 旋**有消除、**1.16 次**炸弹并入累计器、低倍局累计倍数中位 **9**(改前分别约 3.2 旋 / 0.4 次 / 0)。 +> 注意:新配比下 buy_fs 的实际 RTP 会落在 **~81.5%**(10x 桶占 69%,该桶实际均值 10.97 > 名义 10.0);要压回 80% 就把这 10 个箱的 rtp 设成 0.785 再重算。 +> **(已修正,见第六节"配比口径")** 现在配比不再按名义 expect_mul 算,而是按每个箱子池内 `AVG(actual_mul)` 算, +> 「设 80% → 实际就赔 80%」,不再需要手改 rtp 这种补偿手段。 + +--- + +## 四点五、免费旋转"每旋保底消除"(三模式统一) + +> 动机:旧实现 30 格里自然凑出 8 连的概率只有 **17%**,再叠 45% 强制空转,导致 **86.8% 的 FS 旋完全空转**(一局 15 旋里平均只有 2.4 旋有消除),观感"每轮都不消"。 +> 现在 normal/ante/buy_fs **只要进了 FS 走的就是同一条代码路径**(`_free_spin_session`),区别只在"这一局值多少钱"。 + +| 参数 | 值 | 作用 | +|---|---|---| +| `FS_SPIN_WIN_PROB` | 0.60 | 不限额时每旋"至少消一波"的基础概率 | +| `FS_CASCADE_CONTINUE_PROB` | 0.10 | 消完一波后继续连消的概率(越低,同样预算能铺到越多旋) | +| `FS_CASCADE_MAX` | 5 | 单旋最大消除波数 | +| `FS_GUARANTEE_COMBOS` | 8 连 I/H/G | 保底用最小档消除(0.25/0.4/0.5 倍底注),不覆盖已有 S/M 格 | +| `FS_GUARANTEE_COST` | 0.45 | 每多一次"保底消除+炸弹累计"的边际赔付(≈0.45×k,见下) | +| `FS_DRY_FORCE_PROB` | {5:0.5, 6:0.85} | 连续空转到第 5/6 旋时"按概率补保底"(软兜底,避免长空尾) | +| `FS_DRY_FORCE_HARD` | 8 | 连续空转到这个旋数无条件补 | +| `FS_FLOOR_SPEND` | ≈0.28 | 保底预算的换算口径(0.25 / (1−0.10)) | +| `FS_FLOOR_BUDGET_CAP` | ≈4.17 | 一局的保底预算上限(15 旋 × 0.28) | + +**预算规则**(`generate_round`): + +| 场景 | 预算 | 说明 | +|---|---|---| +| buy_fs / 高倍桶补桶(`bias` = 桶目标) | `保底预算 = min(上限, 目标 − 触发盘赢额)`,差额给"一旋大奖"(`_force_win`) | 保证每旋在消又不打穿低倍桶 | +| buy_fs 自然阶段(bias=None) | 不限额 → 每旋 60% 保底 | 15 旋全在消(累计器会把整局抬到几百倍,所以只出现在高倍桶) | +| **normal/ante 自然触发 FS** | 固定 `FS_FLOOR_BUDGET_CAP`(无大奖旋) | 免费摇出来的没有价格可拆,保底预算 + 炸弹累计自然结算 | + +- **保底消除"配额 + 随机间隔"(2026-09 修正)**:一局能买几次消除由预算反推 + `k = max(1, √(保底预算 / FS_GUARANTEE_COST))`;每次消除(自然或保底)后**重抽下一次的位置** + (`_fs_next_target`:间隔 = `round(剩余旋数/剩余配额 + δ)`,δ∈{-2..2} 权重 {1,2,3,2,1}, + 均值 0 → 整局消除次数不变、位置随机);空转太久再按 `FS_DRY_FORCE_PROB` 软兜底(第 5/6 旋 + 0.5/0.85,第 8 旋必补),防止配额提前用完留下长空尾。 + > **为什么要改成随机**:先是"等距铺"(`due += k/总局数`,到点必消),虽然没有长空尾,但间隔被 + > 压在 1~5 旋、**87% 的局"最长连空正好 4 旋"**,玩家几局就看穿"到第 4 旋肯定消"。随机化后 + > 相邻两次消除的间隔散成 1~7 旋(35/20/14/11/8/8/3),每局最长连空 1~8 旋散开(单值最高 41%); + > 硬闸版本(到第 N 旋必消)会留下新的固定天花板(硬顶 6 时 42% 的局正好卡在 6),所以兜底必须是 + > **概率递增**而不是硬上限。 + > **为什么要改(上一版)**:旧写法 `p = min(1, 剩余预算 / (FS_FLOOR_SPEND × 剩余旋数))` 里扣预算 + > 扣的是**已被累计倍数放大后的赢额**(`win = 基础分 × max(1, 累计+本旋炸弹)`),累计器一涨,几次 + > 保底就把预算烧穿 → `p = 0` → 后半局再也不会消。全库实测(`python _regen.py fsdry`):买 FS + > **87.9%** 的局出现 ≥4 旋连空、**51.7%** 的局一直空转到结束、单局最长 **29 旋**;natural 触发 + > FS 更高(96~97%)。修完同一份预算下:买 FS 有消除 **4.2 → 4.8 旋/局**、最长连空 ≥7 旋的局 + > **61% → 2%**、单局最长 29 → 11 旋、平均最长连空 9.6 → 4.5 旋。 +- **物理上限(调生成器前必须知道)**:k 次消除的整局赔付 ≈ `FS_GUARANTEE_COST × k²`(每次消除都把炸弹并进 + 累计器,倍数作用到之后每一次消除 → 平方增长)。所以 10x 桶(预算≈7)只买得起 **~4 次消除**; + 想再多消就得靠"少出炸弹",但那样赔付地板会抬高、这些回合不再落在低倍桶(实测 10x 桶直接填不满)。 +- **炸弹值按档位配**(`spin_bomb_max`):目标 < `FS_BIG_BOMB_TARGET`(100) 用 2~8 的小球(预算买得起多次"消除+爆"),目标 ≥100 用全量 2~1000。 + > 想给低倍桶更多消除**不能靠"把球做小"**:球小确实省钱,但低倍桶变成"小球多消"后赔付地板抬高、 + > 命中率下降(实测 10x 桶只填到 117/292,且高倍桶一旦出大球消除次数反掉)—— 详见 `docs/玫瑰女郎炸弹倍数分档方案.md`。 +- **保底消除种在"刚落下/刚补位"的格子上**(`_force_small_win(grid, prefer=补位格)`):这些格子本来就有 `drop_data`/`fill_data` 记录,前端按动画数据落地就能与 `grid_state` 完全一致;`drop_data`/`fill_data` 里的 `symbol` 也统一按**最终盘面**回填(`_spin` 里补位、保底之后统一同步一次)。 + > 改前的老数据里,保底消除会随机改写**没移动过**的格子 → 那类格子没有任何动画记录,前端只能靠 `grid_state` 兜底(不会出现空格,但符号要等这一步渲染完才对齐)。全库体检:`grid_state` 空白格 0、`fill_data` 漏格 0、`drop_data` 与重力不符 0、drop/fill 符号与盘面不符 0;老数据剩 10,603 格属于上述"原地改写"。 + +**验收口径**:`python _regen.py fsdry [box_id]` 打印每模式/每桶的"有消除旋数、最长连空分布、≥4 连空占比" +(买 FS 的 ≥4 连空一列按后台配比加权 = 玩家实际遇到的概率),并列出最差局;单测 +`tests/test_simulator.py::TestFreeSpinFloorSpread` 锁住"平均最长连空 < 8、不出现十几旋死尾巴"。 + +实测(box1:natural 触发 FS 池内 49 个回合,`_regen.py fsstats 1`):每局 **5.1~5.5 旋**有消除、**3.9~4.0 次**炸弹并入累计器、局末累计倍数中位 **15~16**,落桶与名义值基本贴合(桶 12 实测 115.3 vs 名义 100、桶 13 实测 147.8 vs 150)。 +买 FS(`_regen.py fsstats 21`,按配比加权):每局 **4.6 旋**有消除、**3.16 次**炸弹并入;连空体检 `_regen.py fsdry` 见四点五。 + +--- + +## 五、爆奖动画(小/中/大 3 档) + +> 判定口径:**只在免费旋转里判档**(normal/ante 靠自然触发 FS、buy_fs 本身即 FS), +> **按单旋赢额独立判档**,达标即在该旋末帧盖章,旋与旋互不影响,**同一档一局可多次触发**; +> 基础旋(`spinNum=0`)永远是 `null`,不播大奖动画。 + +档位编码:`small < middle < big`(小 / 中 / 大)。 + +阈值表(单位:单旋赢额 ÷ 基础下注,配置于 `BIGWIN_TIERS`)——**两种情况两套阈值**: + +| 情况 | 模式 | small | middle | big | +|---|---|---|---|---| +| ① 自然免费旋转 | `normal` / `ante` | ≥5 | ≥25 | ≥100 | +| ② 购买免费旋转 | `buy_fs` | ≥20 | ≥60 | ≥200 | + +为什么两套:normal/ante 的自然 FS 预算与旋数完全一致(整局约 80~130x),所以共用一套; +buy_fs 花 50 倍买来的赢额分散在 15+ 旋里,单旋阈值整体下调(否则连"小"都很难出现)。 + +实测出现频率(2026-09 重跑后的池子,逐局统计): + +| 情况 | small | middle | big | +|---|---|---|---| +| ① 自然 FS(normal 519 局 / ante 818 局) | 91.7% / 93.5% 的局出现(每局 2.3~2.5 次) | 49.3% / 48.8%(每局 0.6 次) | 31.4% / 39.0%(每局 0.3~0.4 次) | +| ② 买 FS(12205 局) | 19.5% 的局出现(每局 0.47 次) | 6.3%(每局 0.13 次) | **1.5%**(每局 0.02 次) | + +> 买 FS 的"小"只覆盖 19.5% 的局:它门槛是"单旋 ≥20 倍"(= 售价的 0.4 倍),而 10x/16x 桶整局才 10~20 倍,所以那些局不会有任何爆奖动画。 +> 想让"小"更常见就把 `BIGWIN_TIERS['buy_fs']` 的第一档调低(比如 10),改完重跑数据即可。 + +--- + +## 六、饰品桶(17 档)与匹配/发放分离 + +`ORNAMENTS` 定义 17 个倍率桶(0x ~ 1000x),`expect_mul` 严格 = `(range_low + range_high) / 2`,区间连续无缝。 + +**匹配 vs 发放分离**(顶桶特例): +- 17 号桶(展示名 `1000x`)匹配区间 **630~1370**,中点/期望 = 1000; +- 但实际发放**封顶 1000 倍**:回合理论总分落在 1000~1370 仍归此桶,发钱时截断到 1000 并标记 `win_capped`。 +- 即:**落桶用理论分(≤1370),发钱用截断值(≤1000)**,两层各司其职。 + +### 六点五、配比口径:按"池内实际会赔多少"算(2026-09 修正) + +> 以前的配比(`count`)是按**名义** `expect_mul`(区间中点)反推的,于是「设 80% → 实际赔 81.8%」这种系统性偏差: +> 桶内实际均值跟区间中点差很多(实测 **-18% ~ +11%**),桶 17 又受 1000x 封顶影响实际只有 **~835**(不是 1000)。 + +现在 Java `OlympusDistributionService` 与 py `_recalc_boxes.py` 都改成: + +| 环节 | 口径 | +|---|---| +| 配比 `calcRounds` | 期望值取 **该箱池内 `AVG(actual_mul)`**(`loadPoolExpect(boxId)`;某桶池子里一条回合都没有时才回退名义值) | +| 后台「目标RTP」列 | 同一个口径反推 = `Σ(count × 池内均值) / Σcount / cm` —— 也就是**这个箱子真实会赔多少** | +| 抽回合的 payoutCap 过滤 | 仍用名义 `expect_mul`(`OlympusRoundPicker` 读 DB,偏保守,保留安全余量) | +| 微调 | 从"只在最大的 3 个桶之间搬 1 个"改成**任意两个桶搬 1 个**(保持单调),否则池内均值口径下根本走不到目标,会差 0.5~1 个百分点 | + +效果(30 个箱全部重算后实测):每个箱的期望赔付 **79.97% ~ 80.02%**(目标 0.8),`simulate_cycle.py` 整轮收敛 80.0%。 +注意:池子重新生成过之后要**再跑一次重算**(配比是跟着池子里的这批回合走的);重算会把 `used` 归零, +Redis 的本轮账本要一起清(后台「一键重算」会清;py 侧 `_recalc_boxes.py` 只动库,跑完手动清, +`ruoyi:olympus:cycle:{boxId}:{audience}`),否则「实时RTP」显示的是旧抽数+新抽数混在一起的值。 + +--- + +## 七、多身份爆率(audience) + +参照现有宝箱的"普通/主播/机器人"爆率,olympus 同样支持多身份差异化 RTP。但实现方式不同: + +| | 现有宝箱 | olympus | +|---|---|---| +| 爆率载体 | 每饰品配多列 odds(real/anchor/robot) | 每饰品桶的 `count`(按 audience 分套) | +| 开奖方式 | 开箱实时按身份 roll 饰品 | 开局按身份桶分布抽一个**预生成回合** | + +**核心:回合库存池(round/step)完全共享,仅“抽哪个桶”的概率按身份区分。** 四档身份(对齐 `tt_user`): + +| audience | 对应身份 | 识别字段 | +|---|---|---| +| `common` | 普通用户(及招商/卡商/超管等) | user_type=02 及其他 | +| `anchor` | 主播 | user_type=01 | +| `robot` | 机器人 | user_type=03 | +| `white` | 白号 | is_blank_account=1(**压过 user_type**) | + +**身份解析优先级(Java 开局)**:白号最高优先——`is_blank_account==1 → white`(不管哪个 user_type);否则 01→anchor / 03→robot / 其余→common。 + +实现: +- `game_olympus_box_ornament` 带 `audience` 列,唯一键 `(box_id, ornament_id, audience)`,每身份一套 `count`/`used` 分布; +- 生成器写库时四档各写一套(首版相同,后台可差异化调各桶 count); +- Java 开局时按优先级选 audience 分布抽回合(回合池共享,无需重生)。 + +**白号池(white)专项规则**(消耗隔离——白号不碰其他人奖池): +- 独立有限库存:白号抽取只递减 `white` 自己的 `used`、抽干独立补桶,**永不消耗 common/anchor/robot 库存**(由 audience 维度天然隔离); +- white 桶范围与 common 一致,默认分布相同,运营可单独调; +- 水位隔离:白号 bet/win **不计入水位**(对齐 bullet); +- 白号赢奖不进真实经济(提货虚假成功、不计充值/主播收益,与现有白号语义一致)。 +- 机器人暂不细分幸运/非幸运,后续视 2v2 需求再扩展。 + +--- + +## 八、模块 API 说明 + +### 8.1 `olympus/config.py` — 规则配置(唯一事实来源) + +| 名称 | 类型 | 说明 | +|---|---|---| +| `SymbolRule` | dataclass | 符号赔付规则,`payout_for(count)` 按 8/10/12 档取倍率 | +| `SYMBOL_RULES` | dict | 9 种普通符号赔付表 | +| `SCATTER_PAYOUTS` | dict | `{6:100, 5:5, 4:3}` | +| `SCATTER_FREE_SPIN_TRIGGER` / `SCATTER_RETRIGGER_TRIGGER` | int | 4 / 3 | +| `FREE_SPINS_AWARDED` / `FREE_SPIN_RETRIGGER_COUNT` / `MAX_FREE_SPINS` | int | 15 / 5 / 60 | +| `MULTIPLIER_VALUES` | frozenset | 16 个合法炸弹倍数 | +| `MAX_WIN_MULTIPLIER` | Decimal | 1000(封顶) | +| `GameMode` / `MODES` / `DEFAULT_MODE` | dataclass/dict | 三模式定义,`price_for(bet)` 算扣费 | +| `ORNAMENTS` | list | 17 档饰品桶 | +| `GRID_ROWS/COLS/SIZE` | int | 5 / 6 / 30 | +| `SYMBOL_SHORT` / `SHORT_TO_SYMBOL` | dict | 符号↔短码(A~I, S, M)互转 | +| `SYMBOL_WEIGHTS` / `BOMB_*` | - | 生成器随机权重、炸弹概率/品质 | +| `BOMB_QUALITIES` / `bomb_quality(v)` | list/func | 炸弹 5 档品质,值→品质编码 | +| `BASE_FS_TRIGGER_PROB` / `*_SCATTER` | dict | FS 触发与 SCATTER 散布分布 | +| `MODE_ORNAMENT_IDS` | dict | 各模式可用桶范围 | +| `BIGWIN_TIERS` / `bigwin_level(mode, mul)` | dict/func | 爆奖小/中/大 3 档阈值与判档(两种情况两套阈值) | + +### 8.2 `olympus/models.py` — 数据模型(全部 frozen dataclass) + +| 模型 | 关键字段 | +|---|---| +| `Bomb` | `bomb_id, value, position` | +| `Elimination` | `symbol, count, positions, payout_multiplier` | +| `CascadeResult` | `cascade_index, eliminations, bombs, base_win, accumulated_symbols` | +| `SpinResult` | `spin_index, cascades, base_win, multiplier_before/after, bombs_added, effective_multiplier, total_win, accumulated_symbols, win_capped` | +| `SessionState` | `next_spin_index, accumulated_multiplier, total_win, spins`;`reset()` | + +### 8.3 `olympus/engine.py` — `OlympusEngine` 结算状态机 + +规则的**可执行规范**(一局 = 一次付费;一 spin 内可多次 cascade): + +```python +engine = OlympusEngine(bet=10, mode="normal") # 校验模式与档位 +engine.begin_spin(is_free_spin=False) # 开旋(FS 需传 True 才启用累计器) +engine.record_cascade(symbols={"CROWN": [...]}, bombs=[Bomb("m1", 5)]) # 逐波消除 +result = engine.end_spin() # 末帧统一引爆 + 封顶 → SpinResult +``` + +关键方法:`buy_free_spins_allowed()` / `ante_boost_active()` / `reset_session()`;非法操作抛 `RuleViolation`。 + +--- + +## 九、数据库表(前缀 `game_olympus_`,共 12 张) + +| 表 | 用途 | +|---|---| +| `box` | 箱子配置(模式×档位,`box_type/bet/price`) | +| `ornament` | 17 档饰品桶定义(种子随 schema 写入) | +| `box_ornament` | 箱子-桶数量分布(`count/used`,后台调此控 RTP) | +| `round` | 预生成回合(`ornament_id/actual_mul/free_spin_count/total_cascades`) | +| `step` | 级联步骤明细(见下方字段) | +| `record` | 玩家游戏记录(自带 `settled/结算/补发`字段) | +| `user` / `win` | 用户统计 / 每日盈亏 | +| `prize_ornament` / `prize_range` | 返奖饰品定义与区间 | +| `box_template` | 箱子分布模板(抽干补桶依据) | +| `reward_log` | 奖池水位调整日志(注水/抽水/重置审计,含操作后总池余额) | + +**`step` 表核心字段**(前端播放 + 审计): + +| 字段 | 含义 | +|---|---| +| `grid_state` | 本步 6×5 网格最终状态 | +| `multiplier_detail` | 炸弹 `[{pos,val,quality}]` | +| `symbol_links` | **本旋累积**连线 `[{symbol,count,loc,base_score,tumble}]`;`tumble` = 旋内第几波,**接口只下发这个字段**(本波新增 = `tumble` 等于本步 `cascade_seq` 的那些;获胜者面板按 symbol 汇总) | +| `drop_data/fill_data` | 下落/填充动画数据 | +| `scatter_trigger/retrigger_info` | 触发/重触发免费旋转信息(仅末帧) | +| `accumulator_before/after` | 炸弹累计器前后值(跨旋传递);`after` **已下发**为接口字段 `accumulator`(未 ×bet,断线重连接回累计用),`before` 仍仅落库留档 | +| `win_amount` | **本旋累计到本步的赢额**(该旋末帧即这一旋的总额;不是本步增量) | +| `spin_win` | **本局累计到本步的赢额**(最后一步 = 整局实付 = 回合 `actual_mul`) | +| `win_capped` | 本步是否被 1000x 截断 | +| `bigwin_level` | 爆奖档位 `small/middle/big`(按单旋赢额,只在免费旋转里判档,盖在该旋末帧;基础旋恒为 null) | +| `is_final` | 本旋末帧标记 | +| `spin_winners` / `accumulated_symbols` / `removed_positions` / `base_score` | **仅落库留档,接口已不下发**(都能由 `symbol_links` 推出:面板=按 symbol 汇总、消除格位=上一步 `loc` 并集、基础分=`base_score` 之和) | + +--- + +## 十、回合生成器(`olympus_simulator.py` + `generate_rounds.py`) + +流程:`generate_round(box, bias)` → 生成 cascade 链 → 自然累计 → 落桶 → 发放截断。 + +- **职责边界**:生成器只负责"库存多样性"(每桶预生成足量各异回合),**不做 RTP 收敛**——RTP 由后台配 `box_ornament.count` 控制。 +- `fill_distribution(box, counts)`:两阶段填桶(自由生成 + 缺额加权 bias 补桶),保证每桶命中目标数量。 +- 关键实现:炸弹位置随重力下落同步重映射;免费旋转会话 15 起、≥3S 重触发 +5;`bias ≥ FS_ONLY_TARGET_MUL(80x)` 时改用"自然触发 FS"的回合来装该桶(见第三点五)。 + +--- + +## 十一、运行方式 + +> 环境:Python ≥ 3.9,依赖 `pymysql`(仅写库时需要)。DB 连接走环境变量 `OLY_DB_*`,默认对齐 `docker-compose.yml`(`127.0.0.1:23306 / changzhou`)。 + +```bash +# 建表 + 写入饰品桶种子 +python olympus_db.py + +# 干跑:三模式各桶抽样,验证多样性/封顶(不写库) +python generate_rounds.py --per-bucket 20 + +# 全部 30 箱写库(推荐:池子按后台配比×1.5 分配,见下节"池子怎么分配") +python generate_rounds.py --all --alloc config --write + +# 备用:按每箱固定总回合跨桶均分(旧行为,正式发布可用 --per-box 2000) +python generate_rounds.py --all --per-box 2000 --write + +# 单箱写库 +python generate_rounds.py --box-type normal --bet 1 --alloc config --write + +# 重跑前的安全流程(快照→清空→生成→校验;--alloc config 会自动保留配比,快照只作兜底) +python _regen.py snapshot && python _regen.py clear +python generate_rounds.py --all --alloc config --write # 保底消除改"随机间隔"后低倍桶命中率约减半, + # fill_distribution 尝试上限已从 ×30 提到 ×60, + # 全库重跑耗时约翻倍(30 箱 ≈45 分钟~1 小时) +python _regen.py verify +python _regen.py fsdry # 免费旋转连空体检(见四点五) +python _recalc_boxes.py --mode normal && python _recalc_boxes.py --mode ante && python _recalc_boxes.py --mode buy_fs +# 重算只动库里的 used,不清 Redis 账本 → 手动清 ruoyi:olympus:cycle:{boxId}:{audience}, +# 或后台对每个箱点一次「一键重算分布」;再跑 python _make_templates.py 刷新模板 + +# 重置(改 box_id 方案前用):清空 box/box_ornament/round/step/box_template +python _reset_boxes.py + +# 建奖池水位调整日志表(对齐 bullet 审计) +python _create_reward_log.py + +# 规则回归测试(25 项) +python tests/test_engine.py +``` + +**box_id 规则**:`--all` 按模式顺序×档位顺序**从 1 顺序编号**(normal 1~10、ante 11~20、buy_fs 21~30,共 30 箱)。 + +**池子怎么分配(重要)**:用 `--alloc config` —— **每桶池子 = 后台配比 × 1.5**(下限 20 条),而不是跨桶均分: + +| 方式 | 效果 | +|---|---| +| `--per-box N`(旧,均分) | 每桶 N/桶数 条;normal 0x 桶只有 77 条,而配比一个 cycle 要发 548 次 → 同一条回合被反复发给玩家(**7.1×**) | +| `--alloc config`(推荐) | 每桶 ≥ 配比;normal 0x 桶 837 条(配比 563)、buy_fs 低倍桶 447/280/174 条(配比 297/186/118)→ 每个回合一个 cycle 内最多发一次 | + +`--alloc config` 还会把**现有配比原样写回**(不会把池子形状覆盖成 count),所以不需要再手工还原配比。 +池子形状与配比的对应关系写死在 `generate_rounds.py` 的 `ALLOC_K=1.5` / `ALLOC_FLOOR=60`,要调就改这两个常量。 + +> **为什么下限是 60(2026-09 调)**:池子 = `max(ALLOC_FLOOR, 配比×1.5)`,下限只作用在**配比 ≤13 的桶(6.5x 以上)**。 +> 这几个桶条数少、盘面又显眼(有赢奖/大奖动画),是玩家真正会"看着眼熟"的;0x~4x 虽然复现间隔最短 +> (约 4,500 抽一次),但空转/小赔的盘面没人记得住,所以不值得为它们整体加大池子(那就得动 `ALLOC_K`, +> 池子 +64%、重跑时间 ×1.7)。下限 20→60 只让池子 +8%,把 6.5x 以上的复现间隔拉长 3 倍: +> 10x 桶 5,455→16,364 抽、100x 桶 6 万→18 万抽(按全服每天 5,000 抽算,10x 从"几乎每天重"变成"三天重一次")。 +> 复现间隔 = `池子条数 ÷ 该桶配比 × 一轮抽数`,调这两个常量前建议照这个式子算一遍。 + +> 端侧配合:Java `OlympusRoundPicker` 已改为**同一玩家最近 30 条回合不再重发**(Redis List `ruoyi:olympus:seen:{userId}:{boxId}`,`LPUSH`+`LTRIM`),抽回合时从 40 条候选里过滤。 +> 两者叠加的效果:玩家在池子耗完前**不会看到重复盘面**;只靠加大池子做不到(要 ~1.4GB 数据且仍有 30% 重复)。 + +### 抽完补池(refill)与"实时 RTP 是否达标"仿真 + +一个 cycle 抽满后(`drawn ≥ N = fixedSum/(1−收敛比例)`),Java `OlympusGameService` 会 `resetCycle` + `refillFromConfig(boxId, audience)`: + +| 情况 | 行为 | +|---|---| +| 该 (box, audience) 有**生效模板**(`is_active=1`,取 id 最大一条) | 按模板 `items_json` 重写 count(used 归 0),并按固定池反推回写 `total_ornaments` | +| 没有模板 | 只把 `used` 归零(配比不变,等价于"填满"但形状不重置) | + +建模板:`python _make_templates.py`(30 箱 × 4 身份 = 120 条生效模板,内容=当前配比;会先把旧的测试模板置为不生效)。 + +仿真(复刻 Java:固定阶段按 remaining 加权 + 收敛阶段 `desiredNextMul` + 水位 `payoutCap` + 抽完按模板重填): + +``` +python simulate_cycle.py # 全部 30 箱 × common,3 个 cycle(水位跨 cycle 持久) +python simulate_cycle.py --audience white # 白号(payoutCap=null,不进水位) +python simulate_cycle.py --box 21 --cycles 5 +python simulate_cycle.py --cr 0.1 # 试不同收敛比例(只在仿真里覆盖,不写库) +``` + +实测(rtp=80%、收敛比例=20%,30 箱 × 3 轮 × 1000 抽): + +| 收敛比例 | 固定池末偏差 max/均值 | 整轮末偏差 max/均值 | 最差单轮 | +|---|---|---|---| +| 5% | 4.93% / 1.53% | 2.98% / 0.24% | 83.0% | +| 10% | 6.55% / 1.65% | 1.78% / 0.08% | 81.8% | +| 15% | 7.20% / 1.79% | 0.50% / 0.04% | 80.5% | +| **20%(当前)** | 6.36% / 1.58% | **0.13% / 0.03%** | 79.9% | +| 30% | 6.01% / 1.65% | 0.09% / 0.02% | 79.9% | + +结论:固定阶段自身 ±6% 波动("配比即爆率"),全靠收敛阶段拉回目标;**收敛比例不要低于 15%**。 +水位只在冷启动时起作用(水位 0 → `payoutCap=(0+成本)×rtp`,首轮大奖被压住,导致第 1 轮固定池末偏低 77~78%,但整轮仍达标)。 + +--- + +## 十二、端到端数据流 + +``` +config.py(规则配置,唯一事实来源) + │ + ├─► engine.py 被 test_engine.py 回归验证(规则正确性) + │ + └─► olympus_simulator.py 生成 round + step + │ + ▼ + generate_rounds.py 按桶批量生成 + │ + ▼ + olympus_db.py → MySQL(game_olympus_round / _step / _box_ornament) + │ + ▼ + (后续)Java 服务端消费步骤数据 → dospin 逐步回放 + 结算发奖 +``` + +两层职责:**Python 预生成回合/步骤数据落库**,**Java 服务端实时回放与结算**(对齐 sweet 体系)。 diff --git a/olympus-py/_backup_box_config.json b/olympus-py/_backup_box_config.json new file mode 100644 index 0000000..d1e4d45 --- /dev/null +++ b/olympus-py/_backup_box_config.json @@ -0,0 +1,13889 @@ +{ + "ts": "2026-09-18T09:44:47", + "game_olympus_box": [ + { + "box_id": "1", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "1.00", + "name": "普通旋转-bet1", + "price": "1.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:15", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "2", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "3.00", + "name": "普通旋转-bet3", + "price": "3.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:17", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "3", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "5.00", + "name": "普通旋转-bet5", + "price": "5.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:18", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "4", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "10.00", + "name": "普通旋转-bet10", + "price": "10.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:20", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "5", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "20.00", + "name": "普通旋转-bet20", + "price": "20.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:22", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "6", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "30.00", + "name": "普通旋转-bet30", + "price": "30.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:24", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "7", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "50.00", + "name": "普通旋转-bet50", + "price": "50.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:26", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "8", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "100.00", + "name": "普通旋转-bet100", + "price": "100.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:28", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "9", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "200.00", + "name": "普通旋转-bet200", + "price": "200.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:29", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "10", + "game_type": "OLYMPUS", + "box_type": "normal", + "bet": "300.00", + "name": "普通旋转-bet300", + "price": "300.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:31", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "11", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "1.00", + "name": "底注-bet1", + "price": "2.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:33", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "12", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "3.00", + "name": "底注-bet3", + "price": "6.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:35", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "13", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "5.00", + "name": "底注-bet5", + "price": "10.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:37", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "14", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "10.00", + "name": "底注-bet10", + "price": "20.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:39", + "update_time": "2026-09-15 14:49:22" + }, + { + "box_id": "15", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "20.00", + "name": "底注-bet20", + "price": "40.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:42", + "update_time": "2026-09-15 14:49:23" + }, + { + "box_id": "16", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "30.00", + "name": "底注-bet30", + "price": "60.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:44", + "update_time": "2026-09-15 14:49:23" + }, + { + "box_id": "17", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "50.00", + "name": "底注-bet50", + "price": "100.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:46", + "update_time": "2026-09-15 14:49:23" + }, + { + "box_id": "18", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "100.00", + "name": "底注-bet100", + "price": "200.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:48", + "update_time": "2026-09-15 14:49:23" + }, + { + "box_id": "19", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "200.00", + "name": "底注-bet200", + "price": "400.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:50", + "update_time": "2026-09-15 14:49:23" + }, + { + "box_id": "20", + "game_type": "OLYMPUS", + "box_type": "ante", + "bet": "300.00", + "name": "底注-bet300", + "price": "600.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:35:52", + "update_time": "2026-09-15 14:49:23" + }, + { + "box_id": "21", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "1.00", + "name": "购买免费旋转-bet1", + "price": "50.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:36:01", + "update_time": "2026-09-17 15:16:51" + }, + { + "box_id": "22", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "3.00", + "name": "购买免费旋转-bet3", + "price": "150.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:36:11", + "update_time": "2026-09-15 16:23:00" + }, + { + "box_id": "23", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "5.00", + "name": "购买免费旋转-bet5", + "price": "250.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:36:21", + "update_time": "2026-09-15 16:24:00" + }, + { + "box_id": "24", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "10.00", + "name": "购买免费旋转-bet10", + "price": "500.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:36:32", + "update_time": "2026-09-15 16:25:02" + }, + { + "box_id": "25", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "20.00", + "name": "购买免费旋转-bet20", + "price": "1000.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:36:42", + "update_time": "2026-09-15 16:25:59" + }, + { + "box_id": "26", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "30.00", + "name": "购买免费旋转-bet30", + "price": "1500.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:36:52", + "update_time": "2026-09-15 16:27:05" + }, + { + "box_id": "27", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "50.00", + "name": "购买免费旋转-bet50", + "price": "2500.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:37:03", + "update_time": "2026-09-15 16:28:12" + }, + { + "box_id": "28", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "100.00", + "name": "购买免费旋转-bet100", + "price": "5000.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:37:13", + "update_time": "2026-09-15 16:29:17" + }, + { + "box_id": "29", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "200.00", + "name": "购买免费旋转-bet200", + "price": "10000.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:37:23", + "update_time": "2026-09-15 16:30:22" + }, + { + "box_id": "30", + "game_type": "OLYMPUS", + "box_type": "buy_fs", + "bet": "300.00", + "name": "购买免费旋转-bet300", + "price": "15000.00", + "total_ornaments": "3000", + "rtp": "0.8000", + "rtp_converge_ratio": "0.200", + "sort": "0", + "status": "1", + "del_flag": "0", + "remark": null, + "create_time": "2026-08-01 14:37:32", + "update_time": "2026-09-15 16:31:36" + } + ], + "game_olympus_box_ornament": [ + { + "id": "77350", + "box_id": "1", + "ornament_id": "1", + "audience": "common", + "count": "1352", + "used": "0" + }, + { + "id": "77351", + "box_id": "1", + "ornament_id": "2", + "audience": "common", + "count": "586", + "used": "0" + }, + { + "id": "77352", + "box_id": "1", + "ornament_id": "3", + "audience": "common", + "count": "256", + "used": "0" + }, + { + "id": "77353", + "box_id": "1", + "ornament_id": "4", + "audience": "common", + "count": "112", + "used": "0" + }, + { + "id": "77354", + "box_id": "1", + "ornament_id": "5", + "audience": "common", + "count": "48", + "used": "0" + }, + { + "id": "77355", + "box_id": "1", + "ornament_id": "6", + "audience": "common", + "count": "22", + "used": "0" + }, + { + "id": "77356", + "box_id": "1", + "ornament_id": "7", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "77357", + "box_id": "1", + "ornament_id": "8", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "77358", + "box_id": "1", + "ornament_id": "9", + "audience": "common", + "count": "4", + "used": "0" + }, + { + "id": "77359", + "box_id": "1", + "ornament_id": "10", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77360", + "box_id": "1", + "ornament_id": "11", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77361", + "box_id": "1", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77362", + "box_id": "1", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77363", + "box_id": "1", + "ornament_id": "1", + "audience": "anchor", + "count": "1352", + "used": "0" + }, + { + "id": "77364", + "box_id": "1", + "ornament_id": "2", + "audience": "anchor", + "count": "586", + "used": "0" + }, + { + "id": "77365", + "box_id": "1", + "ornament_id": "3", + "audience": "anchor", + "count": "256", + "used": "0" + }, + { + "id": "77366", + "box_id": "1", + "ornament_id": "4", + "audience": "anchor", + "count": "112", + "used": "0" + }, + { + "id": "77367", + "box_id": "1", + "ornament_id": "5", + "audience": "anchor", + "count": "48", + "used": "0" + }, + { + "id": "77368", + "box_id": "1", + "ornament_id": "6", + "audience": "anchor", + "count": "22", + "used": "0" + }, + { + "id": "77369", + "box_id": "1", + "ornament_id": "7", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "77370", + "box_id": "1", + "ornament_id": "8", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "77371", + "box_id": "1", + "ornament_id": "9", + "audience": "anchor", + "count": "4", + "used": "0" + }, + { + "id": "77372", + "box_id": "1", + "ornament_id": "10", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77373", + "box_id": "1", + "ornament_id": "11", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77374", + "box_id": "1", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77375", + "box_id": "1", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77376", + "box_id": "1", + "ornament_id": "1", + "audience": "robot", + "count": "1352", + "used": "0" + }, + { + "id": "77377", + "box_id": "1", + "ornament_id": "2", + "audience": "robot", + "count": "586", + "used": "0" + }, + { + "id": "77378", + "box_id": "1", + "ornament_id": "3", + "audience": "robot", + "count": "256", + "used": "0" + }, + { + "id": "77379", + "box_id": "1", + "ornament_id": "4", + "audience": "robot", + "count": "112", + "used": "0" + }, + { + "id": "77380", + "box_id": "1", + "ornament_id": "5", + "audience": "robot", + "count": "48", + "used": "0" + }, + { + "id": "77381", + "box_id": "1", + "ornament_id": "6", + "audience": "robot", + "count": "22", + "used": "0" + }, + { + "id": "77382", + "box_id": "1", + "ornament_id": "7", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "77383", + "box_id": "1", + "ornament_id": "8", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "77384", + "box_id": "1", + "ornament_id": "9", + "audience": "robot", + "count": "4", + "used": "0" + }, + { + "id": "77385", + "box_id": "1", + "ornament_id": "10", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77386", + "box_id": "1", + "ornament_id": "11", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77387", + "box_id": "1", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77388", + "box_id": "1", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77389", + "box_id": "1", + "ornament_id": "1", + "audience": "white", + "count": "1352", + "used": "0" + }, + { + "id": "77390", + "box_id": "1", + "ornament_id": "2", + "audience": "white", + "count": "586", + "used": "0" + }, + { + "id": "77391", + "box_id": "1", + "ornament_id": "3", + "audience": "white", + "count": "256", + "used": "0" + }, + { + "id": "77392", + "box_id": "1", + "ornament_id": "4", + "audience": "white", + "count": "112", + "used": "0" + }, + { + "id": "77393", + "box_id": "1", + "ornament_id": "5", + "audience": "white", + "count": "48", + "used": "0" + }, + { + "id": "77394", + "box_id": "1", + "ornament_id": "6", + "audience": "white", + "count": "22", + "used": "0" + }, + { + "id": "77395", + "box_id": "1", + "ornament_id": "7", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "77396", + "box_id": "1", + "ornament_id": "8", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "77397", + "box_id": "1", + "ornament_id": "9", + "audience": "white", + "count": "4", + "used": "0" + }, + { + "id": "77398", + "box_id": "1", + "ornament_id": "10", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77399", + "box_id": "1", + "ornament_id": "11", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77400", + "box_id": "1", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77401", + "box_id": "1", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77402", + "box_id": "2", + "ornament_id": "1", + "audience": "common", + "count": "1351", + "used": "0" + }, + { + "id": "77403", + "box_id": "2", + "ornament_id": "2", + "audience": "common", + "count": "587", + "used": "0" + }, + { + "id": "77404", + "box_id": "2", + "ornament_id": "3", + "audience": "common", + "count": "255", + "used": "0" + }, + { + "id": "77405", + "box_id": "2", + "ornament_id": "4", + "audience": "common", + "count": "112", + "used": "0" + }, + { + "id": "77406", + "box_id": "2", + "ornament_id": "5", + "audience": "common", + "count": "49", + "used": "0" + }, + { + "id": "77407", + "box_id": "2", + "ornament_id": "6", + "audience": "common", + "count": "22", + "used": "0" + }, + { + "id": "77408", + "box_id": "2", + "ornament_id": "7", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "77409", + "box_id": "2", + "ornament_id": "8", + "audience": "common", + "count": "6", + "used": "0" + }, + { + "id": "77410", + "box_id": "2", + "ornament_id": "9", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "77411", + "box_id": "2", + "ornament_id": "10", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77412", + "box_id": "2", + "ornament_id": "11", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77413", + "box_id": "2", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77414", + "box_id": "2", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77415", + "box_id": "2", + "ornament_id": "1", + "audience": "anchor", + "count": "1351", + "used": "0" + }, + { + "id": "77416", + "box_id": "2", + "ornament_id": "2", + "audience": "anchor", + "count": "587", + "used": "0" + }, + { + "id": "77417", + "box_id": "2", + "ornament_id": "3", + "audience": "anchor", + "count": "255", + "used": "0" + }, + { + "id": "77418", + "box_id": "2", + "ornament_id": "4", + "audience": "anchor", + "count": "112", + "used": "0" + }, + { + "id": "77419", + "box_id": "2", + "ornament_id": "5", + "audience": "anchor", + "count": "49", + "used": "0" + }, + { + "id": "77420", + "box_id": "2", + "ornament_id": "6", + "audience": "anchor", + "count": "22", + "used": "0" + }, + { + "id": "77421", + "box_id": "2", + "ornament_id": "7", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "77422", + "box_id": "2", + "ornament_id": "8", + "audience": "anchor", + "count": "6", + "used": "0" + }, + { + "id": "77423", + "box_id": "2", + "ornament_id": "9", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "77424", + "box_id": "2", + "ornament_id": "10", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77425", + "box_id": "2", + "ornament_id": "11", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77426", + "box_id": "2", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77427", + "box_id": "2", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77428", + "box_id": "2", + "ornament_id": "1", + "audience": "robot", + "count": "1351", + "used": "0" + }, + { + "id": "77429", + "box_id": "2", + "ornament_id": "2", + "audience": "robot", + "count": "587", + "used": "0" + }, + { + "id": "77430", + "box_id": "2", + "ornament_id": "3", + "audience": "robot", + "count": "255", + "used": "0" + }, + { + "id": "77431", + "box_id": "2", + "ornament_id": "4", + "audience": "robot", + "count": "112", + "used": "0" + }, + { + "id": "77432", + "box_id": "2", + "ornament_id": "5", + "audience": "robot", + "count": "49", + "used": "0" + }, + { + "id": "77433", + "box_id": "2", + "ornament_id": "6", + "audience": "robot", + "count": "22", + "used": "0" + }, + { + "id": "77434", + "box_id": "2", + "ornament_id": "7", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "77435", + "box_id": "2", + "ornament_id": "8", + "audience": "robot", + "count": "6", + "used": "0" + }, + { + "id": "77436", + "box_id": "2", + "ornament_id": "9", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "77437", + "box_id": "2", + "ornament_id": "10", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77438", + "box_id": "2", + "ornament_id": "11", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77439", + "box_id": "2", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77440", + "box_id": "2", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77441", + "box_id": "2", + "ornament_id": "1", + "audience": "white", + "count": "1351", + "used": "0" + }, + { + "id": "77442", + "box_id": "2", + "ornament_id": "2", + "audience": "white", + "count": "587", + "used": "0" + }, + { + "id": "77443", + "box_id": "2", + "ornament_id": "3", + "audience": "white", + "count": "255", + "used": "0" + }, + { + "id": "77444", + "box_id": "2", + "ornament_id": "4", + "audience": "white", + "count": "112", + "used": "0" + }, + { + "id": "77445", + "box_id": "2", + "ornament_id": "5", + "audience": "white", + "count": "49", + "used": "0" + }, + { + "id": "77446", + "box_id": "2", + "ornament_id": "6", + "audience": "white", + "count": "22", + "used": "0" + }, + { + "id": "77447", + "box_id": "2", + "ornament_id": "7", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "77448", + "box_id": "2", + "ornament_id": "8", + "audience": "white", + "count": "6", + "used": "0" + }, + { + "id": "77449", + "box_id": "2", + "ornament_id": "9", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "77450", + "box_id": "2", + "ornament_id": "10", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77451", + "box_id": "2", + "ornament_id": "11", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77452", + "box_id": "2", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77453", + "box_id": "2", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77454", + "box_id": "3", + "ornament_id": "1", + "audience": "common", + "count": "1351", + "used": "0" + }, + { + "id": "77455", + "box_id": "3", + "ornament_id": "2", + "audience": "common", + "count": "587", + "used": "0" + }, + { + "id": "77456", + "box_id": "3", + "ornament_id": "3", + "audience": "common", + "count": "256", + "used": "0" + }, + { + "id": "77457", + "box_id": "3", + "ornament_id": "4", + "audience": "common", + "count": "112", + "used": "0" + }, + { + "id": "77458", + "box_id": "3", + "ornament_id": "5", + "audience": "common", + "count": "49", + "used": "0" + }, + { + "id": "77459", + "box_id": "3", + "ornament_id": "6", + "audience": "common", + "count": "21", + "used": "0" + }, + { + "id": "77460", + "box_id": "3", + "ornament_id": "7", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "77461", + "box_id": "3", + "ornament_id": "8", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "77462", + "box_id": "3", + "ornament_id": "9", + "audience": "common", + "count": "4", + "used": "0" + }, + { + "id": "77463", + "box_id": "3", + "ornament_id": "10", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77464", + "box_id": "3", + "ornament_id": "11", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77465", + "box_id": "3", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77466", + "box_id": "3", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77467", + "box_id": "3", + "ornament_id": "1", + "audience": "anchor", + "count": "1351", + "used": "0" + }, + { + "id": "77468", + "box_id": "3", + "ornament_id": "2", + "audience": "anchor", + "count": "587", + "used": "0" + }, + { + "id": "77469", + "box_id": "3", + "ornament_id": "3", + "audience": "anchor", + "count": "256", + "used": "0" + }, + { + "id": "77470", + "box_id": "3", + "ornament_id": "4", + "audience": "anchor", + "count": "112", + "used": "0" + }, + { + "id": "77471", + "box_id": "3", + "ornament_id": "5", + "audience": "anchor", + "count": "49", + "used": "0" + }, + { + "id": "77472", + "box_id": "3", + "ornament_id": "6", + "audience": "anchor", + "count": "21", + "used": "0" + }, + { + "id": "77473", + "box_id": "3", + "ornament_id": "7", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "77474", + "box_id": "3", + "ornament_id": "8", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "77475", + "box_id": "3", + "ornament_id": "9", + "audience": "anchor", + "count": "4", + "used": "0" + }, + { + "id": "77476", + "box_id": "3", + "ornament_id": "10", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77477", + "box_id": "3", + "ornament_id": "11", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77478", + "box_id": "3", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77479", + "box_id": "3", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77480", + "box_id": "3", + "ornament_id": "1", + "audience": "robot", + "count": "1351", + "used": "0" + }, + { + "id": "77481", + "box_id": "3", + "ornament_id": "2", + "audience": "robot", + "count": "587", + "used": "0" + }, + { + "id": "77482", + "box_id": "3", + "ornament_id": "3", + "audience": "robot", + "count": "256", + "used": "0" + }, + { + "id": "77483", + "box_id": "3", + "ornament_id": "4", + "audience": "robot", + "count": "112", + "used": "0" + }, + { + "id": "77484", + "box_id": "3", + "ornament_id": "5", + "audience": "robot", + "count": "49", + "used": "0" + }, + { + "id": "77485", + "box_id": "3", + "ornament_id": "6", + "audience": "robot", + "count": "21", + "used": "0" + }, + { + "id": "77486", + "box_id": "3", + "ornament_id": "7", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "77487", + "box_id": "3", + "ornament_id": "8", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "77488", + "box_id": "3", + "ornament_id": "9", + "audience": "robot", + "count": "4", + "used": "0" + }, + { + "id": "77489", + "box_id": "3", + "ornament_id": "10", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77490", + "box_id": "3", + "ornament_id": "11", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77491", + "box_id": "3", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77492", + "box_id": "3", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77493", + "box_id": "3", + "ornament_id": "1", + "audience": "white", + "count": "1351", + "used": "0" + }, + { + "id": "77494", + "box_id": "3", + "ornament_id": "2", + "audience": "white", + "count": "587", + "used": "0" + }, + { + "id": "77495", + "box_id": "3", + "ornament_id": "3", + "audience": "white", + "count": "256", + "used": "0" + }, + { + "id": "77496", + "box_id": "3", + "ornament_id": "4", + "audience": "white", + "count": "112", + "used": "0" + }, + { + "id": "77497", + "box_id": "3", + "ornament_id": "5", + "audience": "white", + "count": "49", + "used": "0" + }, + { + "id": "77498", + "box_id": "3", + "ornament_id": "6", + "audience": "white", + "count": "21", + "used": "0" + }, + { + "id": "77499", + "box_id": "3", + "ornament_id": "7", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "77500", + "box_id": "3", + "ornament_id": "8", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "77501", + "box_id": "3", + "ornament_id": "9", + "audience": "white", + "count": "4", + "used": "0" + }, + { + "id": "77502", + "box_id": "3", + "ornament_id": "10", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77503", + "box_id": "3", + "ornament_id": "11", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77504", + "box_id": "3", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77505", + "box_id": "3", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77506", + "box_id": "4", + "ornament_id": "1", + "audience": "common", + "count": "1351", + "used": "0" + }, + { + "id": "77507", + "box_id": "4", + "ornament_id": "2", + "audience": "common", + "count": "587", + "used": "0" + }, + { + "id": "77508", + "box_id": "4", + "ornament_id": "3", + "audience": "common", + "count": "256", + "used": "0" + }, + { + "id": "77509", + "box_id": "4", + "ornament_id": "4", + "audience": "common", + "count": "111", + "used": "0" + }, + { + "id": "77510", + "box_id": "4", + "ornament_id": "5", + "audience": "common", + "count": "49", + "used": "0" + }, + { + "id": "77511", + "box_id": "4", + "ornament_id": "6", + "audience": "common", + "count": "22", + "used": "0" + }, + { + "id": "77512", + "box_id": "4", + "ornament_id": "7", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "77513", + "box_id": "4", + "ornament_id": "8", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "77514", + "box_id": "4", + "ornament_id": "9", + "audience": "common", + "count": "4", + "used": "0" + }, + { + "id": "77515", + "box_id": "4", + "ornament_id": "10", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77516", + "box_id": "4", + "ornament_id": "11", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77517", + "box_id": "4", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77518", + "box_id": "4", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77519", + "box_id": "4", + "ornament_id": "1", + "audience": "anchor", + "count": "1351", + "used": "0" + }, + { + "id": "77520", + "box_id": "4", + "ornament_id": "2", + "audience": "anchor", + "count": "587", + "used": "0" + }, + { + "id": "77521", + "box_id": "4", + "ornament_id": "3", + "audience": "anchor", + "count": "256", + "used": "0" + }, + { + "id": "77522", + "box_id": "4", + "ornament_id": "4", + "audience": "anchor", + "count": "111", + "used": "0" + }, + { + "id": "77523", + "box_id": "4", + "ornament_id": "5", + "audience": "anchor", + "count": "49", + "used": "0" + }, + { + "id": "77524", + "box_id": "4", + "ornament_id": "6", + "audience": "anchor", + "count": "22", + "used": "0" + }, + { + "id": "77525", + "box_id": "4", + "ornament_id": "7", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "77526", + "box_id": "4", + "ornament_id": "8", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "77527", + "box_id": "4", + "ornament_id": "9", + "audience": "anchor", + "count": "4", + "used": "0" + }, + { + "id": "77528", + "box_id": "4", + "ornament_id": "10", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77529", + "box_id": "4", + "ornament_id": "11", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77530", + "box_id": "4", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77531", + "box_id": "4", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77532", + "box_id": "4", + "ornament_id": "1", + "audience": "robot", + "count": "1351", + "used": "0" + }, + { + "id": "77533", + "box_id": "4", + "ornament_id": "2", + "audience": "robot", + "count": "587", + "used": "0" + }, + { + "id": "77534", + "box_id": "4", + "ornament_id": "3", + "audience": "robot", + "count": "256", + "used": "0" + }, + { + "id": "77535", + "box_id": "4", + "ornament_id": "4", + "audience": "robot", + "count": "111", + "used": "0" + }, + { + "id": "77536", + "box_id": "4", + "ornament_id": "5", + "audience": "robot", + "count": "49", + "used": "0" + }, + { + "id": "77537", + "box_id": "4", + "ornament_id": "6", + "audience": "robot", + "count": "22", + "used": "0" + }, + { + "id": "77538", + "box_id": "4", + "ornament_id": "7", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "77539", + "box_id": "4", + "ornament_id": "8", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "77540", + "box_id": "4", + "ornament_id": "9", + "audience": "robot", + "count": "4", + "used": "0" + }, + { + "id": "77541", + "box_id": "4", + "ornament_id": "10", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77542", + "box_id": "4", + "ornament_id": "11", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77543", + "box_id": "4", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77544", + "box_id": "4", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77545", + "box_id": "4", + "ornament_id": "1", + "audience": "white", + "count": "1351", + "used": "0" + }, + { + "id": "77546", + "box_id": "4", + "ornament_id": "2", + "audience": "white", + "count": "587", + "used": "0" + }, + { + "id": "77547", + "box_id": "4", + "ornament_id": "3", + "audience": "white", + "count": "256", + "used": "0" + }, + { + "id": "77548", + "box_id": "4", + "ornament_id": "4", + "audience": "white", + "count": "111", + "used": "0" + }, + { + "id": "77549", + "box_id": "4", + "ornament_id": "5", + "audience": "white", + "count": "49", + "used": "0" + }, + { + "id": "77550", + "box_id": "4", + "ornament_id": "6", + "audience": "white", + "count": "22", + "used": "0" + }, + { + "id": "77551", + "box_id": "4", + "ornament_id": "7", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "77552", + "box_id": "4", + "ornament_id": "8", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "77553", + "box_id": "4", + "ornament_id": "9", + "audience": "white", + "count": "4", + "used": "0" + }, + { + "id": "77554", + "box_id": "4", + "ornament_id": "10", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77555", + "box_id": "4", + "ornament_id": "11", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77556", + "box_id": "4", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77557", + "box_id": "4", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77558", + "box_id": "5", + "ornament_id": "1", + "audience": "common", + "count": "1359", + "used": "0" + }, + { + "id": "77559", + "box_id": "5", + "ornament_id": "2", + "audience": "common", + "count": "587", + "used": "0" + }, + { + "id": "77560", + "box_id": "5", + "ornament_id": "3", + "audience": "common", + "count": "253", + "used": "0" + }, + { + "id": "77561", + "box_id": "5", + "ornament_id": "4", + "audience": "common", + "count": "109", + "used": "0" + }, + { + "id": "77562", + "box_id": "5", + "ornament_id": "5", + "audience": "common", + "count": "48", + "used": "0" + }, + { + "id": "77563", + "box_id": "5", + "ornament_id": "6", + "audience": "common", + "count": "21", + "used": "0" + }, + { + "id": "77564", + "box_id": "5", + "ornament_id": "7", + "audience": "common", + "count": "9", + "used": "0" + }, + { + "id": "77565", + "box_id": "5", + "ornament_id": "8", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "77566", + "box_id": "5", + "ornament_id": "9", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "77567", + "box_id": "5", + "ornament_id": "10", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "77568", + "box_id": "5", + "ornament_id": "11", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77569", + "box_id": "5", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77570", + "box_id": "5", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77571", + "box_id": "5", + "ornament_id": "1", + "audience": "anchor", + "count": "1359", + "used": "0" + }, + { + "id": "77572", + "box_id": "5", + "ornament_id": "2", + "audience": "anchor", + "count": "587", + "used": "0" + }, + { + "id": "77573", + "box_id": "5", + "ornament_id": "3", + "audience": "anchor", + "count": "253", + "used": "0" + }, + { + "id": "77574", + "box_id": "5", + "ornament_id": "4", + "audience": "anchor", + "count": "109", + "used": "0" + }, + { + "id": "77575", + "box_id": "5", + "ornament_id": "5", + "audience": "anchor", + "count": "48", + "used": "0" + }, + { + "id": "77576", + "box_id": "5", + "ornament_id": "6", + "audience": "anchor", + "count": "21", + "used": "0" + }, + { + "id": "77577", + "box_id": "5", + "ornament_id": "7", + "audience": "anchor", + "count": "9", + "used": "0" + }, + { + "id": "77578", + "box_id": "5", + "ornament_id": "8", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "77579", + "box_id": "5", + "ornament_id": "9", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "77580", + "box_id": "5", + "ornament_id": "10", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "77581", + "box_id": "5", + "ornament_id": "11", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77582", + "box_id": "5", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77583", + "box_id": "5", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77584", + "box_id": "5", + "ornament_id": "1", + "audience": "robot", + "count": "1359", + "used": "0" + }, + { + "id": "77585", + "box_id": "5", + "ornament_id": "2", + "audience": "robot", + "count": "587", + "used": "0" + }, + { + "id": "77586", + "box_id": "5", + "ornament_id": "3", + "audience": "robot", + "count": "253", + "used": "0" + }, + { + "id": "77587", + "box_id": "5", + "ornament_id": "4", + "audience": "robot", + "count": "109", + "used": "0" + }, + { + "id": "77588", + "box_id": "5", + "ornament_id": "5", + "audience": "robot", + "count": "48", + "used": "0" + }, + { + "id": "77589", + "box_id": "5", + "ornament_id": "6", + "audience": "robot", + "count": "21", + "used": "0" + }, + { + "id": "77590", + "box_id": "5", + "ornament_id": "7", + "audience": "robot", + "count": "9", + "used": "0" + }, + { + "id": "77591", + "box_id": "5", + "ornament_id": "8", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "77592", + "box_id": "5", + "ornament_id": "9", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "77593", + "box_id": "5", + "ornament_id": "10", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "77594", + "box_id": "5", + "ornament_id": "11", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77595", + "box_id": "5", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77596", + "box_id": "5", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77597", + "box_id": "5", + "ornament_id": "1", + "audience": "white", + "count": "1359", + "used": "0" + }, + { + "id": "77598", + "box_id": "5", + "ornament_id": "2", + "audience": "white", + "count": "587", + "used": "0" + }, + { + "id": "77599", + "box_id": "5", + "ornament_id": "3", + "audience": "white", + "count": "253", + "used": "0" + }, + { + "id": "77600", + "box_id": "5", + "ornament_id": "4", + "audience": "white", + "count": "109", + "used": "0" + }, + { + "id": "77601", + "box_id": "5", + "ornament_id": "5", + "audience": "white", + "count": "48", + "used": "0" + }, + { + "id": "77602", + "box_id": "5", + "ornament_id": "6", + "audience": "white", + "count": "21", + "used": "0" + }, + { + "id": "77603", + "box_id": "5", + "ornament_id": "7", + "audience": "white", + "count": "9", + "used": "0" + }, + { + "id": "77604", + "box_id": "5", + "ornament_id": "8", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "77605", + "box_id": "5", + "ornament_id": "9", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "77606", + "box_id": "5", + "ornament_id": "10", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "77607", + "box_id": "5", + "ornament_id": "11", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77608", + "box_id": "5", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77609", + "box_id": "5", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77610", + "box_id": "6", + "ornament_id": "1", + "audience": "common", + "count": "1360", + "used": "0" + }, + { + "id": "77611", + "box_id": "6", + "ornament_id": "2", + "audience": "common", + "count": "587", + "used": "0" + }, + { + "id": "77612", + "box_id": "6", + "ornament_id": "3", + "audience": "common", + "count": "252", + "used": "0" + }, + { + "id": "77613", + "box_id": "6", + "ornament_id": "4", + "audience": "common", + "count": "109", + "used": "0" + }, + { + "id": "77614", + "box_id": "6", + "ornament_id": "5", + "audience": "common", + "count": "48", + "used": "0" + }, + { + "id": "77615", + "box_id": "6", + "ornament_id": "6", + "audience": "common", + "count": "20", + "used": "0" + }, + { + "id": "77616", + "box_id": "6", + "ornament_id": "7", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "77617", + "box_id": "6", + "ornament_id": "8", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "77618", + "box_id": "6", + "ornament_id": "9", + "audience": "common", + "count": "4", + "used": "0" + }, + { + "id": "77619", + "box_id": "6", + "ornament_id": "10", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77620", + "box_id": "6", + "ornament_id": "11", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77621", + "box_id": "6", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77622", + "box_id": "6", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77623", + "box_id": "6", + "ornament_id": "1", + "audience": "anchor", + "count": "1360", + "used": "0" + }, + { + "id": "77624", + "box_id": "6", + "ornament_id": "2", + "audience": "anchor", + "count": "587", + "used": "0" + }, + { + "id": "77625", + "box_id": "6", + "ornament_id": "3", + "audience": "anchor", + "count": "252", + "used": "0" + }, + { + "id": "77626", + "box_id": "6", + "ornament_id": "4", + "audience": "anchor", + "count": "109", + "used": "0" + }, + { + "id": "77627", + "box_id": "6", + "ornament_id": "5", + "audience": "anchor", + "count": "48", + "used": "0" + }, + { + "id": "77628", + "box_id": "6", + "ornament_id": "6", + "audience": "anchor", + "count": "20", + "used": "0" + }, + { + "id": "77629", + "box_id": "6", + "ornament_id": "7", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "77630", + "box_id": "6", + "ornament_id": "8", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "77631", + "box_id": "6", + "ornament_id": "9", + "audience": "anchor", + "count": "4", + "used": "0" + }, + { + "id": "77632", + "box_id": "6", + "ornament_id": "10", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77633", + "box_id": "6", + "ornament_id": "11", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77634", + "box_id": "6", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77635", + "box_id": "6", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77636", + "box_id": "6", + "ornament_id": "1", + "audience": "robot", + "count": "1360", + "used": "0" + }, + { + "id": "77637", + "box_id": "6", + "ornament_id": "2", + "audience": "robot", + "count": "587", + "used": "0" + }, + { + "id": "77638", + "box_id": "6", + "ornament_id": "3", + "audience": "robot", + "count": "252", + "used": "0" + }, + { + "id": "77639", + "box_id": "6", + "ornament_id": "4", + "audience": "robot", + "count": "109", + "used": "0" + }, + { + "id": "77640", + "box_id": "6", + "ornament_id": "5", + "audience": "robot", + "count": "48", + "used": "0" + }, + { + "id": "77641", + "box_id": "6", + "ornament_id": "6", + "audience": "robot", + "count": "20", + "used": "0" + }, + { + "id": "77642", + "box_id": "6", + "ornament_id": "7", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "77643", + "box_id": "6", + "ornament_id": "8", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "77644", + "box_id": "6", + "ornament_id": "9", + "audience": "robot", + "count": "4", + "used": "0" + }, + { + "id": "77645", + "box_id": "6", + "ornament_id": "10", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77646", + "box_id": "6", + "ornament_id": "11", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77647", + "box_id": "6", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77648", + "box_id": "6", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77649", + "box_id": "6", + "ornament_id": "1", + "audience": "white", + "count": "1360", + "used": "0" + }, + { + "id": "77650", + "box_id": "6", + "ornament_id": "2", + "audience": "white", + "count": "587", + "used": "0" + }, + { + "id": "77651", + "box_id": "6", + "ornament_id": "3", + "audience": "white", + "count": "252", + "used": "0" + }, + { + "id": "77652", + "box_id": "6", + "ornament_id": "4", + "audience": "white", + "count": "109", + "used": "0" + }, + { + "id": "77653", + "box_id": "6", + "ornament_id": "5", + "audience": "white", + "count": "48", + "used": "0" + }, + { + "id": "77654", + "box_id": "6", + "ornament_id": "6", + "audience": "white", + "count": "20", + "used": "0" + }, + { + "id": "77655", + "box_id": "6", + "ornament_id": "7", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "77656", + "box_id": "6", + "ornament_id": "8", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "77657", + "box_id": "6", + "ornament_id": "9", + "audience": "white", + "count": "4", + "used": "0" + }, + { + "id": "77658", + "box_id": "6", + "ornament_id": "10", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77659", + "box_id": "6", + "ornament_id": "11", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77660", + "box_id": "6", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77661", + "box_id": "6", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77662", + "box_id": "7", + "ornament_id": "1", + "audience": "common", + "count": "1346", + "used": "0" + }, + { + "id": "77663", + "box_id": "7", + "ornament_id": "2", + "audience": "common", + "count": "588", + "used": "0" + }, + { + "id": "77664", + "box_id": "7", + "ornament_id": "3", + "audience": "common", + "count": "258", + "used": "0" + }, + { + "id": "77665", + "box_id": "7", + "ornament_id": "4", + "audience": "common", + "count": "113", + "used": "0" + }, + { + "id": "77666", + "box_id": "7", + "ornament_id": "5", + "audience": "common", + "count": "50", + "used": "0" + }, + { + "id": "77667", + "box_id": "7", + "ornament_id": "6", + "audience": "common", + "count": "22", + "used": "0" + }, + { + "id": "77668", + "box_id": "7", + "ornament_id": "7", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "77669", + "box_id": "7", + "ornament_id": "8", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "77670", + "box_id": "7", + "ornament_id": "9", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77671", + "box_id": "7", + "ornament_id": "10", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77672", + "box_id": "7", + "ornament_id": "11", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77673", + "box_id": "7", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77674", + "box_id": "7", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77675", + "box_id": "7", + "ornament_id": "1", + "audience": "anchor", + "count": "1346", + "used": "0" + }, + { + "id": "77676", + "box_id": "7", + "ornament_id": "2", + "audience": "anchor", + "count": "588", + "used": "0" + }, + { + "id": "77677", + "box_id": "7", + "ornament_id": "3", + "audience": "anchor", + "count": "258", + "used": "0" + }, + { + "id": "77678", + "box_id": "7", + "ornament_id": "4", + "audience": "anchor", + "count": "113", + "used": "0" + }, + { + "id": "77679", + "box_id": "7", + "ornament_id": "5", + "audience": "anchor", + "count": "50", + "used": "0" + }, + { + "id": "77680", + "box_id": "7", + "ornament_id": "6", + "audience": "anchor", + "count": "22", + "used": "0" + }, + { + "id": "77681", + "box_id": "7", + "ornament_id": "7", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "77682", + "box_id": "7", + "ornament_id": "8", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "77683", + "box_id": "7", + "ornament_id": "9", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77684", + "box_id": "7", + "ornament_id": "10", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77685", + "box_id": "7", + "ornament_id": "11", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77686", + "box_id": "7", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77687", + "box_id": "7", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77688", + "box_id": "7", + "ornament_id": "1", + "audience": "robot", + "count": "1346", + "used": "0" + }, + { + "id": "77689", + "box_id": "7", + "ornament_id": "2", + "audience": "robot", + "count": "588", + "used": "0" + }, + { + "id": "77690", + "box_id": "7", + "ornament_id": "3", + "audience": "robot", + "count": "258", + "used": "0" + }, + { + "id": "77691", + "box_id": "7", + "ornament_id": "4", + "audience": "robot", + "count": "113", + "used": "0" + }, + { + "id": "77692", + "box_id": "7", + "ornament_id": "5", + "audience": "robot", + "count": "50", + "used": "0" + }, + { + "id": "77693", + "box_id": "7", + "ornament_id": "6", + "audience": "robot", + "count": "22", + "used": "0" + }, + { + "id": "77694", + "box_id": "7", + "ornament_id": "7", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "77695", + "box_id": "7", + "ornament_id": "8", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "77696", + "box_id": "7", + "ornament_id": "9", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77697", + "box_id": "7", + "ornament_id": "10", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77698", + "box_id": "7", + "ornament_id": "11", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77699", + "box_id": "7", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77700", + "box_id": "7", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77701", + "box_id": "7", + "ornament_id": "1", + "audience": "white", + "count": "1346", + "used": "0" + }, + { + "id": "77702", + "box_id": "7", + "ornament_id": "2", + "audience": "white", + "count": "588", + "used": "0" + }, + { + "id": "77703", + "box_id": "7", + "ornament_id": "3", + "audience": "white", + "count": "258", + "used": "0" + }, + { + "id": "77704", + "box_id": "7", + "ornament_id": "4", + "audience": "white", + "count": "113", + "used": "0" + }, + { + "id": "77705", + "box_id": "7", + "ornament_id": "5", + "audience": "white", + "count": "50", + "used": "0" + }, + { + "id": "77706", + "box_id": "7", + "ornament_id": "6", + "audience": "white", + "count": "22", + "used": "0" + }, + { + "id": "77707", + "box_id": "7", + "ornament_id": "7", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "77708", + "box_id": "7", + "ornament_id": "8", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "77709", + "box_id": "7", + "ornament_id": "9", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77710", + "box_id": "7", + "ornament_id": "10", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77711", + "box_id": "7", + "ornament_id": "11", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77712", + "box_id": "7", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77713", + "box_id": "7", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77714", + "box_id": "8", + "ornament_id": "1", + "audience": "common", + "count": "1357", + "used": "0" + }, + { + "id": "77715", + "box_id": "8", + "ornament_id": "2", + "audience": "common", + "count": "586", + "used": "0" + }, + { + "id": "77716", + "box_id": "8", + "ornament_id": "3", + "audience": "common", + "count": "254", + "used": "0" + }, + { + "id": "77717", + "box_id": "8", + "ornament_id": "4", + "audience": "common", + "count": "110", + "used": "0" + }, + { + "id": "77718", + "box_id": "8", + "ornament_id": "5", + "audience": "common", + "count": "48", + "used": "0" + }, + { + "id": "77719", + "box_id": "8", + "ornament_id": "6", + "audience": "common", + "count": "21", + "used": "0" + }, + { + "id": "77720", + "box_id": "8", + "ornament_id": "7", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "77721", + "box_id": "8", + "ornament_id": "8", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "77722", + "box_id": "8", + "ornament_id": "9", + "audience": "common", + "count": "4", + "used": "0" + }, + { + "id": "77723", + "box_id": "8", + "ornament_id": "10", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77724", + "box_id": "8", + "ornament_id": "11", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77725", + "box_id": "8", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77726", + "box_id": "8", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77727", + "box_id": "8", + "ornament_id": "1", + "audience": "anchor", + "count": "1357", + "used": "0" + }, + { + "id": "77728", + "box_id": "8", + "ornament_id": "2", + "audience": "anchor", + "count": "586", + "used": "0" + }, + { + "id": "77729", + "box_id": "8", + "ornament_id": "3", + "audience": "anchor", + "count": "254", + "used": "0" + }, + { + "id": "77730", + "box_id": "8", + "ornament_id": "4", + "audience": "anchor", + "count": "110", + "used": "0" + }, + { + "id": "77731", + "box_id": "8", + "ornament_id": "5", + "audience": "anchor", + "count": "48", + "used": "0" + }, + { + "id": "77732", + "box_id": "8", + "ornament_id": "6", + "audience": "anchor", + "count": "21", + "used": "0" + }, + { + "id": "77733", + "box_id": "8", + "ornament_id": "7", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "77734", + "box_id": "8", + "ornament_id": "8", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "77735", + "box_id": "8", + "ornament_id": "9", + "audience": "anchor", + "count": "4", + "used": "0" + }, + { + "id": "77736", + "box_id": "8", + "ornament_id": "10", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77737", + "box_id": "8", + "ornament_id": "11", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77738", + "box_id": "8", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77739", + "box_id": "8", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77740", + "box_id": "8", + "ornament_id": "1", + "audience": "robot", + "count": "1357", + "used": "0" + }, + { + "id": "77741", + "box_id": "8", + "ornament_id": "2", + "audience": "robot", + "count": "586", + "used": "0" + }, + { + "id": "77742", + "box_id": "8", + "ornament_id": "3", + "audience": "robot", + "count": "254", + "used": "0" + }, + { + "id": "77743", + "box_id": "8", + "ornament_id": "4", + "audience": "robot", + "count": "110", + "used": "0" + }, + { + "id": "77744", + "box_id": "8", + "ornament_id": "5", + "audience": "robot", + "count": "48", + "used": "0" + }, + { + "id": "77745", + "box_id": "8", + "ornament_id": "6", + "audience": "robot", + "count": "21", + "used": "0" + }, + { + "id": "77746", + "box_id": "8", + "ornament_id": "7", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "77747", + "box_id": "8", + "ornament_id": "8", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "77748", + "box_id": "8", + "ornament_id": "9", + "audience": "robot", + "count": "4", + "used": "0" + }, + { + "id": "77749", + "box_id": "8", + "ornament_id": "10", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77750", + "box_id": "8", + "ornament_id": "11", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77751", + "box_id": "8", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77752", + "box_id": "8", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77753", + "box_id": "8", + "ornament_id": "1", + "audience": "white", + "count": "1357", + "used": "0" + }, + { + "id": "77754", + "box_id": "8", + "ornament_id": "2", + "audience": "white", + "count": "586", + "used": "0" + }, + { + "id": "77755", + "box_id": "8", + "ornament_id": "3", + "audience": "white", + "count": "254", + "used": "0" + }, + { + "id": "77756", + "box_id": "8", + "ornament_id": "4", + "audience": "white", + "count": "110", + "used": "0" + }, + { + "id": "77757", + "box_id": "8", + "ornament_id": "5", + "audience": "white", + "count": "48", + "used": "0" + }, + { + "id": "77758", + "box_id": "8", + "ornament_id": "6", + "audience": "white", + "count": "21", + "used": "0" + }, + { + "id": "77759", + "box_id": "8", + "ornament_id": "7", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "77760", + "box_id": "8", + "ornament_id": "8", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "77761", + "box_id": "8", + "ornament_id": "9", + "audience": "white", + "count": "4", + "used": "0" + }, + { + "id": "77762", + "box_id": "8", + "ornament_id": "10", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77763", + "box_id": "8", + "ornament_id": "11", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77764", + "box_id": "8", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77765", + "box_id": "8", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77766", + "box_id": "9", + "ornament_id": "1", + "audience": "common", + "count": "1351", + "used": "0" + }, + { + "id": "77767", + "box_id": "9", + "ornament_id": "2", + "audience": "common", + "count": "586", + "used": "0" + }, + { + "id": "77768", + "box_id": "9", + "ornament_id": "3", + "audience": "common", + "count": "256", + "used": "0" + }, + { + "id": "77769", + "box_id": "9", + "ornament_id": "4", + "audience": "common", + "count": "113", + "used": "0" + }, + { + "id": "77770", + "box_id": "9", + "ornament_id": "5", + "audience": "common", + "count": "49", + "used": "0" + }, + { + "id": "77771", + "box_id": "9", + "ornament_id": "6", + "audience": "common", + "count": "22", + "used": "0" + }, + { + "id": "77772", + "box_id": "9", + "ornament_id": "7", + "audience": "common", + "count": "9", + "used": "0" + }, + { + "id": "77773", + "box_id": "9", + "ornament_id": "8", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "77774", + "box_id": "9", + "ornament_id": "9", + "audience": "common", + "count": "4", + "used": "0" + }, + { + "id": "77775", + "box_id": "9", + "ornament_id": "10", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77776", + "box_id": "9", + "ornament_id": "11", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77777", + "box_id": "9", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77778", + "box_id": "9", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77779", + "box_id": "9", + "ornament_id": "1", + "audience": "anchor", + "count": "1351", + "used": "0" + }, + { + "id": "77780", + "box_id": "9", + "ornament_id": "2", + "audience": "anchor", + "count": "586", + "used": "0" + }, + { + "id": "77781", + "box_id": "9", + "ornament_id": "3", + "audience": "anchor", + "count": "256", + "used": "0" + }, + { + "id": "77782", + "box_id": "9", + "ornament_id": "4", + "audience": "anchor", + "count": "113", + "used": "0" + }, + { + "id": "77783", + "box_id": "9", + "ornament_id": "5", + "audience": "anchor", + "count": "49", + "used": "0" + }, + { + "id": "77784", + "box_id": "9", + "ornament_id": "6", + "audience": "anchor", + "count": "22", + "used": "0" + }, + { + "id": "77785", + "box_id": "9", + "ornament_id": "7", + "audience": "anchor", + "count": "9", + "used": "0" + }, + { + "id": "77786", + "box_id": "9", + "ornament_id": "8", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "77787", + "box_id": "9", + "ornament_id": "9", + "audience": "anchor", + "count": "4", + "used": "0" + }, + { + "id": "77788", + "box_id": "9", + "ornament_id": "10", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77789", + "box_id": "9", + "ornament_id": "11", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77790", + "box_id": "9", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77791", + "box_id": "9", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77792", + "box_id": "9", + "ornament_id": "1", + "audience": "robot", + "count": "1351", + "used": "0" + }, + { + "id": "77793", + "box_id": "9", + "ornament_id": "2", + "audience": "robot", + "count": "586", + "used": "0" + }, + { + "id": "77794", + "box_id": "9", + "ornament_id": "3", + "audience": "robot", + "count": "256", + "used": "0" + }, + { + "id": "77795", + "box_id": "9", + "ornament_id": "4", + "audience": "robot", + "count": "113", + "used": "0" + }, + { + "id": "77796", + "box_id": "9", + "ornament_id": "5", + "audience": "robot", + "count": "49", + "used": "0" + }, + { + "id": "77797", + "box_id": "9", + "ornament_id": "6", + "audience": "robot", + "count": "22", + "used": "0" + }, + { + "id": "77798", + "box_id": "9", + "ornament_id": "7", + "audience": "robot", + "count": "9", + "used": "0" + }, + { + "id": "77799", + "box_id": "9", + "ornament_id": "8", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "77800", + "box_id": "9", + "ornament_id": "9", + "audience": "robot", + "count": "4", + "used": "0" + }, + { + "id": "77801", + "box_id": "9", + "ornament_id": "10", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77802", + "box_id": "9", + "ornament_id": "11", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77803", + "box_id": "9", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77804", + "box_id": "9", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77805", + "box_id": "9", + "ornament_id": "1", + "audience": "white", + "count": "1351", + "used": "0" + }, + { + "id": "77806", + "box_id": "9", + "ornament_id": "2", + "audience": "white", + "count": "586", + "used": "0" + }, + { + "id": "77807", + "box_id": "9", + "ornament_id": "3", + "audience": "white", + "count": "256", + "used": "0" + }, + { + "id": "77808", + "box_id": "9", + "ornament_id": "4", + "audience": "white", + "count": "113", + "used": "0" + }, + { + "id": "77809", + "box_id": "9", + "ornament_id": "5", + "audience": "white", + "count": "49", + "used": "0" + }, + { + "id": "77810", + "box_id": "9", + "ornament_id": "6", + "audience": "white", + "count": "22", + "used": "0" + }, + { + "id": "77811", + "box_id": "9", + "ornament_id": "7", + "audience": "white", + "count": "9", + "used": "0" + }, + { + "id": "77812", + "box_id": "9", + "ornament_id": "8", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "77813", + "box_id": "9", + "ornament_id": "9", + "audience": "white", + "count": "4", + "used": "0" + }, + { + "id": "77814", + "box_id": "9", + "ornament_id": "10", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77815", + "box_id": "9", + "ornament_id": "11", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77816", + "box_id": "9", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77817", + "box_id": "9", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77818", + "box_id": "10", + "ornament_id": "1", + "audience": "common", + "count": "1360", + "used": "0" + }, + { + "id": "77819", + "box_id": "10", + "ornament_id": "2", + "audience": "common", + "count": "585", + "used": "0" + }, + { + "id": "77820", + "box_id": "10", + "ornament_id": "3", + "audience": "common", + "count": "253", + "used": "0" + }, + { + "id": "77821", + "box_id": "10", + "ornament_id": "4", + "audience": "common", + "count": "109", + "used": "0" + }, + { + "id": "77822", + "box_id": "10", + "ornament_id": "5", + "audience": "common", + "count": "49", + "used": "0" + }, + { + "id": "77823", + "box_id": "10", + "ornament_id": "6", + "audience": "common", + "count": "21", + "used": "0" + }, + { + "id": "77824", + "box_id": "10", + "ornament_id": "7", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "77825", + "box_id": "10", + "ornament_id": "8", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "77826", + "box_id": "10", + "ornament_id": "9", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "77827", + "box_id": "10", + "ornament_id": "10", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77828", + "box_id": "10", + "ornament_id": "11", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77829", + "box_id": "10", + "ornament_id": "12", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77830", + "box_id": "10", + "ornament_id": "13", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77831", + "box_id": "10", + "ornament_id": "1", + "audience": "anchor", + "count": "1360", + "used": "0" + }, + { + "id": "77832", + "box_id": "10", + "ornament_id": "2", + "audience": "anchor", + "count": "585", + "used": "0" + }, + { + "id": "77833", + "box_id": "10", + "ornament_id": "3", + "audience": "anchor", + "count": "253", + "used": "0" + }, + { + "id": "77834", + "box_id": "10", + "ornament_id": "4", + "audience": "anchor", + "count": "109", + "used": "0" + }, + { + "id": "77835", + "box_id": "10", + "ornament_id": "5", + "audience": "anchor", + "count": "49", + "used": "0" + }, + { + "id": "77836", + "box_id": "10", + "ornament_id": "6", + "audience": "anchor", + "count": "21", + "used": "0" + }, + { + "id": "77837", + "box_id": "10", + "ornament_id": "7", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "77838", + "box_id": "10", + "ornament_id": "8", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "77839", + "box_id": "10", + "ornament_id": "9", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "77840", + "box_id": "10", + "ornament_id": "10", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77841", + "box_id": "10", + "ornament_id": "11", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77842", + "box_id": "10", + "ornament_id": "12", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77843", + "box_id": "10", + "ornament_id": "13", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77844", + "box_id": "10", + "ornament_id": "1", + "audience": "robot", + "count": "1360", + "used": "0" + }, + { + "id": "77845", + "box_id": "10", + "ornament_id": "2", + "audience": "robot", + "count": "585", + "used": "0" + }, + { + "id": "77846", + "box_id": "10", + "ornament_id": "3", + "audience": "robot", + "count": "253", + "used": "0" + }, + { + "id": "77847", + "box_id": "10", + "ornament_id": "4", + "audience": "robot", + "count": "109", + "used": "0" + }, + { + "id": "77848", + "box_id": "10", + "ornament_id": "5", + "audience": "robot", + "count": "49", + "used": "0" + }, + { + "id": "77849", + "box_id": "10", + "ornament_id": "6", + "audience": "robot", + "count": "21", + "used": "0" + }, + { + "id": "77850", + "box_id": "10", + "ornament_id": "7", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "77851", + "box_id": "10", + "ornament_id": "8", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "77852", + "box_id": "10", + "ornament_id": "9", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "77853", + "box_id": "10", + "ornament_id": "10", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77854", + "box_id": "10", + "ornament_id": "11", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77855", + "box_id": "10", + "ornament_id": "12", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77856", + "box_id": "10", + "ornament_id": "13", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77857", + "box_id": "10", + "ornament_id": "1", + "audience": "white", + "count": "1360", + "used": "0" + }, + { + "id": "77858", + "box_id": "10", + "ornament_id": "2", + "audience": "white", + "count": "585", + "used": "0" + }, + { + "id": "77859", + "box_id": "10", + "ornament_id": "3", + "audience": "white", + "count": "253", + "used": "0" + }, + { + "id": "77860", + "box_id": "10", + "ornament_id": "4", + "audience": "white", + "count": "109", + "used": "0" + }, + { + "id": "77861", + "box_id": "10", + "ornament_id": "5", + "audience": "white", + "count": "49", + "used": "0" + }, + { + "id": "77862", + "box_id": "10", + "ornament_id": "6", + "audience": "white", + "count": "21", + "used": "0" + }, + { + "id": "77863", + "box_id": "10", + "ornament_id": "7", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "77864", + "box_id": "10", + "ornament_id": "8", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "77865", + "box_id": "10", + "ornament_id": "9", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "77866", + "box_id": "10", + "ornament_id": "10", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77867", + "box_id": "10", + "ornament_id": "11", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77868", + "box_id": "10", + "ornament_id": "12", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77869", + "box_id": "10", + "ornament_id": "13", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77870", + "box_id": "11", + "ornament_id": "1", + "audience": "common", + "count": "1091", + "used": "0" + }, + { + "id": "77871", + "box_id": "11", + "ornament_id": "2", + "audience": "common", + "count": "593", + "used": "0" + }, + { + "id": "77872", + "box_id": "11", + "ornament_id": "3", + "audience": "common", + "count": "324", + "used": "0" + }, + { + "id": "77873", + "box_id": "11", + "ornament_id": "4", + "audience": "common", + "count": "175", + "used": "0" + }, + { + "id": "77874", + "box_id": "11", + "ornament_id": "5", + "audience": "common", + "count": "96", + "used": "0" + }, + { + "id": "77875", + "box_id": "11", + "ornament_id": "6", + "audience": "common", + "count": "53", + "used": "0" + }, + { + "id": "77876", + "box_id": "11", + "ornament_id": "7", + "audience": "common", + "count": "29", + "used": "0" + }, + { + "id": "77877", + "box_id": "11", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "77878", + "box_id": "11", + "ornament_id": "9", + "audience": "common", + "count": "8", + "used": "0" + }, + { + "id": "77879", + "box_id": "11", + "ornament_id": "10", + "audience": "common", + "count": "6", + "used": "0" + }, + { + "id": "77880", + "box_id": "11", + "ornament_id": "11", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "77881", + "box_id": "11", + "ornament_id": "12", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "77882", + "box_id": "11", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77883", + "box_id": "11", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77884", + "box_id": "11", + "ornament_id": "1", + "audience": "anchor", + "count": "1091", + "used": "0" + }, + { + "id": "77885", + "box_id": "11", + "ornament_id": "2", + "audience": "anchor", + "count": "593", + "used": "0" + }, + { + "id": "77886", + "box_id": "11", + "ornament_id": "3", + "audience": "anchor", + "count": "324", + "used": "0" + }, + { + "id": "77887", + "box_id": "11", + "ornament_id": "4", + "audience": "anchor", + "count": "175", + "used": "0" + }, + { + "id": "77888", + "box_id": "11", + "ornament_id": "5", + "audience": "anchor", + "count": "96", + "used": "0" + }, + { + "id": "77889", + "box_id": "11", + "ornament_id": "6", + "audience": "anchor", + "count": "53", + "used": "0" + }, + { + "id": "77890", + "box_id": "11", + "ornament_id": "7", + "audience": "anchor", + "count": "29", + "used": "0" + }, + { + "id": "77891", + "box_id": "11", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "77892", + "box_id": "11", + "ornament_id": "9", + "audience": "anchor", + "count": "8", + "used": "0" + }, + { + "id": "77893", + "box_id": "11", + "ornament_id": "10", + "audience": "anchor", + "count": "6", + "used": "0" + }, + { + "id": "77894", + "box_id": "11", + "ornament_id": "11", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "77895", + "box_id": "11", + "ornament_id": "12", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "77896", + "box_id": "11", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77897", + "box_id": "11", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77898", + "box_id": "11", + "ornament_id": "1", + "audience": "robot", + "count": "1091", + "used": "0" + }, + { + "id": "77899", + "box_id": "11", + "ornament_id": "2", + "audience": "robot", + "count": "593", + "used": "0" + }, + { + "id": "77900", + "box_id": "11", + "ornament_id": "3", + "audience": "robot", + "count": "324", + "used": "0" + }, + { + "id": "77901", + "box_id": "11", + "ornament_id": "4", + "audience": "robot", + "count": "175", + "used": "0" + }, + { + "id": "77902", + "box_id": "11", + "ornament_id": "5", + "audience": "robot", + "count": "96", + "used": "0" + }, + { + "id": "77903", + "box_id": "11", + "ornament_id": "6", + "audience": "robot", + "count": "53", + "used": "0" + }, + { + "id": "77904", + "box_id": "11", + "ornament_id": "7", + "audience": "robot", + "count": "29", + "used": "0" + }, + { + "id": "77905", + "box_id": "11", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "77906", + "box_id": "11", + "ornament_id": "9", + "audience": "robot", + "count": "8", + "used": "0" + }, + { + "id": "77907", + "box_id": "11", + "ornament_id": "10", + "audience": "robot", + "count": "6", + "used": "0" + }, + { + "id": "77908", + "box_id": "11", + "ornament_id": "11", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "77909", + "box_id": "11", + "ornament_id": "12", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "77910", + "box_id": "11", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77911", + "box_id": "11", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77912", + "box_id": "11", + "ornament_id": "1", + "audience": "white", + "count": "1091", + "used": "0" + }, + { + "id": "77913", + "box_id": "11", + "ornament_id": "2", + "audience": "white", + "count": "593", + "used": "0" + }, + { + "id": "77914", + "box_id": "11", + "ornament_id": "3", + "audience": "white", + "count": "324", + "used": "0" + }, + { + "id": "77915", + "box_id": "11", + "ornament_id": "4", + "audience": "white", + "count": "175", + "used": "0" + }, + { + "id": "77916", + "box_id": "11", + "ornament_id": "5", + "audience": "white", + "count": "96", + "used": "0" + }, + { + "id": "77917", + "box_id": "11", + "ornament_id": "6", + "audience": "white", + "count": "53", + "used": "0" + }, + { + "id": "77918", + "box_id": "11", + "ornament_id": "7", + "audience": "white", + "count": "29", + "used": "0" + }, + { + "id": "77919", + "box_id": "11", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "77920", + "box_id": "11", + "ornament_id": "9", + "audience": "white", + "count": "8", + "used": "0" + }, + { + "id": "77921", + "box_id": "11", + "ornament_id": "10", + "audience": "white", + "count": "6", + "used": "0" + }, + { + "id": "77922", + "box_id": "11", + "ornament_id": "11", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "77923", + "box_id": "11", + "ornament_id": "12", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "77924", + "box_id": "11", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77925", + "box_id": "11", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77926", + "box_id": "12", + "ornament_id": "1", + "audience": "common", + "count": "1091", + "used": "0" + }, + { + "id": "77927", + "box_id": "12", + "ornament_id": "2", + "audience": "common", + "count": "594", + "used": "0" + }, + { + "id": "77928", + "box_id": "12", + "ornament_id": "3", + "audience": "common", + "count": "322", + "used": "0" + }, + { + "id": "77929", + "box_id": "12", + "ornament_id": "4", + "audience": "common", + "count": "176", + "used": "0" + }, + { + "id": "77930", + "box_id": "12", + "ornament_id": "5", + "audience": "common", + "count": "96", + "used": "0" + }, + { + "id": "77931", + "box_id": "12", + "ornament_id": "6", + "audience": "common", + "count": "53", + "used": "0" + }, + { + "id": "77932", + "box_id": "12", + "ornament_id": "7", + "audience": "common", + "count": "29", + "used": "0" + }, + { + "id": "77933", + "box_id": "12", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "77934", + "box_id": "12", + "ornament_id": "9", + "audience": "common", + "count": "8", + "used": "0" + }, + { + "id": "77935", + "box_id": "12", + "ornament_id": "10", + "audience": "common", + "count": "6", + "used": "0" + }, + { + "id": "77936", + "box_id": "12", + "ornament_id": "11", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "77937", + "box_id": "12", + "ornament_id": "12", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "77938", + "box_id": "12", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77939", + "box_id": "12", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77940", + "box_id": "12", + "ornament_id": "1", + "audience": "anchor", + "count": "1091", + "used": "0" + }, + { + "id": "77941", + "box_id": "12", + "ornament_id": "2", + "audience": "anchor", + "count": "594", + "used": "0" + }, + { + "id": "77942", + "box_id": "12", + "ornament_id": "3", + "audience": "anchor", + "count": "322", + "used": "0" + }, + { + "id": "77943", + "box_id": "12", + "ornament_id": "4", + "audience": "anchor", + "count": "176", + "used": "0" + }, + { + "id": "77944", + "box_id": "12", + "ornament_id": "5", + "audience": "anchor", + "count": "96", + "used": "0" + }, + { + "id": "77945", + "box_id": "12", + "ornament_id": "6", + "audience": "anchor", + "count": "53", + "used": "0" + }, + { + "id": "77946", + "box_id": "12", + "ornament_id": "7", + "audience": "anchor", + "count": "29", + "used": "0" + }, + { + "id": "77947", + "box_id": "12", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "77948", + "box_id": "12", + "ornament_id": "9", + "audience": "anchor", + "count": "8", + "used": "0" + }, + { + "id": "77949", + "box_id": "12", + "ornament_id": "10", + "audience": "anchor", + "count": "6", + "used": "0" + }, + { + "id": "77950", + "box_id": "12", + "ornament_id": "11", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "77951", + "box_id": "12", + "ornament_id": "12", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "77952", + "box_id": "12", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "77953", + "box_id": "12", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "77954", + "box_id": "12", + "ornament_id": "1", + "audience": "robot", + "count": "1091", + "used": "0" + }, + { + "id": "77955", + "box_id": "12", + "ornament_id": "2", + "audience": "robot", + "count": "594", + "used": "0" + }, + { + "id": "77956", + "box_id": "12", + "ornament_id": "3", + "audience": "robot", + "count": "322", + "used": "0" + }, + { + "id": "77957", + "box_id": "12", + "ornament_id": "4", + "audience": "robot", + "count": "176", + "used": "0" + }, + { + "id": "77958", + "box_id": "12", + "ornament_id": "5", + "audience": "robot", + "count": "96", + "used": "0" + }, + { + "id": "77959", + "box_id": "12", + "ornament_id": "6", + "audience": "robot", + "count": "53", + "used": "0" + }, + { + "id": "77960", + "box_id": "12", + "ornament_id": "7", + "audience": "robot", + "count": "29", + "used": "0" + }, + { + "id": "77961", + "box_id": "12", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "77962", + "box_id": "12", + "ornament_id": "9", + "audience": "robot", + "count": "8", + "used": "0" + }, + { + "id": "77963", + "box_id": "12", + "ornament_id": "10", + "audience": "robot", + "count": "6", + "used": "0" + }, + { + "id": "77964", + "box_id": "12", + "ornament_id": "11", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "77965", + "box_id": "12", + "ornament_id": "12", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "77966", + "box_id": "12", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "77967", + "box_id": "12", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "77968", + "box_id": "12", + "ornament_id": "1", + "audience": "white", + "count": "1091", + "used": "0" + }, + { + "id": "77969", + "box_id": "12", + "ornament_id": "2", + "audience": "white", + "count": "594", + "used": "0" + }, + { + "id": "77970", + "box_id": "12", + "ornament_id": "3", + "audience": "white", + "count": "322", + "used": "0" + }, + { + "id": "77971", + "box_id": "12", + "ornament_id": "4", + "audience": "white", + "count": "176", + "used": "0" + }, + { + "id": "77972", + "box_id": "12", + "ornament_id": "5", + "audience": "white", + "count": "96", + "used": "0" + }, + { + "id": "77973", + "box_id": "12", + "ornament_id": "6", + "audience": "white", + "count": "53", + "used": "0" + }, + { + "id": "77974", + "box_id": "12", + "ornament_id": "7", + "audience": "white", + "count": "29", + "used": "0" + }, + { + "id": "77975", + "box_id": "12", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "77976", + "box_id": "12", + "ornament_id": "9", + "audience": "white", + "count": "8", + "used": "0" + }, + { + "id": "77977", + "box_id": "12", + "ornament_id": "10", + "audience": "white", + "count": "6", + "used": "0" + }, + { + "id": "77978", + "box_id": "12", + "ornament_id": "11", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "77979", + "box_id": "12", + "ornament_id": "12", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "77980", + "box_id": "12", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "77981", + "box_id": "12", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "77982", + "box_id": "13", + "ornament_id": "1", + "audience": "common", + "count": "1090", + "used": "0" + }, + { + "id": "77983", + "box_id": "13", + "ornament_id": "2", + "audience": "common", + "count": "593", + "used": "0" + }, + { + "id": "77984", + "box_id": "13", + "ornament_id": "3", + "audience": "common", + "count": "323", + "used": "0" + }, + { + "id": "77985", + "box_id": "13", + "ornament_id": "4", + "audience": "common", + "count": "176", + "used": "0" + }, + { + "id": "77986", + "box_id": "13", + "ornament_id": "5", + "audience": "common", + "count": "96", + "used": "0" + }, + { + "id": "77987", + "box_id": "13", + "ornament_id": "6", + "audience": "common", + "count": "53", + "used": "0" + }, + { + "id": "77988", + "box_id": "13", + "ornament_id": "7", + "audience": "common", + "count": "29", + "used": "0" + }, + { + "id": "77989", + "box_id": "13", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "77990", + "box_id": "13", + "ornament_id": "9", + "audience": "common", + "count": "9", + "used": "0" + }, + { + "id": "77991", + "box_id": "13", + "ornament_id": "10", + "audience": "common", + "count": "6", + "used": "0" + }, + { + "id": "77992", + "box_id": "13", + "ornament_id": "11", + "audience": "common", + "count": "4", + "used": "0" + }, + { + "id": "77993", + "box_id": "13", + "ornament_id": "12", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77994", + "box_id": "13", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "77995", + "box_id": "13", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "77996", + "box_id": "13", + "ornament_id": "1", + "audience": "anchor", + "count": "1090", + "used": "0" + }, + { + "id": "77997", + "box_id": "13", + "ornament_id": "2", + "audience": "anchor", + "count": "593", + "used": "0" + }, + { + "id": "77998", + "box_id": "13", + "ornament_id": "3", + "audience": "anchor", + "count": "323", + "used": "0" + }, + { + "id": "77999", + "box_id": "13", + "ornament_id": "4", + "audience": "anchor", + "count": "176", + "used": "0" + }, + { + "id": "78000", + "box_id": "13", + "ornament_id": "5", + "audience": "anchor", + "count": "96", + "used": "0" + }, + { + "id": "78001", + "box_id": "13", + "ornament_id": "6", + "audience": "anchor", + "count": "53", + "used": "0" + }, + { + "id": "78002", + "box_id": "13", + "ornament_id": "7", + "audience": "anchor", + "count": "29", + "used": "0" + }, + { + "id": "78003", + "box_id": "13", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78004", + "box_id": "13", + "ornament_id": "9", + "audience": "anchor", + "count": "9", + "used": "0" + }, + { + "id": "78005", + "box_id": "13", + "ornament_id": "10", + "audience": "anchor", + "count": "6", + "used": "0" + }, + { + "id": "78006", + "box_id": "13", + "ornament_id": "11", + "audience": "anchor", + "count": "4", + "used": "0" + }, + { + "id": "78007", + "box_id": "13", + "ornament_id": "12", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "78008", + "box_id": "13", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "78009", + "box_id": "13", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "78010", + "box_id": "13", + "ornament_id": "1", + "audience": "robot", + "count": "1090", + "used": "0" + }, + { + "id": "78011", + "box_id": "13", + "ornament_id": "2", + "audience": "robot", + "count": "593", + "used": "0" + }, + { + "id": "78012", + "box_id": "13", + "ornament_id": "3", + "audience": "robot", + "count": "323", + "used": "0" + }, + { + "id": "78013", + "box_id": "13", + "ornament_id": "4", + "audience": "robot", + "count": "176", + "used": "0" + }, + { + "id": "78014", + "box_id": "13", + "ornament_id": "5", + "audience": "robot", + "count": "96", + "used": "0" + }, + { + "id": "78015", + "box_id": "13", + "ornament_id": "6", + "audience": "robot", + "count": "53", + "used": "0" + }, + { + "id": "78016", + "box_id": "13", + "ornament_id": "7", + "audience": "robot", + "count": "29", + "used": "0" + }, + { + "id": "78017", + "box_id": "13", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78018", + "box_id": "13", + "ornament_id": "9", + "audience": "robot", + "count": "9", + "used": "0" + }, + { + "id": "78019", + "box_id": "13", + "ornament_id": "10", + "audience": "robot", + "count": "6", + "used": "0" + }, + { + "id": "78020", + "box_id": "13", + "ornament_id": "11", + "audience": "robot", + "count": "4", + "used": "0" + }, + { + "id": "78021", + "box_id": "13", + "ornament_id": "12", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "78022", + "box_id": "13", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "78023", + "box_id": "13", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "78024", + "box_id": "13", + "ornament_id": "1", + "audience": "white", + "count": "1090", + "used": "0" + }, + { + "id": "78025", + "box_id": "13", + "ornament_id": "2", + "audience": "white", + "count": "593", + "used": "0" + }, + { + "id": "78026", + "box_id": "13", + "ornament_id": "3", + "audience": "white", + "count": "323", + "used": "0" + }, + { + "id": "78027", + "box_id": "13", + "ornament_id": "4", + "audience": "white", + "count": "176", + "used": "0" + }, + { + "id": "78028", + "box_id": "13", + "ornament_id": "5", + "audience": "white", + "count": "96", + "used": "0" + }, + { + "id": "78029", + "box_id": "13", + "ornament_id": "6", + "audience": "white", + "count": "53", + "used": "0" + }, + { + "id": "78030", + "box_id": "13", + "ornament_id": "7", + "audience": "white", + "count": "29", + "used": "0" + }, + { + "id": "78031", + "box_id": "13", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78032", + "box_id": "13", + "ornament_id": "9", + "audience": "white", + "count": "9", + "used": "0" + }, + { + "id": "78033", + "box_id": "13", + "ornament_id": "10", + "audience": "white", + "count": "6", + "used": "0" + }, + { + "id": "78034", + "box_id": "13", + "ornament_id": "11", + "audience": "white", + "count": "4", + "used": "0" + }, + { + "id": "78035", + "box_id": "13", + "ornament_id": "12", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "78036", + "box_id": "13", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "78037", + "box_id": "13", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "78038", + "box_id": "14", + "ornament_id": "1", + "audience": "common", + "count": "1097", + "used": "0" + }, + { + "id": "78039", + "box_id": "14", + "ornament_id": "2", + "audience": "common", + "count": "593", + "used": "0" + }, + { + "id": "78040", + "box_id": "14", + "ornament_id": "3", + "audience": "common", + "count": "322", + "used": "0" + }, + { + "id": "78041", + "box_id": "14", + "ornament_id": "4", + "audience": "common", + "count": "175", + "used": "0" + }, + { + "id": "78042", + "box_id": "14", + "ornament_id": "5", + "audience": "common", + "count": "94", + "used": "0" + }, + { + "id": "78043", + "box_id": "14", + "ornament_id": "6", + "audience": "common", + "count": "52", + "used": "0" + }, + { + "id": "78044", + "box_id": "14", + "ornament_id": "7", + "audience": "common", + "count": "28", + "used": "0" + }, + { + "id": "78045", + "box_id": "14", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78046", + "box_id": "14", + "ornament_id": "9", + "audience": "common", + "count": "9", + "used": "0" + }, + { + "id": "78047", + "box_id": "14", + "ornament_id": "10", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "78048", + "box_id": "14", + "ornament_id": "11", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78049", + "box_id": "14", + "ornament_id": "12", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78050", + "box_id": "14", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "78051", + "box_id": "14", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "78052", + "box_id": "14", + "ornament_id": "1", + "audience": "anchor", + "count": "1097", + "used": "0" + }, + { + "id": "78053", + "box_id": "14", + "ornament_id": "2", + "audience": "anchor", + "count": "593", + "used": "0" + }, + { + "id": "78054", + "box_id": "14", + "ornament_id": "3", + "audience": "anchor", + "count": "322", + "used": "0" + }, + { + "id": "78055", + "box_id": "14", + "ornament_id": "4", + "audience": "anchor", + "count": "175", + "used": "0" + }, + { + "id": "78056", + "box_id": "14", + "ornament_id": "5", + "audience": "anchor", + "count": "94", + "used": "0" + }, + { + "id": "78057", + "box_id": "14", + "ornament_id": "6", + "audience": "anchor", + "count": "52", + "used": "0" + }, + { + "id": "78058", + "box_id": "14", + "ornament_id": "7", + "audience": "anchor", + "count": "28", + "used": "0" + }, + { + "id": "78059", + "box_id": "14", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78060", + "box_id": "14", + "ornament_id": "9", + "audience": "anchor", + "count": "9", + "used": "0" + }, + { + "id": "78061", + "box_id": "14", + "ornament_id": "10", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "78062", + "box_id": "14", + "ornament_id": "11", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78063", + "box_id": "14", + "ornament_id": "12", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78064", + "box_id": "14", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "78065", + "box_id": "14", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "78066", + "box_id": "14", + "ornament_id": "1", + "audience": "robot", + "count": "1097", + "used": "0" + }, + { + "id": "78067", + "box_id": "14", + "ornament_id": "2", + "audience": "robot", + "count": "593", + "used": "0" + }, + { + "id": "78068", + "box_id": "14", + "ornament_id": "3", + "audience": "robot", + "count": "322", + "used": "0" + }, + { + "id": "78069", + "box_id": "14", + "ornament_id": "4", + "audience": "robot", + "count": "175", + "used": "0" + }, + { + "id": "78070", + "box_id": "14", + "ornament_id": "5", + "audience": "robot", + "count": "94", + "used": "0" + }, + { + "id": "78071", + "box_id": "14", + "ornament_id": "6", + "audience": "robot", + "count": "52", + "used": "0" + }, + { + "id": "78072", + "box_id": "14", + "ornament_id": "7", + "audience": "robot", + "count": "28", + "used": "0" + }, + { + "id": "78073", + "box_id": "14", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78074", + "box_id": "14", + "ornament_id": "9", + "audience": "robot", + "count": "9", + "used": "0" + }, + { + "id": "78075", + "box_id": "14", + "ornament_id": "10", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "78076", + "box_id": "14", + "ornament_id": "11", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78077", + "box_id": "14", + "ornament_id": "12", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78078", + "box_id": "14", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "78079", + "box_id": "14", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "78080", + "box_id": "14", + "ornament_id": "1", + "audience": "white", + "count": "1097", + "used": "0" + }, + { + "id": "78081", + "box_id": "14", + "ornament_id": "2", + "audience": "white", + "count": "593", + "used": "0" + }, + { + "id": "78082", + "box_id": "14", + "ornament_id": "3", + "audience": "white", + "count": "322", + "used": "0" + }, + { + "id": "78083", + "box_id": "14", + "ornament_id": "4", + "audience": "white", + "count": "175", + "used": "0" + }, + { + "id": "78084", + "box_id": "14", + "ornament_id": "5", + "audience": "white", + "count": "94", + "used": "0" + }, + { + "id": "78085", + "box_id": "14", + "ornament_id": "6", + "audience": "white", + "count": "52", + "used": "0" + }, + { + "id": "78086", + "box_id": "14", + "ornament_id": "7", + "audience": "white", + "count": "28", + "used": "0" + }, + { + "id": "78087", + "box_id": "14", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78088", + "box_id": "14", + "ornament_id": "9", + "audience": "white", + "count": "9", + "used": "0" + }, + { + "id": "78089", + "box_id": "14", + "ornament_id": "10", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "78090", + "box_id": "14", + "ornament_id": "11", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78091", + "box_id": "14", + "ornament_id": "12", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78092", + "box_id": "14", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "78093", + "box_id": "14", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "78094", + "box_id": "15", + "ornament_id": "1", + "audience": "common", + "count": "1091", + "used": "0" + }, + { + "id": "78095", + "box_id": "15", + "ornament_id": "2", + "audience": "common", + "count": "594", + "used": "0" + }, + { + "id": "78096", + "box_id": "15", + "ornament_id": "3", + "audience": "common", + "count": "323", + "used": "0" + }, + { + "id": "78097", + "box_id": "15", + "ornament_id": "4", + "audience": "common", + "count": "175", + "used": "0" + }, + { + "id": "78098", + "box_id": "15", + "ornament_id": "5", + "audience": "common", + "count": "96", + "used": "0" + }, + { + "id": "78099", + "box_id": "15", + "ornament_id": "6", + "audience": "common", + "count": "53", + "used": "0" + }, + { + "id": "78100", + "box_id": "15", + "ornament_id": "7", + "audience": "common", + "count": "29", + "used": "0" + }, + { + "id": "78101", + "box_id": "15", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78102", + "box_id": "15", + "ornament_id": "9", + "audience": "common", + "count": "8", + "used": "0" + }, + { + "id": "78103", + "box_id": "15", + "ornament_id": "10", + "audience": "common", + "count": "6", + "used": "0" + }, + { + "id": "78104", + "box_id": "15", + "ornament_id": "11", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78105", + "box_id": "15", + "ornament_id": "12", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78106", + "box_id": "15", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "78107", + "box_id": "15", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "78108", + "box_id": "15", + "ornament_id": "1", + "audience": "anchor", + "count": "1091", + "used": "0" + }, + { + "id": "78109", + "box_id": "15", + "ornament_id": "2", + "audience": "anchor", + "count": "594", + "used": "0" + }, + { + "id": "78110", + "box_id": "15", + "ornament_id": "3", + "audience": "anchor", + "count": "323", + "used": "0" + }, + { + "id": "78111", + "box_id": "15", + "ornament_id": "4", + "audience": "anchor", + "count": "175", + "used": "0" + }, + { + "id": "78112", + "box_id": "15", + "ornament_id": "5", + "audience": "anchor", + "count": "96", + "used": "0" + }, + { + "id": "78113", + "box_id": "15", + "ornament_id": "6", + "audience": "anchor", + "count": "53", + "used": "0" + }, + { + "id": "78114", + "box_id": "15", + "ornament_id": "7", + "audience": "anchor", + "count": "29", + "used": "0" + }, + { + "id": "78115", + "box_id": "15", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78116", + "box_id": "15", + "ornament_id": "9", + "audience": "anchor", + "count": "8", + "used": "0" + }, + { + "id": "78117", + "box_id": "15", + "ornament_id": "10", + "audience": "anchor", + "count": "6", + "used": "0" + }, + { + "id": "78118", + "box_id": "15", + "ornament_id": "11", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78119", + "box_id": "15", + "ornament_id": "12", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78120", + "box_id": "15", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "78121", + "box_id": "15", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "78122", + "box_id": "15", + "ornament_id": "1", + "audience": "robot", + "count": "1091", + "used": "0" + }, + { + "id": "78123", + "box_id": "15", + "ornament_id": "2", + "audience": "robot", + "count": "594", + "used": "0" + }, + { + "id": "78124", + "box_id": "15", + "ornament_id": "3", + "audience": "robot", + "count": "323", + "used": "0" + }, + { + "id": "78125", + "box_id": "15", + "ornament_id": "4", + "audience": "robot", + "count": "175", + "used": "0" + }, + { + "id": "78126", + "box_id": "15", + "ornament_id": "5", + "audience": "robot", + "count": "96", + "used": "0" + }, + { + "id": "78127", + "box_id": "15", + "ornament_id": "6", + "audience": "robot", + "count": "53", + "used": "0" + }, + { + "id": "78128", + "box_id": "15", + "ornament_id": "7", + "audience": "robot", + "count": "29", + "used": "0" + }, + { + "id": "78129", + "box_id": "15", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78130", + "box_id": "15", + "ornament_id": "9", + "audience": "robot", + "count": "8", + "used": "0" + }, + { + "id": "78131", + "box_id": "15", + "ornament_id": "10", + "audience": "robot", + "count": "6", + "used": "0" + }, + { + "id": "78132", + "box_id": "15", + "ornament_id": "11", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78133", + "box_id": "15", + "ornament_id": "12", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78134", + "box_id": "15", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "78135", + "box_id": "15", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "78136", + "box_id": "15", + "ornament_id": "1", + "audience": "white", + "count": "1091", + "used": "0" + }, + { + "id": "78137", + "box_id": "15", + "ornament_id": "2", + "audience": "white", + "count": "594", + "used": "0" + }, + { + "id": "78138", + "box_id": "15", + "ornament_id": "3", + "audience": "white", + "count": "323", + "used": "0" + }, + { + "id": "78139", + "box_id": "15", + "ornament_id": "4", + "audience": "white", + "count": "175", + "used": "0" + }, + { + "id": "78140", + "box_id": "15", + "ornament_id": "5", + "audience": "white", + "count": "96", + "used": "0" + }, + { + "id": "78141", + "box_id": "15", + "ornament_id": "6", + "audience": "white", + "count": "53", + "used": "0" + }, + { + "id": "78142", + "box_id": "15", + "ornament_id": "7", + "audience": "white", + "count": "29", + "used": "0" + }, + { + "id": "78143", + "box_id": "15", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78144", + "box_id": "15", + "ornament_id": "9", + "audience": "white", + "count": "8", + "used": "0" + }, + { + "id": "78145", + "box_id": "15", + "ornament_id": "10", + "audience": "white", + "count": "6", + "used": "0" + }, + { + "id": "78146", + "box_id": "15", + "ornament_id": "11", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78147", + "box_id": "15", + "ornament_id": "12", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78148", + "box_id": "15", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "78149", + "box_id": "15", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "78150", + "box_id": "16", + "ornament_id": "1", + "audience": "common", + "count": "1097", + "used": "0" + }, + { + "id": "78151", + "box_id": "16", + "ornament_id": "2", + "audience": "common", + "count": "593", + "used": "0" + }, + { + "id": "78152", + "box_id": "16", + "ornament_id": "3", + "audience": "common", + "count": "321", + "used": "0" + }, + { + "id": "78153", + "box_id": "16", + "ornament_id": "4", + "audience": "common", + "count": "175", + "used": "0" + }, + { + "id": "78154", + "box_id": "16", + "ornament_id": "5", + "audience": "common", + "count": "95", + "used": "0" + }, + { + "id": "78155", + "box_id": "16", + "ornament_id": "6", + "audience": "common", + "count": "52", + "used": "0" + }, + { + "id": "78156", + "box_id": "16", + "ornament_id": "7", + "audience": "common", + "count": "28", + "used": "0" + }, + { + "id": "78157", + "box_id": "16", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78158", + "box_id": "16", + "ornament_id": "9", + "audience": "common", + "count": "9", + "used": "0" + }, + { + "id": "78159", + "box_id": "16", + "ornament_id": "10", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "78160", + "box_id": "16", + "ornament_id": "11", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78161", + "box_id": "16", + "ornament_id": "12", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78162", + "box_id": "16", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "78163", + "box_id": "16", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "78164", + "box_id": "16", + "ornament_id": "1", + "audience": "anchor", + "count": "1097", + "used": "0" + }, + { + "id": "78165", + "box_id": "16", + "ornament_id": "2", + "audience": "anchor", + "count": "593", + "used": "0" + }, + { + "id": "78166", + "box_id": "16", + "ornament_id": "3", + "audience": "anchor", + "count": "321", + "used": "0" + }, + { + "id": "78167", + "box_id": "16", + "ornament_id": "4", + "audience": "anchor", + "count": "175", + "used": "0" + }, + { + "id": "78168", + "box_id": "16", + "ornament_id": "5", + "audience": "anchor", + "count": "95", + "used": "0" + }, + { + "id": "78169", + "box_id": "16", + "ornament_id": "6", + "audience": "anchor", + "count": "52", + "used": "0" + }, + { + "id": "78170", + "box_id": "16", + "ornament_id": "7", + "audience": "anchor", + "count": "28", + "used": "0" + }, + { + "id": "78171", + "box_id": "16", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78172", + "box_id": "16", + "ornament_id": "9", + "audience": "anchor", + "count": "9", + "used": "0" + }, + { + "id": "78173", + "box_id": "16", + "ornament_id": "10", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "78174", + "box_id": "16", + "ornament_id": "11", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78175", + "box_id": "16", + "ornament_id": "12", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78176", + "box_id": "16", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "78177", + "box_id": "16", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "78178", + "box_id": "16", + "ornament_id": "1", + "audience": "robot", + "count": "1097", + "used": "0" + }, + { + "id": "78179", + "box_id": "16", + "ornament_id": "2", + "audience": "robot", + "count": "593", + "used": "0" + }, + { + "id": "78180", + "box_id": "16", + "ornament_id": "3", + "audience": "robot", + "count": "321", + "used": "0" + }, + { + "id": "78181", + "box_id": "16", + "ornament_id": "4", + "audience": "robot", + "count": "175", + "used": "0" + }, + { + "id": "78182", + "box_id": "16", + "ornament_id": "5", + "audience": "robot", + "count": "95", + "used": "0" + }, + { + "id": "78183", + "box_id": "16", + "ornament_id": "6", + "audience": "robot", + "count": "52", + "used": "0" + }, + { + "id": "78184", + "box_id": "16", + "ornament_id": "7", + "audience": "robot", + "count": "28", + "used": "0" + }, + { + "id": "78185", + "box_id": "16", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78186", + "box_id": "16", + "ornament_id": "9", + "audience": "robot", + "count": "9", + "used": "0" + }, + { + "id": "78187", + "box_id": "16", + "ornament_id": "10", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "78188", + "box_id": "16", + "ornament_id": "11", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78189", + "box_id": "16", + "ornament_id": "12", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78190", + "box_id": "16", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "78191", + "box_id": "16", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "78192", + "box_id": "16", + "ornament_id": "1", + "audience": "white", + "count": "1097", + "used": "0" + }, + { + "id": "78193", + "box_id": "16", + "ornament_id": "2", + "audience": "white", + "count": "593", + "used": "0" + }, + { + "id": "78194", + "box_id": "16", + "ornament_id": "3", + "audience": "white", + "count": "321", + "used": "0" + }, + { + "id": "78195", + "box_id": "16", + "ornament_id": "4", + "audience": "white", + "count": "175", + "used": "0" + }, + { + "id": "78196", + "box_id": "16", + "ornament_id": "5", + "audience": "white", + "count": "95", + "used": "0" + }, + { + "id": "78197", + "box_id": "16", + "ornament_id": "6", + "audience": "white", + "count": "52", + "used": "0" + }, + { + "id": "78198", + "box_id": "16", + "ornament_id": "7", + "audience": "white", + "count": "28", + "used": "0" + }, + { + "id": "78199", + "box_id": "16", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78200", + "box_id": "16", + "ornament_id": "9", + "audience": "white", + "count": "9", + "used": "0" + }, + { + "id": "78201", + "box_id": "16", + "ornament_id": "10", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "78202", + "box_id": "16", + "ornament_id": "11", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78203", + "box_id": "16", + "ornament_id": "12", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78204", + "box_id": "16", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "78205", + "box_id": "16", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "78206", + "box_id": "17", + "ornament_id": "1", + "audience": "common", + "count": "1094", + "used": "0" + }, + { + "id": "78207", + "box_id": "17", + "ornament_id": "2", + "audience": "common", + "count": "594", + "used": "0" + }, + { + "id": "78208", + "box_id": "17", + "ornament_id": "3", + "audience": "common", + "count": "322", + "used": "0" + }, + { + "id": "78209", + "box_id": "17", + "ornament_id": "4", + "audience": "common", + "count": "175", + "used": "0" + }, + { + "id": "78210", + "box_id": "17", + "ornament_id": "5", + "audience": "common", + "count": "95", + "used": "0" + }, + { + "id": "78211", + "box_id": "17", + "ornament_id": "6", + "audience": "common", + "count": "52", + "used": "0" + }, + { + "id": "78212", + "box_id": "17", + "ornament_id": "7", + "audience": "common", + "count": "29", + "used": "0" + }, + { + "id": "78213", + "box_id": "17", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78214", + "box_id": "17", + "ornament_id": "9", + "audience": "common", + "count": "9", + "used": "0" + }, + { + "id": "78215", + "box_id": "17", + "ornament_id": "10", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "78216", + "box_id": "17", + "ornament_id": "11", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78217", + "box_id": "17", + "ornament_id": "12", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78218", + "box_id": "17", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "78219", + "box_id": "17", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "78220", + "box_id": "17", + "ornament_id": "1", + "audience": "anchor", + "count": "1094", + "used": "0" + }, + { + "id": "78221", + "box_id": "17", + "ornament_id": "2", + "audience": "anchor", + "count": "594", + "used": "0" + }, + { + "id": "78222", + "box_id": "17", + "ornament_id": "3", + "audience": "anchor", + "count": "322", + "used": "0" + }, + { + "id": "78223", + "box_id": "17", + "ornament_id": "4", + "audience": "anchor", + "count": "175", + "used": "0" + }, + { + "id": "78224", + "box_id": "17", + "ornament_id": "5", + "audience": "anchor", + "count": "95", + "used": "0" + }, + { + "id": "78225", + "box_id": "17", + "ornament_id": "6", + "audience": "anchor", + "count": "52", + "used": "0" + }, + { + "id": "78226", + "box_id": "17", + "ornament_id": "7", + "audience": "anchor", + "count": "29", + "used": "0" + }, + { + "id": "78227", + "box_id": "17", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78228", + "box_id": "17", + "ornament_id": "9", + "audience": "anchor", + "count": "9", + "used": "0" + }, + { + "id": "78229", + "box_id": "17", + "ornament_id": "10", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "78230", + "box_id": "17", + "ornament_id": "11", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78231", + "box_id": "17", + "ornament_id": "12", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78232", + "box_id": "17", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "78233", + "box_id": "17", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "78234", + "box_id": "17", + "ornament_id": "1", + "audience": "robot", + "count": "1094", + "used": "0" + }, + { + "id": "78235", + "box_id": "17", + "ornament_id": "2", + "audience": "robot", + "count": "594", + "used": "0" + }, + { + "id": "78236", + "box_id": "17", + "ornament_id": "3", + "audience": "robot", + "count": "322", + "used": "0" + }, + { + "id": "78237", + "box_id": "17", + "ornament_id": "4", + "audience": "robot", + "count": "175", + "used": "0" + }, + { + "id": "78238", + "box_id": "17", + "ornament_id": "5", + "audience": "robot", + "count": "95", + "used": "0" + }, + { + "id": "78239", + "box_id": "17", + "ornament_id": "6", + "audience": "robot", + "count": "52", + "used": "0" + }, + { + "id": "78240", + "box_id": "17", + "ornament_id": "7", + "audience": "robot", + "count": "29", + "used": "0" + }, + { + "id": "78241", + "box_id": "17", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78242", + "box_id": "17", + "ornament_id": "9", + "audience": "robot", + "count": "9", + "used": "0" + }, + { + "id": "78243", + "box_id": "17", + "ornament_id": "10", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "78244", + "box_id": "17", + "ornament_id": "11", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78245", + "box_id": "17", + "ornament_id": "12", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78246", + "box_id": "17", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "78247", + "box_id": "17", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "78248", + "box_id": "17", + "ornament_id": "1", + "audience": "white", + "count": "1094", + "used": "0" + }, + { + "id": "78249", + "box_id": "17", + "ornament_id": "2", + "audience": "white", + "count": "594", + "used": "0" + }, + { + "id": "78250", + "box_id": "17", + "ornament_id": "3", + "audience": "white", + "count": "322", + "used": "0" + }, + { + "id": "78251", + "box_id": "17", + "ornament_id": "4", + "audience": "white", + "count": "175", + "used": "0" + }, + { + "id": "78252", + "box_id": "17", + "ornament_id": "5", + "audience": "white", + "count": "95", + "used": "0" + }, + { + "id": "78253", + "box_id": "17", + "ornament_id": "6", + "audience": "white", + "count": "52", + "used": "0" + }, + { + "id": "78254", + "box_id": "17", + "ornament_id": "7", + "audience": "white", + "count": "29", + "used": "0" + }, + { + "id": "78255", + "box_id": "17", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78256", + "box_id": "17", + "ornament_id": "9", + "audience": "white", + "count": "9", + "used": "0" + }, + { + "id": "78257", + "box_id": "17", + "ornament_id": "10", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "78258", + "box_id": "17", + "ornament_id": "11", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78259", + "box_id": "17", + "ornament_id": "12", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78260", + "box_id": "17", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "78261", + "box_id": "17", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "78262", + "box_id": "18", + "ornament_id": "1", + "audience": "common", + "count": "1096", + "used": "0" + }, + { + "id": "78263", + "box_id": "18", + "ornament_id": "2", + "audience": "common", + "count": "593", + "used": "0" + }, + { + "id": "78264", + "box_id": "18", + "ornament_id": "3", + "audience": "common", + "count": "323", + "used": "0" + }, + { + "id": "78265", + "box_id": "18", + "ornament_id": "4", + "audience": "common", + "count": "175", + "used": "0" + }, + { + "id": "78266", + "box_id": "18", + "ornament_id": "5", + "audience": "common", + "count": "95", + "used": "0" + }, + { + "id": "78267", + "box_id": "18", + "ornament_id": "6", + "audience": "common", + "count": "52", + "used": "0" + }, + { + "id": "78268", + "box_id": "18", + "ornament_id": "7", + "audience": "common", + "count": "27", + "used": "0" + }, + { + "id": "78269", + "box_id": "18", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78270", + "box_id": "18", + "ornament_id": "9", + "audience": "common", + "count": "9", + "used": "0" + }, + { + "id": "78271", + "box_id": "18", + "ornament_id": "10", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "78272", + "box_id": "18", + "ornament_id": "11", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78273", + "box_id": "18", + "ornament_id": "12", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78274", + "box_id": "18", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "78275", + "box_id": "18", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "78276", + "box_id": "18", + "ornament_id": "1", + "audience": "anchor", + "count": "1096", + "used": "0" + }, + { + "id": "78277", + "box_id": "18", + "ornament_id": "2", + "audience": "anchor", + "count": "593", + "used": "0" + }, + { + "id": "78278", + "box_id": "18", + "ornament_id": "3", + "audience": "anchor", + "count": "323", + "used": "0" + }, + { + "id": "78279", + "box_id": "18", + "ornament_id": "4", + "audience": "anchor", + "count": "175", + "used": "0" + }, + { + "id": "78280", + "box_id": "18", + "ornament_id": "5", + "audience": "anchor", + "count": "95", + "used": "0" + }, + { + "id": "78281", + "box_id": "18", + "ornament_id": "6", + "audience": "anchor", + "count": "52", + "used": "0" + }, + { + "id": "78282", + "box_id": "18", + "ornament_id": "7", + "audience": "anchor", + "count": "27", + "used": "0" + }, + { + "id": "78283", + "box_id": "18", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78284", + "box_id": "18", + "ornament_id": "9", + "audience": "anchor", + "count": "9", + "used": "0" + }, + { + "id": "78285", + "box_id": "18", + "ornament_id": "10", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "78286", + "box_id": "18", + "ornament_id": "11", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78287", + "box_id": "18", + "ornament_id": "12", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78288", + "box_id": "18", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "78289", + "box_id": "18", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "78290", + "box_id": "18", + "ornament_id": "1", + "audience": "robot", + "count": "1096", + "used": "0" + }, + { + "id": "78291", + "box_id": "18", + "ornament_id": "2", + "audience": "robot", + "count": "593", + "used": "0" + }, + { + "id": "78292", + "box_id": "18", + "ornament_id": "3", + "audience": "robot", + "count": "323", + "used": "0" + }, + { + "id": "78293", + "box_id": "18", + "ornament_id": "4", + "audience": "robot", + "count": "175", + "used": "0" + }, + { + "id": "78294", + "box_id": "18", + "ornament_id": "5", + "audience": "robot", + "count": "95", + "used": "0" + }, + { + "id": "78295", + "box_id": "18", + "ornament_id": "6", + "audience": "robot", + "count": "52", + "used": "0" + }, + { + "id": "78296", + "box_id": "18", + "ornament_id": "7", + "audience": "robot", + "count": "27", + "used": "0" + }, + { + "id": "78297", + "box_id": "18", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78298", + "box_id": "18", + "ornament_id": "9", + "audience": "robot", + "count": "9", + "used": "0" + }, + { + "id": "78299", + "box_id": "18", + "ornament_id": "10", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "78300", + "box_id": "18", + "ornament_id": "11", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78301", + "box_id": "18", + "ornament_id": "12", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78302", + "box_id": "18", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "78303", + "box_id": "18", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "78304", + "box_id": "18", + "ornament_id": "1", + "audience": "white", + "count": "1096", + "used": "0" + }, + { + "id": "78305", + "box_id": "18", + "ornament_id": "2", + "audience": "white", + "count": "593", + "used": "0" + }, + { + "id": "78306", + "box_id": "18", + "ornament_id": "3", + "audience": "white", + "count": "323", + "used": "0" + }, + { + "id": "78307", + "box_id": "18", + "ornament_id": "4", + "audience": "white", + "count": "175", + "used": "0" + }, + { + "id": "78308", + "box_id": "18", + "ornament_id": "5", + "audience": "white", + "count": "95", + "used": "0" + }, + { + "id": "78309", + "box_id": "18", + "ornament_id": "6", + "audience": "white", + "count": "52", + "used": "0" + }, + { + "id": "78310", + "box_id": "18", + "ornament_id": "7", + "audience": "white", + "count": "27", + "used": "0" + }, + { + "id": "78311", + "box_id": "18", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78312", + "box_id": "18", + "ornament_id": "9", + "audience": "white", + "count": "9", + "used": "0" + }, + { + "id": "78313", + "box_id": "18", + "ornament_id": "10", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "78314", + "box_id": "18", + "ornament_id": "11", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78315", + "box_id": "18", + "ornament_id": "12", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78316", + "box_id": "18", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "78317", + "box_id": "18", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "78318", + "box_id": "19", + "ornament_id": "1", + "audience": "common", + "count": "1091", + "used": "0" + }, + { + "id": "78319", + "box_id": "19", + "ornament_id": "2", + "audience": "common", + "count": "594", + "used": "0" + }, + { + "id": "78320", + "box_id": "19", + "ornament_id": "3", + "audience": "common", + "count": "323", + "used": "0" + }, + { + "id": "78321", + "box_id": "19", + "ornament_id": "4", + "audience": "common", + "count": "176", + "used": "0" + }, + { + "id": "78322", + "box_id": "19", + "ornament_id": "5", + "audience": "common", + "count": "95", + "used": "0" + }, + { + "id": "78323", + "box_id": "19", + "ornament_id": "6", + "audience": "common", + "count": "53", + "used": "0" + }, + { + "id": "78324", + "box_id": "19", + "ornament_id": "7", + "audience": "common", + "count": "29", + "used": "0" + }, + { + "id": "78325", + "box_id": "19", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78326", + "box_id": "19", + "ornament_id": "9", + "audience": "common", + "count": "9", + "used": "0" + }, + { + "id": "78327", + "box_id": "19", + "ornament_id": "10", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "78328", + "box_id": "19", + "ornament_id": "11", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78329", + "box_id": "19", + "ornament_id": "12", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78330", + "box_id": "19", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "78331", + "box_id": "19", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "78332", + "box_id": "19", + "ornament_id": "1", + "audience": "anchor", + "count": "1091", + "used": "0" + }, + { + "id": "78333", + "box_id": "19", + "ornament_id": "2", + "audience": "anchor", + "count": "594", + "used": "0" + }, + { + "id": "78334", + "box_id": "19", + "ornament_id": "3", + "audience": "anchor", + "count": "323", + "used": "0" + }, + { + "id": "78335", + "box_id": "19", + "ornament_id": "4", + "audience": "anchor", + "count": "176", + "used": "0" + }, + { + "id": "78336", + "box_id": "19", + "ornament_id": "5", + "audience": "anchor", + "count": "95", + "used": "0" + }, + { + "id": "78337", + "box_id": "19", + "ornament_id": "6", + "audience": "anchor", + "count": "53", + "used": "0" + }, + { + "id": "78338", + "box_id": "19", + "ornament_id": "7", + "audience": "anchor", + "count": "29", + "used": "0" + }, + { + "id": "78339", + "box_id": "19", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78340", + "box_id": "19", + "ornament_id": "9", + "audience": "anchor", + "count": "9", + "used": "0" + }, + { + "id": "78341", + "box_id": "19", + "ornament_id": "10", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "78342", + "box_id": "19", + "ornament_id": "11", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78343", + "box_id": "19", + "ornament_id": "12", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78344", + "box_id": "19", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "78345", + "box_id": "19", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "78346", + "box_id": "19", + "ornament_id": "1", + "audience": "robot", + "count": "1091", + "used": "0" + }, + { + "id": "78347", + "box_id": "19", + "ornament_id": "2", + "audience": "robot", + "count": "594", + "used": "0" + }, + { + "id": "78348", + "box_id": "19", + "ornament_id": "3", + "audience": "robot", + "count": "323", + "used": "0" + }, + { + "id": "78349", + "box_id": "19", + "ornament_id": "4", + "audience": "robot", + "count": "176", + "used": "0" + }, + { + "id": "78350", + "box_id": "19", + "ornament_id": "5", + "audience": "robot", + "count": "95", + "used": "0" + }, + { + "id": "78351", + "box_id": "19", + "ornament_id": "6", + "audience": "robot", + "count": "53", + "used": "0" + }, + { + "id": "78352", + "box_id": "19", + "ornament_id": "7", + "audience": "robot", + "count": "29", + "used": "0" + }, + { + "id": "78353", + "box_id": "19", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78354", + "box_id": "19", + "ornament_id": "9", + "audience": "robot", + "count": "9", + "used": "0" + }, + { + "id": "78355", + "box_id": "19", + "ornament_id": "10", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "78356", + "box_id": "19", + "ornament_id": "11", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78357", + "box_id": "19", + "ornament_id": "12", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78358", + "box_id": "19", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "78359", + "box_id": "19", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "78360", + "box_id": "19", + "ornament_id": "1", + "audience": "white", + "count": "1091", + "used": "0" + }, + { + "id": "78361", + "box_id": "19", + "ornament_id": "2", + "audience": "white", + "count": "594", + "used": "0" + }, + { + "id": "78362", + "box_id": "19", + "ornament_id": "3", + "audience": "white", + "count": "323", + "used": "0" + }, + { + "id": "78363", + "box_id": "19", + "ornament_id": "4", + "audience": "white", + "count": "176", + "used": "0" + }, + { + "id": "78364", + "box_id": "19", + "ornament_id": "5", + "audience": "white", + "count": "95", + "used": "0" + }, + { + "id": "78365", + "box_id": "19", + "ornament_id": "6", + "audience": "white", + "count": "53", + "used": "0" + }, + { + "id": "78366", + "box_id": "19", + "ornament_id": "7", + "audience": "white", + "count": "29", + "used": "0" + }, + { + "id": "78367", + "box_id": "19", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78368", + "box_id": "19", + "ornament_id": "9", + "audience": "white", + "count": "9", + "used": "0" + }, + { + "id": "78369", + "box_id": "19", + "ornament_id": "10", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "78370", + "box_id": "19", + "ornament_id": "11", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78371", + "box_id": "19", + "ornament_id": "12", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78372", + "box_id": "19", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "78373", + "box_id": "19", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "78374", + "box_id": "20", + "ornament_id": "1", + "audience": "common", + "count": "1090", + "used": "0" + }, + { + "id": "78375", + "box_id": "20", + "ornament_id": "2", + "audience": "common", + "count": "594", + "used": "0" + }, + { + "id": "78376", + "box_id": "20", + "ornament_id": "3", + "audience": "common", + "count": "323", + "used": "0" + }, + { + "id": "78377", + "box_id": "20", + "ornament_id": "4", + "audience": "common", + "count": "176", + "used": "0" + }, + { + "id": "78378", + "box_id": "20", + "ornament_id": "5", + "audience": "common", + "count": "96", + "used": "0" + }, + { + "id": "78379", + "box_id": "20", + "ornament_id": "6", + "audience": "common", + "count": "53", + "used": "0" + }, + { + "id": "78380", + "box_id": "20", + "ornament_id": "7", + "audience": "common", + "count": "29", + "used": "0" + }, + { + "id": "78381", + "box_id": "20", + "ornament_id": "8", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78382", + "box_id": "20", + "ornament_id": "9", + "audience": "common", + "count": "9", + "used": "0" + }, + { + "id": "78383", + "box_id": "20", + "ornament_id": "10", + "audience": "common", + "count": "5", + "used": "0" + }, + { + "id": "78384", + "box_id": "20", + "ornament_id": "11", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78385", + "box_id": "20", + "ornament_id": "12", + "audience": "common", + "count": "3", + "used": "0" + }, + { + "id": "78386", + "box_id": "20", + "ornament_id": "13", + "audience": "common", + "count": "2", + "used": "0" + }, + { + "id": "78387", + "box_id": "20", + "ornament_id": "14", + "audience": "common", + "count": "1", + "used": "0" + }, + { + "id": "78388", + "box_id": "20", + "ornament_id": "1", + "audience": "anchor", + "count": "1090", + "used": "0" + }, + { + "id": "78389", + "box_id": "20", + "ornament_id": "2", + "audience": "anchor", + "count": "594", + "used": "0" + }, + { + "id": "78390", + "box_id": "20", + "ornament_id": "3", + "audience": "anchor", + "count": "323", + "used": "0" + }, + { + "id": "78391", + "box_id": "20", + "ornament_id": "4", + "audience": "anchor", + "count": "176", + "used": "0" + }, + { + "id": "78392", + "box_id": "20", + "ornament_id": "5", + "audience": "anchor", + "count": "96", + "used": "0" + }, + { + "id": "78393", + "box_id": "20", + "ornament_id": "6", + "audience": "anchor", + "count": "53", + "used": "0" + }, + { + "id": "78394", + "box_id": "20", + "ornament_id": "7", + "audience": "anchor", + "count": "29", + "used": "0" + }, + { + "id": "78395", + "box_id": "20", + "ornament_id": "8", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78396", + "box_id": "20", + "ornament_id": "9", + "audience": "anchor", + "count": "9", + "used": "0" + }, + { + "id": "78397", + "box_id": "20", + "ornament_id": "10", + "audience": "anchor", + "count": "5", + "used": "0" + }, + { + "id": "78398", + "box_id": "20", + "ornament_id": "11", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78399", + "box_id": "20", + "ornament_id": "12", + "audience": "anchor", + "count": "3", + "used": "0" + }, + { + "id": "78400", + "box_id": "20", + "ornament_id": "13", + "audience": "anchor", + "count": "2", + "used": "0" + }, + { + "id": "78401", + "box_id": "20", + "ornament_id": "14", + "audience": "anchor", + "count": "1", + "used": "0" + }, + { + "id": "78402", + "box_id": "20", + "ornament_id": "1", + "audience": "robot", + "count": "1090", + "used": "0" + }, + { + "id": "78403", + "box_id": "20", + "ornament_id": "2", + "audience": "robot", + "count": "594", + "used": "0" + }, + { + "id": "78404", + "box_id": "20", + "ornament_id": "3", + "audience": "robot", + "count": "323", + "used": "0" + }, + { + "id": "78405", + "box_id": "20", + "ornament_id": "4", + "audience": "robot", + "count": "176", + "used": "0" + }, + { + "id": "78406", + "box_id": "20", + "ornament_id": "5", + "audience": "robot", + "count": "96", + "used": "0" + }, + { + "id": "78407", + "box_id": "20", + "ornament_id": "6", + "audience": "robot", + "count": "53", + "used": "0" + }, + { + "id": "78408", + "box_id": "20", + "ornament_id": "7", + "audience": "robot", + "count": "29", + "used": "0" + }, + { + "id": "78409", + "box_id": "20", + "ornament_id": "8", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78410", + "box_id": "20", + "ornament_id": "9", + "audience": "robot", + "count": "9", + "used": "0" + }, + { + "id": "78411", + "box_id": "20", + "ornament_id": "10", + "audience": "robot", + "count": "5", + "used": "0" + }, + { + "id": "78412", + "box_id": "20", + "ornament_id": "11", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78413", + "box_id": "20", + "ornament_id": "12", + "audience": "robot", + "count": "3", + "used": "0" + }, + { + "id": "78414", + "box_id": "20", + "ornament_id": "13", + "audience": "robot", + "count": "2", + "used": "0" + }, + { + "id": "78415", + "box_id": "20", + "ornament_id": "14", + "audience": "robot", + "count": "1", + "used": "0" + }, + { + "id": "78416", + "box_id": "20", + "ornament_id": "1", + "audience": "white", + "count": "1090", + "used": "0" + }, + { + "id": "78417", + "box_id": "20", + "ornament_id": "2", + "audience": "white", + "count": "594", + "used": "0" + }, + { + "id": "78418", + "box_id": "20", + "ornament_id": "3", + "audience": "white", + "count": "323", + "used": "0" + }, + { + "id": "78419", + "box_id": "20", + "ornament_id": "4", + "audience": "white", + "count": "176", + "used": "0" + }, + { + "id": "78420", + "box_id": "20", + "ornament_id": "5", + "audience": "white", + "count": "96", + "used": "0" + }, + { + "id": "78421", + "box_id": "20", + "ornament_id": "6", + "audience": "white", + "count": "53", + "used": "0" + }, + { + "id": "78422", + "box_id": "20", + "ornament_id": "7", + "audience": "white", + "count": "29", + "used": "0" + }, + { + "id": "78423", + "box_id": "20", + "ornament_id": "8", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78424", + "box_id": "20", + "ornament_id": "9", + "audience": "white", + "count": "9", + "used": "0" + }, + { + "id": "78425", + "box_id": "20", + "ornament_id": "10", + "audience": "white", + "count": "5", + "used": "0" + }, + { + "id": "78426", + "box_id": "20", + "ornament_id": "11", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78427", + "box_id": "20", + "ornament_id": "12", + "audience": "white", + "count": "3", + "used": "0" + }, + { + "id": "78428", + "box_id": "20", + "ornament_id": "13", + "audience": "white", + "count": "2", + "used": "0" + }, + { + "id": "78429", + "box_id": "20", + "ornament_id": "14", + "audience": "white", + "count": "1", + "used": "0" + }, + { + "id": "78430", + "box_id": "21", + "ornament_id": "7", + "audience": "common", + "count": "868", + "used": "0" + }, + { + "id": "78431", + "box_id": "21", + "ornament_id": "8", + "audience": "common", + "count": "556", + "used": "0" + }, + { + "id": "78432", + "box_id": "21", + "ornament_id": "9", + "audience": "common", + "count": "356", + "used": "0" + }, + { + "id": "78433", + "box_id": "21", + "ornament_id": "10", + "audience": "common", + "count": "228", + "used": "0" + }, + { + "id": "78434", + "box_id": "21", + "ornament_id": "11", + "audience": "common", + "count": "146", + "used": "0" + }, + { + "id": "78435", + "box_id": "21", + "ornament_id": "12", + "audience": "common", + "count": "94", + "used": "0" + }, + { + "id": "78436", + "box_id": "21", + "ornament_id": "13", + "audience": "common", + "count": "61", + "used": "0" + }, + { + "id": "78437", + "box_id": "21", + "ornament_id": "14", + "audience": "common", + "count": "39", + "used": "0" + }, + { + "id": "78438", + "box_id": "21", + "ornament_id": "15", + "audience": "common", + "count": "25", + "used": "0" + }, + { + "id": "78439", + "box_id": "21", + "ornament_id": "16", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78440", + "box_id": "21", + "ornament_id": "17", + "audience": "common", + "count": "11", + "used": "0" + }, + { + "id": "78441", + "box_id": "21", + "ornament_id": "7", + "audience": "anchor", + "count": "868", + "used": "0" + }, + { + "id": "78442", + "box_id": "21", + "ornament_id": "8", + "audience": "anchor", + "count": "556", + "used": "0" + }, + { + "id": "78443", + "box_id": "21", + "ornament_id": "9", + "audience": "anchor", + "count": "356", + "used": "0" + }, + { + "id": "78444", + "box_id": "21", + "ornament_id": "10", + "audience": "anchor", + "count": "228", + "used": "0" + }, + { + "id": "78445", + "box_id": "21", + "ornament_id": "11", + "audience": "anchor", + "count": "146", + "used": "0" + }, + { + "id": "78446", + "box_id": "21", + "ornament_id": "12", + "audience": "anchor", + "count": "94", + "used": "0" + }, + { + "id": "78447", + "box_id": "21", + "ornament_id": "13", + "audience": "anchor", + "count": "61", + "used": "0" + }, + { + "id": "78448", + "box_id": "21", + "ornament_id": "14", + "audience": "anchor", + "count": "39", + "used": "0" + }, + { + "id": "78449", + "box_id": "21", + "ornament_id": "15", + "audience": "anchor", + "count": "25", + "used": "0" + }, + { + "id": "78450", + "box_id": "21", + "ornament_id": "16", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78451", + "box_id": "21", + "ornament_id": "17", + "audience": "anchor", + "count": "11", + "used": "0" + }, + { + "id": "78452", + "box_id": "21", + "ornament_id": "7", + "audience": "robot", + "count": "868", + "used": "0" + }, + { + "id": "78453", + "box_id": "21", + "ornament_id": "8", + "audience": "robot", + "count": "556", + "used": "0" + }, + { + "id": "78454", + "box_id": "21", + "ornament_id": "9", + "audience": "robot", + "count": "356", + "used": "0" + }, + { + "id": "78455", + "box_id": "21", + "ornament_id": "10", + "audience": "robot", + "count": "228", + "used": "0" + }, + { + "id": "78456", + "box_id": "21", + "ornament_id": "11", + "audience": "robot", + "count": "146", + "used": "0" + }, + { + "id": "78457", + "box_id": "21", + "ornament_id": "12", + "audience": "robot", + "count": "94", + "used": "0" + }, + { + "id": "78458", + "box_id": "21", + "ornament_id": "13", + "audience": "robot", + "count": "61", + "used": "0" + }, + { + "id": "78459", + "box_id": "21", + "ornament_id": "14", + "audience": "robot", + "count": "39", + "used": "0" + }, + { + "id": "78460", + "box_id": "21", + "ornament_id": "15", + "audience": "robot", + "count": "25", + "used": "0" + }, + { + "id": "78461", + "box_id": "21", + "ornament_id": "16", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78462", + "box_id": "21", + "ornament_id": "17", + "audience": "robot", + "count": "11", + "used": "0" + }, + { + "id": "78463", + "box_id": "21", + "ornament_id": "7", + "audience": "white", + "count": "868", + "used": "0" + }, + { + "id": "78464", + "box_id": "21", + "ornament_id": "8", + "audience": "white", + "count": "556", + "used": "0" + }, + { + "id": "78465", + "box_id": "21", + "ornament_id": "9", + "audience": "white", + "count": "356", + "used": "0" + }, + { + "id": "78466", + "box_id": "21", + "ornament_id": "10", + "audience": "white", + "count": "228", + "used": "0" + }, + { + "id": "78467", + "box_id": "21", + "ornament_id": "11", + "audience": "white", + "count": "146", + "used": "0" + }, + { + "id": "78468", + "box_id": "21", + "ornament_id": "12", + "audience": "white", + "count": "94", + "used": "0" + }, + { + "id": "78469", + "box_id": "21", + "ornament_id": "13", + "audience": "white", + "count": "61", + "used": "0" + }, + { + "id": "78470", + "box_id": "21", + "ornament_id": "14", + "audience": "white", + "count": "39", + "used": "0" + }, + { + "id": "78471", + "box_id": "21", + "ornament_id": "15", + "audience": "white", + "count": "25", + "used": "0" + }, + { + "id": "78472", + "box_id": "21", + "ornament_id": "16", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78473", + "box_id": "21", + "ornament_id": "17", + "audience": "white", + "count": "11", + "used": "0" + }, + { + "id": "78474", + "box_id": "22", + "ornament_id": "7", + "audience": "common", + "count": "879", + "used": "0" + }, + { + "id": "78475", + "box_id": "22", + "ornament_id": "8", + "audience": "common", + "count": "557", + "used": "0" + }, + { + "id": "78476", + "box_id": "22", + "ornament_id": "9", + "audience": "common", + "count": "355", + "used": "0" + }, + { + "id": "78477", + "box_id": "22", + "ornament_id": "10", + "audience": "common", + "count": "225", + "used": "0" + }, + { + "id": "78478", + "box_id": "22", + "ornament_id": "11", + "audience": "common", + "count": "144", + "used": "0" + }, + { + "id": "78479", + "box_id": "22", + "ornament_id": "12", + "audience": "common", + "count": "92", + "used": "0" + }, + { + "id": "78480", + "box_id": "22", + "ornament_id": "13", + "audience": "common", + "count": "59", + "used": "0" + }, + { + "id": "78481", + "box_id": "22", + "ornament_id": "14", + "audience": "common", + "count": "38", + "used": "0" + }, + { + "id": "78482", + "box_id": "22", + "ornament_id": "15", + "audience": "common", + "count": "24", + "used": "0" + }, + { + "id": "78483", + "box_id": "22", + "ornament_id": "16", + "audience": "common", + "count": "17", + "used": "0" + }, + { + "id": "78484", + "box_id": "22", + "ornament_id": "17", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "78485", + "box_id": "22", + "ornament_id": "7", + "audience": "anchor", + "count": "879", + "used": "0" + }, + { + "id": "78486", + "box_id": "22", + "ornament_id": "8", + "audience": "anchor", + "count": "557", + "used": "0" + }, + { + "id": "78487", + "box_id": "22", + "ornament_id": "9", + "audience": "anchor", + "count": "355", + "used": "0" + }, + { + "id": "78488", + "box_id": "22", + "ornament_id": "10", + "audience": "anchor", + "count": "225", + "used": "0" + }, + { + "id": "78489", + "box_id": "22", + "ornament_id": "11", + "audience": "anchor", + "count": "144", + "used": "0" + }, + { + "id": "78490", + "box_id": "22", + "ornament_id": "12", + "audience": "anchor", + "count": "92", + "used": "0" + }, + { + "id": "78491", + "box_id": "22", + "ornament_id": "13", + "audience": "anchor", + "count": "59", + "used": "0" + }, + { + "id": "78492", + "box_id": "22", + "ornament_id": "14", + "audience": "anchor", + "count": "38", + "used": "0" + }, + { + "id": "78493", + "box_id": "22", + "ornament_id": "15", + "audience": "anchor", + "count": "24", + "used": "0" + }, + { + "id": "78494", + "box_id": "22", + "ornament_id": "16", + "audience": "anchor", + "count": "17", + "used": "0" + }, + { + "id": "78495", + "box_id": "22", + "ornament_id": "17", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "78496", + "box_id": "22", + "ornament_id": "7", + "audience": "robot", + "count": "879", + "used": "0" + }, + { + "id": "78497", + "box_id": "22", + "ornament_id": "8", + "audience": "robot", + "count": "557", + "used": "0" + }, + { + "id": "78498", + "box_id": "22", + "ornament_id": "9", + "audience": "robot", + "count": "355", + "used": "0" + }, + { + "id": "78499", + "box_id": "22", + "ornament_id": "10", + "audience": "robot", + "count": "225", + "used": "0" + }, + { + "id": "78500", + "box_id": "22", + "ornament_id": "11", + "audience": "robot", + "count": "144", + "used": "0" + }, + { + "id": "78501", + "box_id": "22", + "ornament_id": "12", + "audience": "robot", + "count": "92", + "used": "0" + }, + { + "id": "78502", + "box_id": "22", + "ornament_id": "13", + "audience": "robot", + "count": "59", + "used": "0" + }, + { + "id": "78503", + "box_id": "22", + "ornament_id": "14", + "audience": "robot", + "count": "38", + "used": "0" + }, + { + "id": "78504", + "box_id": "22", + "ornament_id": "15", + "audience": "robot", + "count": "24", + "used": "0" + }, + { + "id": "78505", + "box_id": "22", + "ornament_id": "16", + "audience": "robot", + "count": "17", + "used": "0" + }, + { + "id": "78506", + "box_id": "22", + "ornament_id": "17", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "78507", + "box_id": "22", + "ornament_id": "7", + "audience": "white", + "count": "879", + "used": "0" + }, + { + "id": "78508", + "box_id": "22", + "ornament_id": "8", + "audience": "white", + "count": "557", + "used": "0" + }, + { + "id": "78509", + "box_id": "22", + "ornament_id": "9", + "audience": "white", + "count": "355", + "used": "0" + }, + { + "id": "78510", + "box_id": "22", + "ornament_id": "10", + "audience": "white", + "count": "225", + "used": "0" + }, + { + "id": "78511", + "box_id": "22", + "ornament_id": "11", + "audience": "white", + "count": "144", + "used": "0" + }, + { + "id": "78512", + "box_id": "22", + "ornament_id": "12", + "audience": "white", + "count": "92", + "used": "0" + }, + { + "id": "78513", + "box_id": "22", + "ornament_id": "13", + "audience": "white", + "count": "59", + "used": "0" + }, + { + "id": "78514", + "box_id": "22", + "ornament_id": "14", + "audience": "white", + "count": "38", + "used": "0" + }, + { + "id": "78515", + "box_id": "22", + "ornament_id": "15", + "audience": "white", + "count": "24", + "used": "0" + }, + { + "id": "78516", + "box_id": "22", + "ornament_id": "16", + "audience": "white", + "count": "17", + "used": "0" + }, + { + "id": "78517", + "box_id": "22", + "ornament_id": "17", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "78518", + "box_id": "23", + "ornament_id": "7", + "audience": "common", + "count": "877", + "used": "0" + }, + { + "id": "78519", + "box_id": "23", + "ornament_id": "8", + "audience": "common", + "count": "557", + "used": "0" + }, + { + "id": "78520", + "box_id": "23", + "ornament_id": "9", + "audience": "common", + "count": "356", + "used": "0" + }, + { + "id": "78521", + "box_id": "23", + "ornament_id": "10", + "audience": "common", + "count": "226", + "used": "0" + }, + { + "id": "78522", + "box_id": "23", + "ornament_id": "11", + "audience": "common", + "count": "143", + "used": "0" + }, + { + "id": "78523", + "box_id": "23", + "ornament_id": "12", + "audience": "common", + "count": "92", + "used": "0" + }, + { + "id": "78524", + "box_id": "23", + "ornament_id": "13", + "audience": "common", + "count": "59", + "used": "0" + }, + { + "id": "78525", + "box_id": "23", + "ornament_id": "14", + "audience": "common", + "count": "38", + "used": "0" + }, + { + "id": "78526", + "box_id": "23", + "ornament_id": "15", + "audience": "common", + "count": "25", + "used": "0" + }, + { + "id": "78527", + "box_id": "23", + "ornament_id": "16", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78528", + "box_id": "23", + "ornament_id": "17", + "audience": "common", + "count": "11", + "used": "0" + }, + { + "id": "78529", + "box_id": "23", + "ornament_id": "7", + "audience": "anchor", + "count": "877", + "used": "0" + }, + { + "id": "78530", + "box_id": "23", + "ornament_id": "8", + "audience": "anchor", + "count": "557", + "used": "0" + }, + { + "id": "78531", + "box_id": "23", + "ornament_id": "9", + "audience": "anchor", + "count": "356", + "used": "0" + }, + { + "id": "78532", + "box_id": "23", + "ornament_id": "10", + "audience": "anchor", + "count": "226", + "used": "0" + }, + { + "id": "78533", + "box_id": "23", + "ornament_id": "11", + "audience": "anchor", + "count": "143", + "used": "0" + }, + { + "id": "78534", + "box_id": "23", + "ornament_id": "12", + "audience": "anchor", + "count": "92", + "used": "0" + }, + { + "id": "78535", + "box_id": "23", + "ornament_id": "13", + "audience": "anchor", + "count": "59", + "used": "0" + }, + { + "id": "78536", + "box_id": "23", + "ornament_id": "14", + "audience": "anchor", + "count": "38", + "used": "0" + }, + { + "id": "78537", + "box_id": "23", + "ornament_id": "15", + "audience": "anchor", + "count": "25", + "used": "0" + }, + { + "id": "78538", + "box_id": "23", + "ornament_id": "16", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78539", + "box_id": "23", + "ornament_id": "17", + "audience": "anchor", + "count": "11", + "used": "0" + }, + { + "id": "78540", + "box_id": "23", + "ornament_id": "7", + "audience": "robot", + "count": "877", + "used": "0" + }, + { + "id": "78541", + "box_id": "23", + "ornament_id": "8", + "audience": "robot", + "count": "557", + "used": "0" + }, + { + "id": "78542", + "box_id": "23", + "ornament_id": "9", + "audience": "robot", + "count": "356", + "used": "0" + }, + { + "id": "78543", + "box_id": "23", + "ornament_id": "10", + "audience": "robot", + "count": "226", + "used": "0" + }, + { + "id": "78544", + "box_id": "23", + "ornament_id": "11", + "audience": "robot", + "count": "143", + "used": "0" + }, + { + "id": "78545", + "box_id": "23", + "ornament_id": "12", + "audience": "robot", + "count": "92", + "used": "0" + }, + { + "id": "78546", + "box_id": "23", + "ornament_id": "13", + "audience": "robot", + "count": "59", + "used": "0" + }, + { + "id": "78547", + "box_id": "23", + "ornament_id": "14", + "audience": "robot", + "count": "38", + "used": "0" + }, + { + "id": "78548", + "box_id": "23", + "ornament_id": "15", + "audience": "robot", + "count": "25", + "used": "0" + }, + { + "id": "78549", + "box_id": "23", + "ornament_id": "16", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78550", + "box_id": "23", + "ornament_id": "17", + "audience": "robot", + "count": "11", + "used": "0" + }, + { + "id": "78551", + "box_id": "23", + "ornament_id": "7", + "audience": "white", + "count": "877", + "used": "0" + }, + { + "id": "78552", + "box_id": "23", + "ornament_id": "8", + "audience": "white", + "count": "557", + "used": "0" + }, + { + "id": "78553", + "box_id": "23", + "ornament_id": "9", + "audience": "white", + "count": "356", + "used": "0" + }, + { + "id": "78554", + "box_id": "23", + "ornament_id": "10", + "audience": "white", + "count": "226", + "used": "0" + }, + { + "id": "78555", + "box_id": "23", + "ornament_id": "11", + "audience": "white", + "count": "143", + "used": "0" + }, + { + "id": "78556", + "box_id": "23", + "ornament_id": "12", + "audience": "white", + "count": "92", + "used": "0" + }, + { + "id": "78557", + "box_id": "23", + "ornament_id": "13", + "audience": "white", + "count": "59", + "used": "0" + }, + { + "id": "78558", + "box_id": "23", + "ornament_id": "14", + "audience": "white", + "count": "38", + "used": "0" + }, + { + "id": "78559", + "box_id": "23", + "ornament_id": "15", + "audience": "white", + "count": "25", + "used": "0" + }, + { + "id": "78560", + "box_id": "23", + "ornament_id": "16", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78561", + "box_id": "23", + "ornament_id": "17", + "audience": "white", + "count": "11", + "used": "0" + }, + { + "id": "78562", + "box_id": "24", + "ornament_id": "7", + "audience": "common", + "count": "868", + "used": "0" + }, + { + "id": "78563", + "box_id": "24", + "ornament_id": "8", + "audience": "common", + "count": "556", + "used": "0" + }, + { + "id": "78564", + "box_id": "24", + "ornament_id": "9", + "audience": "common", + "count": "356", + "used": "0" + }, + { + "id": "78565", + "box_id": "24", + "ornament_id": "10", + "audience": "common", + "count": "228", + "used": "0" + }, + { + "id": "78566", + "box_id": "24", + "ornament_id": "11", + "audience": "common", + "count": "146", + "used": "0" + }, + { + "id": "78567", + "box_id": "24", + "ornament_id": "12", + "audience": "common", + "count": "94", + "used": "0" + }, + { + "id": "78568", + "box_id": "24", + "ornament_id": "13", + "audience": "common", + "count": "60", + "used": "0" + }, + { + "id": "78569", + "box_id": "24", + "ornament_id": "14", + "audience": "common", + "count": "39", + "used": "0" + }, + { + "id": "78570", + "box_id": "24", + "ornament_id": "15", + "audience": "common", + "count": "26", + "used": "0" + }, + { + "id": "78571", + "box_id": "24", + "ornament_id": "16", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78572", + "box_id": "24", + "ornament_id": "17", + "audience": "common", + "count": "11", + "used": "0" + }, + { + "id": "78573", + "box_id": "24", + "ornament_id": "7", + "audience": "anchor", + "count": "868", + "used": "0" + }, + { + "id": "78574", + "box_id": "24", + "ornament_id": "8", + "audience": "anchor", + "count": "556", + "used": "0" + }, + { + "id": "78575", + "box_id": "24", + "ornament_id": "9", + "audience": "anchor", + "count": "356", + "used": "0" + }, + { + "id": "78576", + "box_id": "24", + "ornament_id": "10", + "audience": "anchor", + "count": "228", + "used": "0" + }, + { + "id": "78577", + "box_id": "24", + "ornament_id": "11", + "audience": "anchor", + "count": "146", + "used": "0" + }, + { + "id": "78578", + "box_id": "24", + "ornament_id": "12", + "audience": "anchor", + "count": "94", + "used": "0" + }, + { + "id": "78579", + "box_id": "24", + "ornament_id": "13", + "audience": "anchor", + "count": "60", + "used": "0" + }, + { + "id": "78580", + "box_id": "24", + "ornament_id": "14", + "audience": "anchor", + "count": "39", + "used": "0" + }, + { + "id": "78581", + "box_id": "24", + "ornament_id": "15", + "audience": "anchor", + "count": "26", + "used": "0" + }, + { + "id": "78582", + "box_id": "24", + "ornament_id": "16", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78583", + "box_id": "24", + "ornament_id": "17", + "audience": "anchor", + "count": "11", + "used": "0" + }, + { + "id": "78584", + "box_id": "24", + "ornament_id": "7", + "audience": "robot", + "count": "868", + "used": "0" + }, + { + "id": "78585", + "box_id": "24", + "ornament_id": "8", + "audience": "robot", + "count": "556", + "used": "0" + }, + { + "id": "78586", + "box_id": "24", + "ornament_id": "9", + "audience": "robot", + "count": "356", + "used": "0" + }, + { + "id": "78587", + "box_id": "24", + "ornament_id": "10", + "audience": "robot", + "count": "228", + "used": "0" + }, + { + "id": "78588", + "box_id": "24", + "ornament_id": "11", + "audience": "robot", + "count": "146", + "used": "0" + }, + { + "id": "78589", + "box_id": "24", + "ornament_id": "12", + "audience": "robot", + "count": "94", + "used": "0" + }, + { + "id": "78590", + "box_id": "24", + "ornament_id": "13", + "audience": "robot", + "count": "60", + "used": "0" + }, + { + "id": "78591", + "box_id": "24", + "ornament_id": "14", + "audience": "robot", + "count": "39", + "used": "0" + }, + { + "id": "78592", + "box_id": "24", + "ornament_id": "15", + "audience": "robot", + "count": "26", + "used": "0" + }, + { + "id": "78593", + "box_id": "24", + "ornament_id": "16", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78594", + "box_id": "24", + "ornament_id": "17", + "audience": "robot", + "count": "11", + "used": "0" + }, + { + "id": "78595", + "box_id": "24", + "ornament_id": "7", + "audience": "white", + "count": "868", + "used": "0" + }, + { + "id": "78596", + "box_id": "24", + "ornament_id": "8", + "audience": "white", + "count": "556", + "used": "0" + }, + { + "id": "78597", + "box_id": "24", + "ornament_id": "9", + "audience": "white", + "count": "356", + "used": "0" + }, + { + "id": "78598", + "box_id": "24", + "ornament_id": "10", + "audience": "white", + "count": "228", + "used": "0" + }, + { + "id": "78599", + "box_id": "24", + "ornament_id": "11", + "audience": "white", + "count": "146", + "used": "0" + }, + { + "id": "78600", + "box_id": "24", + "ornament_id": "12", + "audience": "white", + "count": "94", + "used": "0" + }, + { + "id": "78601", + "box_id": "24", + "ornament_id": "13", + "audience": "white", + "count": "60", + "used": "0" + }, + { + "id": "78602", + "box_id": "24", + "ornament_id": "14", + "audience": "white", + "count": "39", + "used": "0" + }, + { + "id": "78603", + "box_id": "24", + "ornament_id": "15", + "audience": "white", + "count": "26", + "used": "0" + }, + { + "id": "78604", + "box_id": "24", + "ornament_id": "16", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78605", + "box_id": "24", + "ornament_id": "17", + "audience": "white", + "count": "11", + "used": "0" + }, + { + "id": "78606", + "box_id": "25", + "ornament_id": "7", + "audience": "common", + "count": "867", + "used": "0" + }, + { + "id": "78607", + "box_id": "25", + "ornament_id": "8", + "audience": "common", + "count": "556", + "used": "0" + }, + { + "id": "78608", + "box_id": "25", + "ornament_id": "9", + "audience": "common", + "count": "356", + "used": "0" + }, + { + "id": "78609", + "box_id": "25", + "ornament_id": "10", + "audience": "common", + "count": "228", + "used": "0" + }, + { + "id": "78610", + "box_id": "25", + "ornament_id": "11", + "audience": "common", + "count": "147", + "used": "0" + }, + { + "id": "78611", + "box_id": "25", + "ornament_id": "12", + "audience": "common", + "count": "94", + "used": "0" + }, + { + "id": "78612", + "box_id": "25", + "ornament_id": "13", + "audience": "common", + "count": "60", + "used": "0" + }, + { + "id": "78613", + "box_id": "25", + "ornament_id": "14", + "audience": "common", + "count": "40", + "used": "0" + }, + { + "id": "78614", + "box_id": "25", + "ornament_id": "15", + "audience": "common", + "count": "25", + "used": "0" + }, + { + "id": "78615", + "box_id": "25", + "ornament_id": "16", + "audience": "common", + "count": "17", + "used": "0" + }, + { + "id": "78616", + "box_id": "25", + "ornament_id": "17", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "78617", + "box_id": "25", + "ornament_id": "7", + "audience": "anchor", + "count": "867", + "used": "0" + }, + { + "id": "78618", + "box_id": "25", + "ornament_id": "8", + "audience": "anchor", + "count": "556", + "used": "0" + }, + { + "id": "78619", + "box_id": "25", + "ornament_id": "9", + "audience": "anchor", + "count": "356", + "used": "0" + }, + { + "id": "78620", + "box_id": "25", + "ornament_id": "10", + "audience": "anchor", + "count": "228", + "used": "0" + }, + { + "id": "78621", + "box_id": "25", + "ornament_id": "11", + "audience": "anchor", + "count": "147", + "used": "0" + }, + { + "id": "78622", + "box_id": "25", + "ornament_id": "12", + "audience": "anchor", + "count": "94", + "used": "0" + }, + { + "id": "78623", + "box_id": "25", + "ornament_id": "13", + "audience": "anchor", + "count": "60", + "used": "0" + }, + { + "id": "78624", + "box_id": "25", + "ornament_id": "14", + "audience": "anchor", + "count": "40", + "used": "0" + }, + { + "id": "78625", + "box_id": "25", + "ornament_id": "15", + "audience": "anchor", + "count": "25", + "used": "0" + }, + { + "id": "78626", + "box_id": "25", + "ornament_id": "16", + "audience": "anchor", + "count": "17", + "used": "0" + }, + { + "id": "78627", + "box_id": "25", + "ornament_id": "17", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "78628", + "box_id": "25", + "ornament_id": "7", + "audience": "robot", + "count": "867", + "used": "0" + }, + { + "id": "78629", + "box_id": "25", + "ornament_id": "8", + "audience": "robot", + "count": "556", + "used": "0" + }, + { + "id": "78630", + "box_id": "25", + "ornament_id": "9", + "audience": "robot", + "count": "356", + "used": "0" + }, + { + "id": "78631", + "box_id": "25", + "ornament_id": "10", + "audience": "robot", + "count": "228", + "used": "0" + }, + { + "id": "78632", + "box_id": "25", + "ornament_id": "11", + "audience": "robot", + "count": "147", + "used": "0" + }, + { + "id": "78633", + "box_id": "25", + "ornament_id": "12", + "audience": "robot", + "count": "94", + "used": "0" + }, + { + "id": "78634", + "box_id": "25", + "ornament_id": "13", + "audience": "robot", + "count": "60", + "used": "0" + }, + { + "id": "78635", + "box_id": "25", + "ornament_id": "14", + "audience": "robot", + "count": "40", + "used": "0" + }, + { + "id": "78636", + "box_id": "25", + "ornament_id": "15", + "audience": "robot", + "count": "25", + "used": "0" + }, + { + "id": "78637", + "box_id": "25", + "ornament_id": "16", + "audience": "robot", + "count": "17", + "used": "0" + }, + { + "id": "78638", + "box_id": "25", + "ornament_id": "17", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "78639", + "box_id": "25", + "ornament_id": "7", + "audience": "white", + "count": "867", + "used": "0" + }, + { + "id": "78640", + "box_id": "25", + "ornament_id": "8", + "audience": "white", + "count": "556", + "used": "0" + }, + { + "id": "78641", + "box_id": "25", + "ornament_id": "9", + "audience": "white", + "count": "356", + "used": "0" + }, + { + "id": "78642", + "box_id": "25", + "ornament_id": "10", + "audience": "white", + "count": "228", + "used": "0" + }, + { + "id": "78643", + "box_id": "25", + "ornament_id": "11", + "audience": "white", + "count": "147", + "used": "0" + }, + { + "id": "78644", + "box_id": "25", + "ornament_id": "12", + "audience": "white", + "count": "94", + "used": "0" + }, + { + "id": "78645", + "box_id": "25", + "ornament_id": "13", + "audience": "white", + "count": "60", + "used": "0" + }, + { + "id": "78646", + "box_id": "25", + "ornament_id": "14", + "audience": "white", + "count": "40", + "used": "0" + }, + { + "id": "78647", + "box_id": "25", + "ornament_id": "15", + "audience": "white", + "count": "25", + "used": "0" + }, + { + "id": "78648", + "box_id": "25", + "ornament_id": "16", + "audience": "white", + "count": "17", + "used": "0" + }, + { + "id": "78649", + "box_id": "25", + "ornament_id": "17", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "78650", + "box_id": "26", + "ornament_id": "7", + "audience": "common", + "count": "877", + "used": "0" + }, + { + "id": "78651", + "box_id": "26", + "ornament_id": "8", + "audience": "common", + "count": "558", + "used": "0" + }, + { + "id": "78652", + "box_id": "26", + "ornament_id": "9", + "audience": "common", + "count": "354", + "used": "0" + }, + { + "id": "78653", + "box_id": "26", + "ornament_id": "10", + "audience": "common", + "count": "226", + "used": "0" + }, + { + "id": "78654", + "box_id": "26", + "ornament_id": "11", + "audience": "common", + "count": "144", + "used": "0" + }, + { + "id": "78655", + "box_id": "26", + "ornament_id": "12", + "audience": "common", + "count": "92", + "used": "0" + }, + { + "id": "78656", + "box_id": "26", + "ornament_id": "13", + "audience": "common", + "count": "60", + "used": "0" + }, + { + "id": "78657", + "box_id": "26", + "ornament_id": "14", + "audience": "common", + "count": "38", + "used": "0" + }, + { + "id": "78658", + "box_id": "26", + "ornament_id": "15", + "audience": "common", + "count": "24", + "used": "0" + }, + { + "id": "78659", + "box_id": "26", + "ornament_id": "16", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78660", + "box_id": "26", + "ornament_id": "17", + "audience": "common", + "count": "11", + "used": "0" + }, + { + "id": "78661", + "box_id": "26", + "ornament_id": "7", + "audience": "anchor", + "count": "877", + "used": "0" + }, + { + "id": "78662", + "box_id": "26", + "ornament_id": "8", + "audience": "anchor", + "count": "558", + "used": "0" + }, + { + "id": "78663", + "box_id": "26", + "ornament_id": "9", + "audience": "anchor", + "count": "354", + "used": "0" + }, + { + "id": "78664", + "box_id": "26", + "ornament_id": "10", + "audience": "anchor", + "count": "226", + "used": "0" + }, + { + "id": "78665", + "box_id": "26", + "ornament_id": "11", + "audience": "anchor", + "count": "144", + "used": "0" + }, + { + "id": "78666", + "box_id": "26", + "ornament_id": "12", + "audience": "anchor", + "count": "92", + "used": "0" + }, + { + "id": "78667", + "box_id": "26", + "ornament_id": "13", + "audience": "anchor", + "count": "60", + "used": "0" + }, + { + "id": "78668", + "box_id": "26", + "ornament_id": "14", + "audience": "anchor", + "count": "38", + "used": "0" + }, + { + "id": "78669", + "box_id": "26", + "ornament_id": "15", + "audience": "anchor", + "count": "24", + "used": "0" + }, + { + "id": "78670", + "box_id": "26", + "ornament_id": "16", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78671", + "box_id": "26", + "ornament_id": "17", + "audience": "anchor", + "count": "11", + "used": "0" + }, + { + "id": "78672", + "box_id": "26", + "ornament_id": "7", + "audience": "robot", + "count": "877", + "used": "0" + }, + { + "id": "78673", + "box_id": "26", + "ornament_id": "8", + "audience": "robot", + "count": "558", + "used": "0" + }, + { + "id": "78674", + "box_id": "26", + "ornament_id": "9", + "audience": "robot", + "count": "354", + "used": "0" + }, + { + "id": "78675", + "box_id": "26", + "ornament_id": "10", + "audience": "robot", + "count": "226", + "used": "0" + }, + { + "id": "78676", + "box_id": "26", + "ornament_id": "11", + "audience": "robot", + "count": "144", + "used": "0" + }, + { + "id": "78677", + "box_id": "26", + "ornament_id": "12", + "audience": "robot", + "count": "92", + "used": "0" + }, + { + "id": "78678", + "box_id": "26", + "ornament_id": "13", + "audience": "robot", + "count": "60", + "used": "0" + }, + { + "id": "78679", + "box_id": "26", + "ornament_id": "14", + "audience": "robot", + "count": "38", + "used": "0" + }, + { + "id": "78680", + "box_id": "26", + "ornament_id": "15", + "audience": "robot", + "count": "24", + "used": "0" + }, + { + "id": "78681", + "box_id": "26", + "ornament_id": "16", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78682", + "box_id": "26", + "ornament_id": "17", + "audience": "robot", + "count": "11", + "used": "0" + }, + { + "id": "78683", + "box_id": "26", + "ornament_id": "7", + "audience": "white", + "count": "877", + "used": "0" + }, + { + "id": "78684", + "box_id": "26", + "ornament_id": "8", + "audience": "white", + "count": "558", + "used": "0" + }, + { + "id": "78685", + "box_id": "26", + "ornament_id": "9", + "audience": "white", + "count": "354", + "used": "0" + }, + { + "id": "78686", + "box_id": "26", + "ornament_id": "10", + "audience": "white", + "count": "226", + "used": "0" + }, + { + "id": "78687", + "box_id": "26", + "ornament_id": "11", + "audience": "white", + "count": "144", + "used": "0" + }, + { + "id": "78688", + "box_id": "26", + "ornament_id": "12", + "audience": "white", + "count": "92", + "used": "0" + }, + { + "id": "78689", + "box_id": "26", + "ornament_id": "13", + "audience": "white", + "count": "60", + "used": "0" + }, + { + "id": "78690", + "box_id": "26", + "ornament_id": "14", + "audience": "white", + "count": "38", + "used": "0" + }, + { + "id": "78691", + "box_id": "26", + "ornament_id": "15", + "audience": "white", + "count": "24", + "used": "0" + }, + { + "id": "78692", + "box_id": "26", + "ornament_id": "16", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78693", + "box_id": "26", + "ornament_id": "17", + "audience": "white", + "count": "11", + "used": "0" + }, + { + "id": "78694", + "box_id": "27", + "ornament_id": "7", + "audience": "common", + "count": "869", + "used": "0" + }, + { + "id": "78695", + "box_id": "27", + "ornament_id": "8", + "audience": "common", + "count": "556", + "used": "0" + }, + { + "id": "78696", + "box_id": "27", + "ornament_id": "9", + "audience": "common", + "count": "357", + "used": "0" + }, + { + "id": "78697", + "box_id": "27", + "ornament_id": "10", + "audience": "common", + "count": "228", + "used": "0" + }, + { + "id": "78698", + "box_id": "27", + "ornament_id": "11", + "audience": "common", + "count": "146", + "used": "0" + }, + { + "id": "78699", + "box_id": "27", + "ornament_id": "12", + "audience": "common", + "count": "94", + "used": "0" + }, + { + "id": "78700", + "box_id": "27", + "ornament_id": "13", + "audience": "common", + "count": "59", + "used": "0" + }, + { + "id": "78701", + "box_id": "27", + "ornament_id": "14", + "audience": "common", + "count": "39", + "used": "0" + }, + { + "id": "78702", + "box_id": "27", + "ornament_id": "15", + "audience": "common", + "count": "25", + "used": "0" + }, + { + "id": "78703", + "box_id": "27", + "ornament_id": "16", + "audience": "common", + "count": "17", + "used": "0" + }, + { + "id": "78704", + "box_id": "27", + "ornament_id": "17", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "78705", + "box_id": "27", + "ornament_id": "7", + "audience": "anchor", + "count": "869", + "used": "0" + }, + { + "id": "78706", + "box_id": "27", + "ornament_id": "8", + "audience": "anchor", + "count": "556", + "used": "0" + }, + { + "id": "78707", + "box_id": "27", + "ornament_id": "9", + "audience": "anchor", + "count": "357", + "used": "0" + }, + { + "id": "78708", + "box_id": "27", + "ornament_id": "10", + "audience": "anchor", + "count": "228", + "used": "0" + }, + { + "id": "78709", + "box_id": "27", + "ornament_id": "11", + "audience": "anchor", + "count": "146", + "used": "0" + }, + { + "id": "78710", + "box_id": "27", + "ornament_id": "12", + "audience": "anchor", + "count": "94", + "used": "0" + }, + { + "id": "78711", + "box_id": "27", + "ornament_id": "13", + "audience": "anchor", + "count": "59", + "used": "0" + }, + { + "id": "78712", + "box_id": "27", + "ornament_id": "14", + "audience": "anchor", + "count": "39", + "used": "0" + }, + { + "id": "78713", + "box_id": "27", + "ornament_id": "15", + "audience": "anchor", + "count": "25", + "used": "0" + }, + { + "id": "78714", + "box_id": "27", + "ornament_id": "16", + "audience": "anchor", + "count": "17", + "used": "0" + }, + { + "id": "78715", + "box_id": "27", + "ornament_id": "17", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "78716", + "box_id": "27", + "ornament_id": "7", + "audience": "robot", + "count": "869", + "used": "0" + }, + { + "id": "78717", + "box_id": "27", + "ornament_id": "8", + "audience": "robot", + "count": "556", + "used": "0" + }, + { + "id": "78718", + "box_id": "27", + "ornament_id": "9", + "audience": "robot", + "count": "357", + "used": "0" + }, + { + "id": "78719", + "box_id": "27", + "ornament_id": "10", + "audience": "robot", + "count": "228", + "used": "0" + }, + { + "id": "78720", + "box_id": "27", + "ornament_id": "11", + "audience": "robot", + "count": "146", + "used": "0" + }, + { + "id": "78721", + "box_id": "27", + "ornament_id": "12", + "audience": "robot", + "count": "94", + "used": "0" + }, + { + "id": "78722", + "box_id": "27", + "ornament_id": "13", + "audience": "robot", + "count": "59", + "used": "0" + }, + { + "id": "78723", + "box_id": "27", + "ornament_id": "14", + "audience": "robot", + "count": "39", + "used": "0" + }, + { + "id": "78724", + "box_id": "27", + "ornament_id": "15", + "audience": "robot", + "count": "25", + "used": "0" + }, + { + "id": "78725", + "box_id": "27", + "ornament_id": "16", + "audience": "robot", + "count": "17", + "used": "0" + }, + { + "id": "78726", + "box_id": "27", + "ornament_id": "17", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "78727", + "box_id": "27", + "ornament_id": "7", + "audience": "white", + "count": "869", + "used": "0" + }, + { + "id": "78728", + "box_id": "27", + "ornament_id": "8", + "audience": "white", + "count": "556", + "used": "0" + }, + { + "id": "78729", + "box_id": "27", + "ornament_id": "9", + "audience": "white", + "count": "357", + "used": "0" + }, + { + "id": "78730", + "box_id": "27", + "ornament_id": "10", + "audience": "white", + "count": "228", + "used": "0" + }, + { + "id": "78731", + "box_id": "27", + "ornament_id": "11", + "audience": "white", + "count": "146", + "used": "0" + }, + { + "id": "78732", + "box_id": "27", + "ornament_id": "12", + "audience": "white", + "count": "94", + "used": "0" + }, + { + "id": "78733", + "box_id": "27", + "ornament_id": "13", + "audience": "white", + "count": "59", + "used": "0" + }, + { + "id": "78734", + "box_id": "27", + "ornament_id": "14", + "audience": "white", + "count": "39", + "used": "0" + }, + { + "id": "78735", + "box_id": "27", + "ornament_id": "15", + "audience": "white", + "count": "25", + "used": "0" + }, + { + "id": "78736", + "box_id": "27", + "ornament_id": "16", + "audience": "white", + "count": "17", + "used": "0" + }, + { + "id": "78737", + "box_id": "27", + "ornament_id": "17", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "78738", + "box_id": "28", + "ornament_id": "7", + "audience": "common", + "count": "879", + "used": "0" + }, + { + "id": "78739", + "box_id": "28", + "ornament_id": "8", + "audience": "common", + "count": "557", + "used": "0" + }, + { + "id": "78740", + "box_id": "28", + "ornament_id": "9", + "audience": "common", + "count": "355", + "used": "0" + }, + { + "id": "78741", + "box_id": "28", + "ornament_id": "10", + "audience": "common", + "count": "227", + "used": "0" + }, + { + "id": "78742", + "box_id": "28", + "ornament_id": "11", + "audience": "common", + "count": "144", + "used": "0" + }, + { + "id": "78743", + "box_id": "28", + "ornament_id": "12", + "audience": "common", + "count": "92", + "used": "0" + }, + { + "id": "78744", + "box_id": "28", + "ornament_id": "13", + "audience": "common", + "count": "58", + "used": "0" + }, + { + "id": "78745", + "box_id": "28", + "ornament_id": "14", + "audience": "common", + "count": "38", + "used": "0" + }, + { + "id": "78746", + "box_id": "28", + "ornament_id": "15", + "audience": "common", + "count": "24", + "used": "0" + }, + { + "id": "78747", + "box_id": "28", + "ornament_id": "16", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78748", + "box_id": "28", + "ornament_id": "17", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "78749", + "box_id": "28", + "ornament_id": "7", + "audience": "anchor", + "count": "879", + "used": "0" + }, + { + "id": "78750", + "box_id": "28", + "ornament_id": "8", + "audience": "anchor", + "count": "557", + "used": "0" + }, + { + "id": "78751", + "box_id": "28", + "ornament_id": "9", + "audience": "anchor", + "count": "355", + "used": "0" + }, + { + "id": "78752", + "box_id": "28", + "ornament_id": "10", + "audience": "anchor", + "count": "227", + "used": "0" + }, + { + "id": "78753", + "box_id": "28", + "ornament_id": "11", + "audience": "anchor", + "count": "144", + "used": "0" + }, + { + "id": "78754", + "box_id": "28", + "ornament_id": "12", + "audience": "anchor", + "count": "92", + "used": "0" + }, + { + "id": "78755", + "box_id": "28", + "ornament_id": "13", + "audience": "anchor", + "count": "58", + "used": "0" + }, + { + "id": "78756", + "box_id": "28", + "ornament_id": "14", + "audience": "anchor", + "count": "38", + "used": "0" + }, + { + "id": "78757", + "box_id": "28", + "ornament_id": "15", + "audience": "anchor", + "count": "24", + "used": "0" + }, + { + "id": "78758", + "box_id": "28", + "ornament_id": "16", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78759", + "box_id": "28", + "ornament_id": "17", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "78760", + "box_id": "28", + "ornament_id": "7", + "audience": "robot", + "count": "879", + "used": "0" + }, + { + "id": "78761", + "box_id": "28", + "ornament_id": "8", + "audience": "robot", + "count": "557", + "used": "0" + }, + { + "id": "78762", + "box_id": "28", + "ornament_id": "9", + "audience": "robot", + "count": "355", + "used": "0" + }, + { + "id": "78763", + "box_id": "28", + "ornament_id": "10", + "audience": "robot", + "count": "227", + "used": "0" + }, + { + "id": "78764", + "box_id": "28", + "ornament_id": "11", + "audience": "robot", + "count": "144", + "used": "0" + }, + { + "id": "78765", + "box_id": "28", + "ornament_id": "12", + "audience": "robot", + "count": "92", + "used": "0" + }, + { + "id": "78766", + "box_id": "28", + "ornament_id": "13", + "audience": "robot", + "count": "58", + "used": "0" + }, + { + "id": "78767", + "box_id": "28", + "ornament_id": "14", + "audience": "robot", + "count": "38", + "used": "0" + }, + { + "id": "78768", + "box_id": "28", + "ornament_id": "15", + "audience": "robot", + "count": "24", + "used": "0" + }, + { + "id": "78769", + "box_id": "28", + "ornament_id": "16", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78770", + "box_id": "28", + "ornament_id": "17", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "78771", + "box_id": "28", + "ornament_id": "7", + "audience": "white", + "count": "879", + "used": "0" + }, + { + "id": "78772", + "box_id": "28", + "ornament_id": "8", + "audience": "white", + "count": "557", + "used": "0" + }, + { + "id": "78773", + "box_id": "28", + "ornament_id": "9", + "audience": "white", + "count": "355", + "used": "0" + }, + { + "id": "78774", + "box_id": "28", + "ornament_id": "10", + "audience": "white", + "count": "227", + "used": "0" + }, + { + "id": "78775", + "box_id": "28", + "ornament_id": "11", + "audience": "white", + "count": "144", + "used": "0" + }, + { + "id": "78776", + "box_id": "28", + "ornament_id": "12", + "audience": "white", + "count": "92", + "used": "0" + }, + { + "id": "78777", + "box_id": "28", + "ornament_id": "13", + "audience": "white", + "count": "58", + "used": "0" + }, + { + "id": "78778", + "box_id": "28", + "ornament_id": "14", + "audience": "white", + "count": "38", + "used": "0" + }, + { + "id": "78779", + "box_id": "28", + "ornament_id": "15", + "audience": "white", + "count": "24", + "used": "0" + }, + { + "id": "78780", + "box_id": "28", + "ornament_id": "16", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78781", + "box_id": "28", + "ornament_id": "17", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "78782", + "box_id": "29", + "ornament_id": "7", + "audience": "common", + "count": "878", + "used": "0" + }, + { + "id": "78783", + "box_id": "29", + "ornament_id": "8", + "audience": "common", + "count": "559", + "used": "0" + }, + { + "id": "78784", + "box_id": "29", + "ornament_id": "9", + "audience": "common", + "count": "355", + "used": "0" + }, + { + "id": "78785", + "box_id": "29", + "ornament_id": "10", + "audience": "common", + "count": "226", + "used": "0" + }, + { + "id": "78786", + "box_id": "29", + "ornament_id": "11", + "audience": "common", + "count": "144", + "used": "0" + }, + { + "id": "78787", + "box_id": "29", + "ornament_id": "12", + "audience": "common", + "count": "91", + "used": "0" + }, + { + "id": "78788", + "box_id": "29", + "ornament_id": "13", + "audience": "common", + "count": "59", + "used": "0" + }, + { + "id": "78789", + "box_id": "29", + "ornament_id": "14", + "audience": "common", + "count": "38", + "used": "0" + }, + { + "id": "78790", + "box_id": "29", + "ornament_id": "15", + "audience": "common", + "count": "24", + "used": "0" + }, + { + "id": "78791", + "box_id": "29", + "ornament_id": "16", + "audience": "common", + "count": "16", + "used": "0" + }, + { + "id": "78792", + "box_id": "29", + "ornament_id": "17", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "78793", + "box_id": "29", + "ornament_id": "7", + "audience": "anchor", + "count": "878", + "used": "0" + }, + { + "id": "78794", + "box_id": "29", + "ornament_id": "8", + "audience": "anchor", + "count": "559", + "used": "0" + }, + { + "id": "78795", + "box_id": "29", + "ornament_id": "9", + "audience": "anchor", + "count": "355", + "used": "0" + }, + { + "id": "78796", + "box_id": "29", + "ornament_id": "10", + "audience": "anchor", + "count": "226", + "used": "0" + }, + { + "id": "78797", + "box_id": "29", + "ornament_id": "11", + "audience": "anchor", + "count": "144", + "used": "0" + }, + { + "id": "78798", + "box_id": "29", + "ornament_id": "12", + "audience": "anchor", + "count": "91", + "used": "0" + }, + { + "id": "78799", + "box_id": "29", + "ornament_id": "13", + "audience": "anchor", + "count": "59", + "used": "0" + }, + { + "id": "78800", + "box_id": "29", + "ornament_id": "14", + "audience": "anchor", + "count": "38", + "used": "0" + }, + { + "id": "78801", + "box_id": "29", + "ornament_id": "15", + "audience": "anchor", + "count": "24", + "used": "0" + }, + { + "id": "78802", + "box_id": "29", + "ornament_id": "16", + "audience": "anchor", + "count": "16", + "used": "0" + }, + { + "id": "78803", + "box_id": "29", + "ornament_id": "17", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "78804", + "box_id": "29", + "ornament_id": "7", + "audience": "robot", + "count": "878", + "used": "0" + }, + { + "id": "78805", + "box_id": "29", + "ornament_id": "8", + "audience": "robot", + "count": "559", + "used": "0" + }, + { + "id": "78806", + "box_id": "29", + "ornament_id": "9", + "audience": "robot", + "count": "355", + "used": "0" + }, + { + "id": "78807", + "box_id": "29", + "ornament_id": "10", + "audience": "robot", + "count": "226", + "used": "0" + }, + { + "id": "78808", + "box_id": "29", + "ornament_id": "11", + "audience": "robot", + "count": "144", + "used": "0" + }, + { + "id": "78809", + "box_id": "29", + "ornament_id": "12", + "audience": "robot", + "count": "91", + "used": "0" + }, + { + "id": "78810", + "box_id": "29", + "ornament_id": "13", + "audience": "robot", + "count": "59", + "used": "0" + }, + { + "id": "78811", + "box_id": "29", + "ornament_id": "14", + "audience": "robot", + "count": "38", + "used": "0" + }, + { + "id": "78812", + "box_id": "29", + "ornament_id": "15", + "audience": "robot", + "count": "24", + "used": "0" + }, + { + "id": "78813", + "box_id": "29", + "ornament_id": "16", + "audience": "robot", + "count": "16", + "used": "0" + }, + { + "id": "78814", + "box_id": "29", + "ornament_id": "17", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "78815", + "box_id": "29", + "ornament_id": "7", + "audience": "white", + "count": "878", + "used": "0" + }, + { + "id": "78816", + "box_id": "29", + "ornament_id": "8", + "audience": "white", + "count": "559", + "used": "0" + }, + { + "id": "78817", + "box_id": "29", + "ornament_id": "9", + "audience": "white", + "count": "355", + "used": "0" + }, + { + "id": "78818", + "box_id": "29", + "ornament_id": "10", + "audience": "white", + "count": "226", + "used": "0" + }, + { + "id": "78819", + "box_id": "29", + "ornament_id": "11", + "audience": "white", + "count": "144", + "used": "0" + }, + { + "id": "78820", + "box_id": "29", + "ornament_id": "12", + "audience": "white", + "count": "91", + "used": "0" + }, + { + "id": "78821", + "box_id": "29", + "ornament_id": "13", + "audience": "white", + "count": "59", + "used": "0" + }, + { + "id": "78822", + "box_id": "29", + "ornament_id": "14", + "audience": "white", + "count": "38", + "used": "0" + }, + { + "id": "78823", + "box_id": "29", + "ornament_id": "15", + "audience": "white", + "count": "24", + "used": "0" + }, + { + "id": "78824", + "box_id": "29", + "ornament_id": "16", + "audience": "white", + "count": "16", + "used": "0" + }, + { + "id": "78825", + "box_id": "29", + "ornament_id": "17", + "audience": "white", + "count": "10", + "used": "0" + }, + { + "id": "78826", + "box_id": "30", + "ornament_id": "7", + "audience": "common", + "count": "869", + "used": "0" + }, + { + "id": "78827", + "box_id": "30", + "ornament_id": "8", + "audience": "common", + "count": "556", + "used": "0" + }, + { + "id": "78828", + "box_id": "30", + "ornament_id": "9", + "audience": "common", + "count": "356", + "used": "0" + }, + { + "id": "78829", + "box_id": "30", + "ornament_id": "10", + "audience": "common", + "count": "229", + "used": "0" + }, + { + "id": "78830", + "box_id": "30", + "ornament_id": "11", + "audience": "common", + "count": "146", + "used": "0" + }, + { + "id": "78831", + "box_id": "30", + "ornament_id": "12", + "audience": "common", + "count": "94", + "used": "0" + }, + { + "id": "78832", + "box_id": "30", + "ornament_id": "13", + "audience": "common", + "count": "60", + "used": "0" + }, + { + "id": "78833", + "box_id": "30", + "ornament_id": "14", + "audience": "common", + "count": "38", + "used": "0" + }, + { + "id": "78834", + "box_id": "30", + "ornament_id": "15", + "audience": "common", + "count": "25", + "used": "0" + }, + { + "id": "78835", + "box_id": "30", + "ornament_id": "16", + "audience": "common", + "count": "17", + "used": "0" + }, + { + "id": "78836", + "box_id": "30", + "ornament_id": "17", + "audience": "common", + "count": "10", + "used": "0" + }, + { + "id": "78837", + "box_id": "30", + "ornament_id": "7", + "audience": "anchor", + "count": "869", + "used": "0" + }, + { + "id": "78838", + "box_id": "30", + "ornament_id": "8", + "audience": "anchor", + "count": "556", + "used": "0" + }, + { + "id": "78839", + "box_id": "30", + "ornament_id": "9", + "audience": "anchor", + "count": "356", + "used": "0" + }, + { + "id": "78840", + "box_id": "30", + "ornament_id": "10", + "audience": "anchor", + "count": "229", + "used": "0" + }, + { + "id": "78841", + "box_id": "30", + "ornament_id": "11", + "audience": "anchor", + "count": "146", + "used": "0" + }, + { + "id": "78842", + "box_id": "30", + "ornament_id": "12", + "audience": "anchor", + "count": "94", + "used": "0" + }, + { + "id": "78843", + "box_id": "30", + "ornament_id": "13", + "audience": "anchor", + "count": "60", + "used": "0" + }, + { + "id": "78844", + "box_id": "30", + "ornament_id": "14", + "audience": "anchor", + "count": "38", + "used": "0" + }, + { + "id": "78845", + "box_id": "30", + "ornament_id": "15", + "audience": "anchor", + "count": "25", + "used": "0" + }, + { + "id": "78846", + "box_id": "30", + "ornament_id": "16", + "audience": "anchor", + "count": "17", + "used": "0" + }, + { + "id": "78847", + "box_id": "30", + "ornament_id": "17", + "audience": "anchor", + "count": "10", + "used": "0" + }, + { + "id": "78848", + "box_id": "30", + "ornament_id": "7", + "audience": "robot", + "count": "869", + "used": "0" + }, + { + "id": "78849", + "box_id": "30", + "ornament_id": "8", + "audience": "robot", + "count": "556", + "used": "0" + }, + { + "id": "78850", + "box_id": "30", + "ornament_id": "9", + "audience": "robot", + "count": "356", + "used": "0" + }, + { + "id": "78851", + "box_id": "30", + "ornament_id": "10", + "audience": "robot", + "count": "229", + "used": "0" + }, + { + "id": "78852", + "box_id": "30", + "ornament_id": "11", + "audience": "robot", + "count": "146", + "used": "0" + }, + { + "id": "78853", + "box_id": "30", + "ornament_id": "12", + "audience": "robot", + "count": "94", + "used": "0" + }, + { + "id": "78854", + "box_id": "30", + "ornament_id": "13", + "audience": "robot", + "count": "60", + "used": "0" + }, + { + "id": "78855", + "box_id": "30", + "ornament_id": "14", + "audience": "robot", + "count": "38", + "used": "0" + }, + { + "id": "78856", + "box_id": "30", + "ornament_id": "15", + "audience": "robot", + "count": "25", + "used": "0" + }, + { + "id": "78857", + "box_id": "30", + "ornament_id": "16", + "audience": "robot", + "count": "17", + "used": "0" + }, + { + "id": "78858", + "box_id": "30", + "ornament_id": "17", + "audience": "robot", + "count": "10", + "used": "0" + }, + { + "id": "78859", + "box_id": "30", + "ornament_id": "7", + "audience": "white", + "count": "869", + "used": "0" + }, + { + "id": "78860", + "box_id": "30", + "ornament_id": "8", + "audience": "white", + "count": "556", + "used": "0" + }, + { + "id": "78861", + "box_id": "30", + "ornament_id": "9", + "audience": "white", + "count": "356", + "used": "0" + }, + { + "id": "78862", + "box_id": "30", + "ornament_id": "10", + "audience": "white", + "count": "229", + "used": "0" + }, + { + "id": "78863", + "box_id": "30", + "ornament_id": "11", + "audience": "white", + "count": "146", + "used": "0" + }, + { + "id": "78864", + "box_id": "30", + "ornament_id": "12", + "audience": "white", + "count": "94", + "used": "0" + }, + { + "id": "78865", + "box_id": "30", + "ornament_id": "13", + "audience": "white", + "count": "60", + "used": "0" + }, + { + "id": "78866", + "box_id": "30", + "ornament_id": "14", + "audience": "white", + "count": "38", + "used": "0" + }, + { + "id": "78867", + "box_id": "30", + "ornament_id": "15", + "audience": "white", + "count": "25", + "used": "0" + }, + { + "id": "78868", + "box_id": "30", + "ornament_id": "16", + "audience": "white", + "count": "17", + "used": "0" + }, + { + "id": "78869", + "box_id": "30", + "ornament_id": "17", + "audience": "white", + "count": "10", + "used": "0" + } + ], + "game_olympus_box_template": [ + { + "id": "2", + "box_id": "1", + "audience": "common", + "name": "ces", + "total_rounds": "900", + "items_json": "[{\"ornamentId\":1,\"count\":592},{\"ornamentId\":2,\"count\":198},{\"ornamentId\":3,\"count\":69},{\"ornamentId\":4,\"count\":22},{\"ornamentId\":5,\"count\":8},{\"ornamentId\":6,\"count\":3},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "0", + "create_time": "2026-08-20 10:11:33" + }, + { + "id": "3", + "box_id": "1", + "audience": "common", + "name": "默认配比", + "total_rounds": "900", + "items_json": "[{\"ornamentId\":1,\"count\":596},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":65},{\"ornamentId\":4,\"count\":22},{\"ornamentId\":5,\"count\":8},{\"ornamentId\":6,\"count\":4},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "4", + "box_id": "1", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "900", + "items_json": "[{\"ornamentId\":1,\"count\":596},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":65},{\"ornamentId\":4,\"count\":22},{\"ornamentId\":5,\"count\":8},{\"ornamentId\":6,\"count\":4},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "5", + "box_id": "1", + "audience": "robot", + "name": "默认配比", + "total_rounds": "900", + "items_json": "[{\"ornamentId\":1,\"count\":596},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":65},{\"ornamentId\":4,\"count\":22},{\"ornamentId\":5,\"count\":8},{\"ornamentId\":6,\"count\":4},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "6", + "box_id": "1", + "audience": "white", + "name": "默认配比", + "total_rounds": "900", + "items_json": "[{\"ornamentId\":1,\"count\":596},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":65},{\"ornamentId\":4,\"count\":22},{\"ornamentId\":5,\"count\":8},{\"ornamentId\":6,\"count\":4},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "7", + "box_id": "2", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":552},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "8", + "box_id": "2", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":552},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "9", + "box_id": "2", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":552},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "10", + "box_id": "2", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":552},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "11", + "box_id": "3", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":552},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "12", + "box_id": "3", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":552},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "13", + "box_id": "3", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":552},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "14", + "box_id": "3", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":552},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "15", + "box_id": "4", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":556},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "16", + "box_id": "4", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":556},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "17", + "box_id": "4", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":556},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "18", + "box_id": "4", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":556},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "19", + "box_id": "5", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":557},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":48},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "20", + "box_id": "5", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":557},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":48},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "21", + "box_id": "5", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":557},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":48},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "22", + "box_id": "5", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":557},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":48},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "23", + "box_id": "6", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":556},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":48},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "24", + "box_id": "6", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":556},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":48},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "25", + "box_id": "6", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":556},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":48},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "26", + "box_id": "6", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":556},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":48},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "27", + "box_id": "7", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":550},{\"ornamentId\":2,\"count\":167},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":6},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "28", + "box_id": "7", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":550},{\"ornamentId\":2,\"count\":167},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":6},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "29", + "box_id": "7", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":550},{\"ornamentId\":2,\"count\":167},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":6},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "30", + "box_id": "7", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":550},{\"ornamentId\":2,\"count\":167},{\"ornamentId\":3,\"count\":51},{\"ornamentId\":4,\"count\":16},{\"ornamentId\":5,\"count\":6},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "31", + "box_id": "8", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":557},{\"ornamentId\":2,\"count\":164},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "32", + "box_id": "8", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":557},{\"ornamentId\":2,\"count\":164},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "33", + "box_id": "8", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":557},{\"ornamentId\":2,\"count\":164},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "34", + "box_id": "8", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":557},{\"ornamentId\":2,\"count\":164},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "35", + "box_id": "9", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":555},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "36", + "box_id": "9", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":555},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "37", + "box_id": "9", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":555},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "38", + "box_id": "9", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":555},{\"ornamentId\":2,\"count\":166},{\"ornamentId\":3,\"count\":49},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "39", + "box_id": "10", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":555},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":50},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "40", + "box_id": "10", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":555},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":50},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "41", + "box_id": "10", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":555},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":50},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "42", + "box_id": "10", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":555},{\"ornamentId\":2,\"count\":165},{\"ornamentId\":3,\"count\":50},{\"ornamentId\":4,\"count\":15},{\"ornamentId\":5,\"count\":5},{\"ornamentId\":6,\"count\":2},{\"ornamentId\":7,\"count\":2},{\"ornamentId\":8,\"count\":1},{\"ornamentId\":9,\"count\":1},{\"ornamentId\":10,\"count\":1},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "43", + "box_id": "11", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":409},{\"ornamentId\":2,\"count\":198},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":7},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "44", + "box_id": "11", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":409},{\"ornamentId\":2,\"count\":198},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":7},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "45", + "box_id": "11", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":409},{\"ornamentId\":2,\"count\":198},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":7},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "46", + "box_id": "11", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":409},{\"ornamentId\":2,\"count\":198},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":7},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "47", + "box_id": "12", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":410},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "48", + "box_id": "12", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":410},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "49", + "box_id": "12", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":410},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "50", + "box_id": "12", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":410},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "51", + "box_id": "13", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":412},{\"ornamentId\":2,\"count\":198},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":21},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":7},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "52", + "box_id": "13", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":412},{\"ornamentId\":2,\"count\":198},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":21},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":7},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "53", + "box_id": "13", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":412},{\"ornamentId\":2,\"count\":198},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":21},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":7},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "54", + "box_id": "13", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":412},{\"ornamentId\":2,\"count\":198},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":21},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":7},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "55", + "box_id": "14", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":415},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":44},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "56", + "box_id": "14", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":415},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":44},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "57", + "box_id": "14", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":415},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":44},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "58", + "box_id": "14", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":415},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":44},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "59", + "box_id": "15", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":10},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":4},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "60", + "box_id": "15", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":10},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":4},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "61", + "box_id": "15", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":10},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":4},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "62", + "box_id": "15", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":10},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":4},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "63", + "box_id": "16", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "64", + "box_id": "16", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "65", + "box_id": "16", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "66", + "box_id": "16", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "67", + "box_id": "17", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":414},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":93},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "68", + "box_id": "17", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":414},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":93},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "69", + "box_id": "17", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":414},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":93},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "70", + "box_id": "17", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":414},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":93},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "71", + "box_id": "18", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "72", + "box_id": "18", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "73", + "box_id": "18", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "74", + "box_id": "18", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":94},{\"ornamentId\":4,\"count\":45},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "75", + "box_id": "19", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":93},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "76", + "box_id": "19", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":93},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "77", + "box_id": "19", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":93},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "78", + "box_id": "19", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":413},{\"ornamentId\":2,\"count\":197},{\"ornamentId\":3,\"count\":93},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":22},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "79", + "box_id": "20", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":411},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "80", + "box_id": "20", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":411},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "81", + "box_id": "20", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":411},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "82", + "box_id": "20", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":1,\"count\":411},{\"ornamentId\":2,\"count\":196},{\"ornamentId\":3,\"count\":95},{\"ornamentId\":4,\"count\":46},{\"ornamentId\":5,\"count\":23},{\"ornamentId\":6,\"count\":11},{\"ornamentId\":7,\"count\":6},{\"ornamentId\":8,\"count\":3},{\"ornamentId\":9,\"count\":3},{\"ornamentId\":10,\"count\":2},{\"ornamentId\":11,\"count\":1},{\"ornamentId\":12,\"count\":1},{\"ornamentId\":13,\"count\":1},{\"ornamentId\":14,\"count\":1}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "83", + "box_id": "21", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":49},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":7},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "84", + "box_id": "21", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":49},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":7},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "85", + "box_id": "21", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":49},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":7},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "86", + "box_id": "21", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":49},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":7},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "87", + "box_id": "22", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":297},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":19},{\"ornamentId\":14,\"count\":11},{\"ornamentId\":15,\"count\":9},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "88", + "box_id": "22", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":297},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":19},{\"ornamentId\":14,\"count\":11},{\"ornamentId\":15,\"count\":9},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "89", + "box_id": "22", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":297},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":19},{\"ornamentId\":14,\"count\":11},{\"ornamentId\":15,\"count\":9},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "90", + "box_id": "22", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":297},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":19},{\"ornamentId\":14,\"count\":11},{\"ornamentId\":15,\"count\":9},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "91", + "box_id": "23", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":18},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "92", + "box_id": "23", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":18},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "93", + "box_id": "23", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":18},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "94", + "box_id": "23", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":18},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "95", + "box_id": "24", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":119},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "96", + "box_id": "24", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":119},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "97", + "box_id": "24", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":119},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "98", + "box_id": "24", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":119},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "99", + "box_id": "25", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":291},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":118},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":7},{\"ornamentId\":17,\"count\":3}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "100", + "box_id": "25", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":291},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":118},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":7},{\"ornamentId\":17,\"count\":3}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "101", + "box_id": "25", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":291},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":118},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":7},{\"ornamentId\":17,\"count\":3}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "102", + "box_id": "25", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":291},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":118},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":7},{\"ornamentId\":17,\"count\":3}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "103", + "box_id": "26", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":116},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":18},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "104", + "box_id": "26", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":116},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":18},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "105", + "box_id": "26", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":116},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":18},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "106", + "box_id": "26", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":116},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":18},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":6},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "107", + "box_id": "27", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":185},{\"ornamentId\":9,\"count\":119},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "108", + "box_id": "27", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":185},{\"ornamentId\":9,\"count\":119},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "109", + "box_id": "27", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":185},{\"ornamentId\":9,\"count\":119},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "110", + "box_id": "27", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":293},{\"ornamentId\":8,\"count\":185},{\"ornamentId\":9,\"count\":119},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "111", + "box_id": "28", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":29},{\"ornamentId\":13,\"count\":19},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "112", + "box_id": "28", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":29},{\"ornamentId\":13,\"count\":19},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "113", + "box_id": "28", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":29},{\"ornamentId\":13,\"count\":19},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "114", + "box_id": "28", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":187},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":29},{\"ornamentId\":13,\"count\":19},{\"ornamentId\":14,\"count\":12},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "115", + "box_id": "29", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":11},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "116", + "box_id": "29", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":11},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "117", + "box_id": "29", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":11},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "118", + "box_id": "29", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":298},{\"ornamentId\":8,\"count\":186},{\"ornamentId\":9,\"count\":117},{\"ornamentId\":10,\"count\":74},{\"ornamentId\":11,\"count\":47},{\"ornamentId\":12,\"count\":30},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":11},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":5},{\"ornamentId\":17,\"count\":4}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "119", + "box_id": "30", + "audience": "common", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":294},{\"ornamentId\":8,\"count\":185},{\"ornamentId\":9,\"count\":118},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":29},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":7},{\"ornamentId\":17,\"count\":3}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "120", + "box_id": "30", + "audience": "anchor", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":294},{\"ornamentId\":8,\"count\":185},{\"ornamentId\":9,\"count\":118},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":29},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":7},{\"ornamentId\":17,\"count\":3}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "121", + "box_id": "30", + "audience": "robot", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":294},{\"ornamentId\":8,\"count\":185},{\"ornamentId\":9,\"count\":118},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":29},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":7},{\"ornamentId\":17,\"count\":3}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + }, + { + "id": "122", + "box_id": "30", + "audience": "white", + "name": "默认配比", + "total_rounds": "800", + "items_json": "[{\"ornamentId\":7,\"count\":294},{\"ornamentId\":8,\"count\":185},{\"ornamentId\":9,\"count\":118},{\"ornamentId\":10,\"count\":75},{\"ornamentId\":11,\"count\":48},{\"ornamentId\":12,\"count\":29},{\"ornamentId\":13,\"count\":20},{\"ornamentId\":14,\"count\":13},{\"ornamentId\":15,\"count\":8},{\"ornamentId\":16,\"count\":7},{\"ornamentId\":17,\"count\":3}]", + "is_active": "1", + "create_time": "2026-09-17 20:08:07" + } + ] +} \ No newline at end of file diff --git a/olympus-py/_create_reward_log.py b/olympus-py/_create_reward_log.py new file mode 100644 index 0000000..bbdd9ee --- /dev/null +++ b/olympus-py/_create_reward_log.py @@ -0,0 +1,27 @@ +"""建奥林匹斯奖池调整日志表 game_olympus_reward_log(幂等,IF NOT EXISTS)。 +用法: python _create_reward_log.py +""" +import olympus_db as db + +DDL = """ +CREATE TABLE IF NOT EXISTS game_olympus_reward_log ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + box_id BIGINT DEFAULT NULL COMMENT '箱子ID(批量/重置为汇总,可空)', + box_name VARCHAR(100) DEFAULT NULL COMMENT '箱子名/汇总描述', + audience VARCHAR(16) NOT NULL DEFAULT 'common' COMMENT '身份 common/anchor/robot/white', + type TINYINT NOT NULL COMMENT '1注水 2抽水 3重置', + amount DECIMAL(14,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额(绝对值)', + balance_before DECIMAL(14,2) DEFAULT NULL COMMENT '调整前水位', + balance_after DECIMAL(14,2) DEFAULT NULL COMMENT '调整后水位', + pool_balance DECIMAL(14,2) DEFAULT NULL COMMENT '操作后总池余额', + operator VARCHAR(64) DEFAULT NULL COMMENT '操作人', + remark VARCHAR(200) DEFAULT NULL COMMENT '备注', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_create (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus奖池水位调整日志'; +""" + +with db.get_conn() as conn: + conn.cursor().execute(DDL) +print('game_olympus_reward_log ready.') diff --git a/olympus-py/_make_templates.py b/olympus-py/_make_templates.py new file mode 100644 index 0000000..d0cce07 --- /dev/null +++ b/olympus-py/_make_templates.py @@ -0,0 +1,71 @@ +"""给每箱 × 每身份建"默认配比"模板并置为生效。 + +为什么需要:箱子的一个 cycle 抽满后(drawn ≥ N),Java 会 + cycleRepository.resetCycle() + distributionService.refillFromConfig(boxId, audience) +而 refillFromConfig 是**按 (boxId, audience) 查生效模板**(is_active=1,取 id 最大的一条): + - 有生效模板 → 按模板 items_json 重写 count(writeDistribution),并按固定池反推回写 total_ornaments + - 没有模板 → 只把 used 归零(配比不变) +所以要让"抽完按模板重填",必须给每个 (box, audience) 都有一条生效模板。 + +用法: + python _make_templates.py --dry # 只看会写什么 + python _make_templates.py # 写入 +""" +import argparse +import json +from collections import defaultdict + +import olympus_db as db +from olympus.config import AUDIENCES + +TEMPLATE_NAME = '默认配比' + + +def build_rows(): + rows = [] + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('SELECT box_id, box_type, bet, rtp FROM game_olympus_box ORDER BY box_id') + boxes = cur.fetchall() + cur.execute('''SELECT box_id, ornament_id, audience, count + FROM game_olympus_box_ornament ORDER BY box_id, audience, ornament_id''') + cfg = defaultdict(dict) + for box_id, orn, aud, cnt in cur.fetchall(): + cfg[(int(box_id), aud)][int(orn)] = int(cnt) + for box_id, box_type, bet, rtp in boxes: + for aud in AUDIENCES: + items = cfg.get((int(box_id), aud), {}) + if not items: + print('!! box%s %s 无配比,跳过' % (box_id, aud)) + continue + items_json = json.dumps( + [{'ornamentId': k, 'count': v} for k, v in sorted(items.items())], + ensure_ascii=False, separators=(',', ':')) + rows.append((int(box_id), aud, TEMPLATE_NAME, sum(items.values()), items_json)) + return rows + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--dry', action='store_true') + args = ap.parse_args() + rows = build_rows() + print('待写模板 %d 条(%d 箱 × %d 身份)' % (len(rows), len(rows) // len(AUDIENCES), len(AUDIENCES))) + if args.dry: + print('样例:', rows[0][:4], rows[0][4][:80], '...') + return + with db.get_conn() as conn: + cur = conn.cursor() + # 先取消原有生效模板,再 upsert(表上有 uk_box_aud_name 唯一键) + cur.execute('''UPDATE game_olympus_box_template SET is_active=0''') + cur.executemany('''INSERT INTO game_olympus_box_template + (box_id, audience, name, total_rounds, items_json, is_active, create_time) + VALUES (%s,%s,%s,%s,%s,1,NOW()) + ON DUPLICATE KEY UPDATE total_rounds=VALUES(total_rounds), + items_json=VALUES(items_json), is_active=1, create_time=NOW()''', rows) + cur.execute('''SELECT COUNT(*), SUM(is_active) FROM game_olympus_box_template''') + print('模板表: 总行数/生效行数 =', cur.fetchone()) + + +if __name__ == '__main__': + main() diff --git a/olympus-py/_recalc_boxes.py b/olympus-py/_recalc_boxes.py new file mode 100644 index 0000000..84bcf2c --- /dev/null +++ b/olympus-py/_recalc_boxes.py @@ -0,0 +1,183 @@ +"""复刻后端 OlympusDistributionService.calcRounds + reset(),重算某模式各箱的桶配比。 + +为什么需要:`OlympusModeType.getOrnamentRange()` 改了(buy_fs 从 5~17 改成 7~17)之后, +后台要「一键重算」才会按新范围重配 count。这个脚本把同一套算法跑在 py 侧, +用来在没有后台操作的场合先把配比算出来(结果与后端一键重算一致)。 + +算法移植自 Java(calcRounds):基础权重 30×0.6^k → 二分 power 命中目标均值 → +按权重分配固定池 → 单调约束 count[i] ≥ count[i+1] → 只在最大的 3 个桶间微调逼近目标。 + +用法: + python _recalc_boxes.py --mode buy_fs --dry # 只看结果 + python _recalc_boxes.py --mode buy_fs # 写库(count 覆盖、used 归零) +""" +import argparse + +import olympus_db as db +from olympus.config import MODES, MODE_ORNAMENT_IDS, ORNAMENTS, payout_expect + +# 回退用的"桶期望":桶 17 展示 1000x 但封顶发放,按实测值算(见 config.PAYOUT_EXPECT_OVERRIDE) +_ORN_MUL = {o['id']: payout_expect(o['id']) for o in ORNAMENTS} + +# 该桶池子里一条回合都没有时才回退到 _ORN_MUL(名义/封顶修正值)。 +# 只要池子里有回合,就用它的实际均值:箱子发的是**自己这批固定回合**,它的均值就是这个箱子的真实期望。 +MIN_POOL_SAMPLE = 1 + + +def load_pool_expect(box_id, mode_code): + """该箱池子里每个桶的**实际**均值(AVG(actual_mul))——配比按"这个箱子真正会赔出去多少"算, + 而不是按桶区间中点:桶内均值偏差(实测 -18%~+11%)和每箱池子的抽样差异会直接变成 RTP 偏差。""" + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('''SELECT ornament_id, COUNT(*), AVG(actual_mul) FROM game_olympus_round + WHERE box_id=%s GROUP BY ornament_id''', (box_id,)) + rows = cur.fetchall() + exp = {i: _ORN_MUL[i] for i in MODE_ORNAMENT_IDS[mode_code]} + for oid, n, avg in rows: + if int(oid) in exp and int(n) >= MIN_POOL_SAMPLE and avg and float(avg) > 0: + exp[int(oid)] = float(avg) + return exp + + +def derive_total_rounds(fixed_rounds, cr): + cr = min(max(cr, 0.0), 0.9) + return int(fixed_rounds / (1.0 - cr) + 0.5) + + +def derive_fixed_rounds(total_rounds, cr): + cr = min(max(cr, 0.0), 0.9) + return int(total_rounds * (1.0 - cr) + 0.5) + + +def calc_rounds(mode_code, total_rounds, target_rtp, expect=None): + """返回 {ornament_id: count},合计 = total_rounds(对齐 Java calcRounds) + + expect:桶 → 该桶"实际赔出去多少"的期望;不传则用名义值(含桶 17 封顶修正)。""" + mul = expect or _ORN_MUL + cm = float(MODES[mode_code].cost_multiplier) + target_avg = target_rtp * cm + ids = sorted(MODE_ORNAMENT_IDS[mode_code]) + n = len(ids) + if total_rounds <= n: + return {i: 1 for i in ids} + base_w = [30.0 * (0.6 ** i) for i in range(n)] + remaining = total_rounds - n + guaranteed = sum(mul[i] for i in ids) + rem_target_avg = (target_avg * total_rounds - guaranteed) / remaining + power, plo, phi = 1.0, 0.2, 10.0 + for _ in range(30): + ws = [w ** power for w in base_w] + sw = sum(ws) + avg = sum(w * mul[i] for w, i in zip(ws, ids)) / sw if sw > 0 else 0.0 + if abs(avg - rem_target_avg) < max(rem_target_avg * 0.01, 0.001): + break + if avg < rem_target_avg: + phi = power + else: + plo = power + power = (plo + phi) / 2 + ws = [w ** power for w in base_w] + tw = sum(ws) + result = {i: 1 for i in ids} + rem = remaining + for i, w in zip(ids, ws): + extra = int(w / tw * remaining + 0.5) if tw > 0 else 0 + put = min(extra, rem) + if put > 0: + result[i] += put + rem -= put + if rem > 0: + result[ids[0]] += rem + for k in range(n - 2, -1, -1): # 单调 count[i] >= count[i+1] + if result[ids[k]] < result[ids[k + 1]]: + result[ids[k]] = result[ids[k + 1]] + diff = total_rounds - sum(result.values()) + if diff: + result[ids[0]] += diff + for _ in range(400): # 微调:任取两个桶搬 1 个(保持单调),只要更接近目标 + cur_mul = sum(result[i] * mul[i] for i in ids) + delta = target_avg * total_rounds - cur_mul + if abs(delta) < 0.01: + break + best, best_diff = None, abs(delta) + for lo in ids: + if result[lo] <= 1: + continue + for hi in ids: + if lo == hi: + continue + change = mul[hi] - mul[lo] + if abs(change) < 0.001: + continue + nd = abs(delta - change) + if nd < best_diff: + ok = True + ca, idx_lo = result[lo] - 1, ids.index(lo) + if idx_lo < n - 1 and ca < result[ids[idx_lo + 1]]: + ok = False + ca2, idx_hi = result[hi] + 1, ids.index(hi) + if idx_hi > 0 and ca2 > result[ids[idx_hi - 1]]: + ok = False + if ids.index(lo) < ids.index(hi) and ca < ca2: + ok = False + if ok: + best, best_diff = (lo, hi), nd + if best is None: + break + result[best[0]] -= 1 + result[best[1]] += 1 + return result + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--mode', default='buy_fs') + ap.add_argument('--dry', action='store_true') + args = ap.parse_args() + from olympus.config import AUDIENCES + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('''SELECT box_id, rtp, rtp_converge_ratio, total_ornaments + FROM game_olympus_box WHERE box_type=%s ORDER BY box_id''', (args.mode,)) + boxes = cur.fetchall() + print('%s 共 %d 箱,桶范围 %s;池内实际均值按**每个箱子自己的池子**算(样本<%d的桶回退名义值)' + % (args.mode, len(boxes), sorted(MODE_ORNAMENT_IDS[args.mode]), MIN_POOL_SAMPLE)) + for box_id, rtp, cr, total in boxes: + rtp = float(rtp or 0.8) + cr = float(cr if cr is not None else 0.15) + fixed = derive_fixed_rounds(int(total or 1000), cr) + pool_expect = load_pool_expect(box_id, args.mode) + counts = calc_rounds(args.mode, fixed, rtp, pool_expect) + s = sum(counts.values()) + cm = float(MODES[args.mode].cost_multiplier) + pay_pool = sum(counts[i] * pool_expect[i] for i in counts) + pay_nominal = sum(counts[i] * _ORN_MUL[i] for i in counts) + print('box%-3d 固定池%d 配比 %s' % ( + box_id, s, {k: v for k, v in sorted(counts.items()) if v > 0})) + print(' 目标RTP(本箱池内实际口径) %.2f%% 名义口径 %.2f%%' + % (100.0 * pay_pool / s / cm, 100.0 * pay_nominal / s / cm)) + if args.dry: + continue + n_cycle = derive_total_rounds(s, cr) + with db.get_conn() as conn: + cur = conn.cursor() + for aud in AUDIENCES: + cur.execute('DELETE FROM game_olympus_box_ornament WHERE box_id=%s AND audience=%s', + (box_id, aud)) + cur.executemany('''INSERT INTO game_olympus_box_ornament + (box_id, ornament_id, audience, count, used) VALUES (%s,%s,%s,%s,0)''', + [(box_id, i, aud, c) for i, c in sorted(counts.items()) if c > 0]) + cur.execute('UPDATE game_olympus_box SET total_ornaments=%s WHERE box_id=%s', + (n_cycle, box_id)) + if not args.dry: + print('已写库(count 覆盖、used 归零、total_ornaments=一轮抽数)。') + print('提醒:本脚本只动库里的 used,**不会清 Redis 的本轮账本**' + '(ruoyi:olympus:cycle:{boxId}:{audience});不清的话后台「实时RTP」会把' + '重置前的旧抽数和新抽数混在一起显示。' + '清账本:docker exec redis_cache redis-cli -a DEL ruoyi:olympus:cycle::,' + '或在后台对这几个箱点一次「一键重算分布」(它会同时清账本)。') + print('记得再跑 python _make_templates.py 刷新模板。') + + +if __name__ == '__main__': + main() diff --git a/olympus-py/_regen.py b/olympus-py/_regen.py new file mode 100644 index 0000000..c6a1d74 --- /dev/null +++ b/olympus-py/_regen.py @@ -0,0 +1,329 @@ +"""重新生成回合数据前的检查 / 快照 / 清空 / 还原(一次性脚本,用完可删)。 + + python _regen.py check # 查看箱配置、桶配比、回合池构成 + python _regen.py snapshot # 备份 box / box_ornament / box_template 到 JSON + python _regen.py clear # 清空 game_olympus_step + game_olympus_round + python _regen.py restore # 从备份还原 box_ornament 的 count(used 归 0) + python _regen.py verify # 校验:buy_fs 触发盘 4S0M、FS 内仍有炸弹、池子每桶 77 + python _regen.py fsstats [box_id] # 免费旋转观感统计:每局几次消除、几次炸弹并入、累计倍数 +""" +import json +import sys +from datetime import datetime +from pathlib import Path + +import olympus_db as db + +SNAP = Path(__file__).resolve().parent / '_backup_box_config.json' + + +def check(): + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('''SELECT box_id, box_type, bet, total_ornaments, rtp + FROM game_olympus_box ORDER BY box_id''') + boxes = cur.fetchall() + print('boxes=%d' % len(boxes)) + for b in boxes[:3] + boxes[-2:]: + print(' ', b) + cur.execute('''SELECT audience, COUNT(DISTINCT box_id), SUM(count), SUM(used) + FROM game_olympus_box_ornament GROUP BY audience''') + print('box_ornament:', cur.fetchall()) + cur.execute('''SELECT box_id, audience, COUNT(*) n, SUM(count) total, SUM(used) used + FROM game_olympus_box_ornament WHERE box_id IN (1,21,30) + GROUP BY box_id, audience ORDER BY box_id''') + for r in cur.fetchall(): + print(' cfg', r) + for box_id in (1, 21, 30): + cur.execute('''SELECT ornament_id, COUNT(*) FROM game_olympus_round + WHERE box_id=%s GROUP BY ornament_id ORDER BY ornament_id''', + (box_id,)) + print(' pool box%d:' % box_id, cur.fetchall()) + cur.execute('''SELECT ornament_id, count FROM game_olympus_box_ornament + WHERE box_id=21 AND audience='common' ORDER BY ornament_id''') + print(' cfg box21 common:', cur.fetchall()) + cur.execute('''SELECT ornament_id, count FROM game_olympus_box_ornament + WHERE box_id=30 AND audience='common' ORDER BY ornament_id''') + print(' cfg box30 common:', cur.fetchall()) + + +def snapshot(): + data = {'ts': datetime.now().isoformat(timespec='seconds')} + with db.get_conn() as conn: + cur = conn.cursor(db.pymysql.cursors.DictCursor) + for t in ('game_olympus_box', 'game_olympus_box_ornament', 'game_olympus_box_template'): + cur.execute('SELECT * FROM ' + t) + data[t] = [{k: (str(v) if v is not None else None) for k, v in row.items()} + for row in cur.fetchall()] + SNAP.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding='utf-8') + print('snapshot ->', SNAP, + {k: len(v) for k, v in data.items() if k != 'ts'}) + + +def clear(): + with db.get_conn() as conn: + cur = conn.cursor() + for t in ('game_olympus_step', 'game_olympus_round'): + cur.execute('TRUNCATE TABLE ' + t) + print('truncated', t) + print('max_round_id =', db.get_max_round_id(), 'max_step_id =', db.get_max_step_id()) + + +def restore(): + data = json.loads(SNAP.read_text(encoding='utf-8')) + rows = data['game_olympus_box_ornament'] + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('SELECT COUNT(*) FROM game_olympus_box_ornament') + before = cur.fetchone()[0] + cur.execute('DELETE FROM game_olympus_box_ornament') + cur.executemany('''INSERT INTO game_olympus_box_ornament + (box_id, ornament_id, audience, count, used) VALUES (%s,%s,%s,%s,0)''', + [(int(r['box_id']), int(r['ornament_id']), r['audience'], + int(r['count'])) for r in rows]) + print('box_ornament: %d -> %d rows (used=0)' % (before, len(rows))) + # box 行对比:库里现值 vs 备份值 + dc = conn.cursor(db.pymysql.cursors.DictCursor) + dc.execute('''SELECT box_id, box_type, bet, total_ornaments, rtp, price, status, sort + FROM game_olympus_box ORDER BY box_id''') + old = {int(r['box_id']): r for r in data['game_olympus_box']} + for r in dc.fetchall(): + o = old.get(int(r['box_id'])) + if o is None: + print(' box %s: 新增' % r['box_id']) + continue + diff = {k: (o[k], str(r[k])) for k in r + if str(o[k]) != str(r[k])} + if diff: + print(' box %s 变化:' % r['box_id'], diff) + + +def verify(): + """grid_state 里数 '"S"'/'"M"' 出现次数(短码是独立 JSON 字符串,不会误匹配符号名)。""" + cnt = ("(CHAR_LENGTH(s.grid_state)-CHAR_LENGTH(REPLACE(s.grid_state,'\"%s\"','')))/3") + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('SELECT COUNT(*) FROM game_olympus_round') + rounds = cur.fetchone()[0] + cur.execute('SELECT COUNT(*) FROM game_olympus_step') + steps = cur.fetchone()[0] + cur.execute('SELECT box_id, COUNT(*) FROM game_olympus_round GROUP BY box_id ORDER BY box_id') + per_box = {r[0]: r[1] for r in cur.fetchall()} + print('rounds=%d steps=%d 每箱回合=%s' % (rounds, steps, sorted(set(per_box.values())))) + cur.execute('''SELECT COUNT(*) FROM game_olympus_round WHERE box_id NOT BETWEEN 1 AND 30''') + print('越界 box_id 回合:', cur.fetchone()[0]) + + # buy_fs 触发盘:每步 4 个 S、0 个 M + cur.execute(f'''SELECT COUNT(*), SUM({cnt % 'S'}), SUM({cnt % 'M'}), MAX(s.cascade_seq) + FROM game_olympus_step s JOIN game_olympus_round r USING(round_id) + WHERE r.box_id BETWEEN 21 AND 30 AND s.free_spin_id=0''') + n, sc, bm, maxseq = cur.fetchone() + print('buy_fs 触发盘: 步数=%s S总数=%s(应=%s) M总数=%s(应=0) 最大级联页=%s' + % (n, sc, 4 * (n or 0), bm, maxseq)) + + # 免费旋转内仍然有炸弹(累计器机制没被误伤) + cur.execute(f'''SELECT COUNT(*), SUM({cnt % 'M'}) FROM game_olympus_step s + JOIN game_olympus_round r USING(round_id) + WHERE s.free_spin_id > 0''') + fn, fm = map(float, cur.fetchone()) + fm = fm or 0.0 + cur.execute(f'''SELECT COUNT(*), SUM({cnt % 'M'}) FROM game_olympus_step s + JOIN game_olympus_round r USING(round_id) + WHERE r.box_id <= 10 AND s.free_spin_id = 0''') + nn, nm = map(float, cur.fetchone()) + nm = nm or 0.0 + print('FS 步数=%s 带炸弹格=%s (%.1f%%)' % (fn, fm, 100.0 * (fm or 0) / (30 * fn))) + print('normal 基础盘步数=%s 带炸弹格=%s (%.1f%%)' % (nn, nm, 100.0 * (nm or 0) / (30 * nn))) + + # ante 基础旋同样不许有炸弹;normal/ante 的 100x+ 桶(基础旋无炸弹赔不出来)应全是 FS 回合 + cur.execute(f'''SELECT COUNT(*), SUM({cnt % 'M'}) FROM game_olympus_step s + JOIN game_olympus_round r USING(round_id) + WHERE r.box_id BETWEEN 11 AND 20 AND s.free_spin_id = 0''') + an, am = map(float, cur.fetchone()) + am = am or 0.0 + print('ante 基础盘步数=%s 带炸弹格=%s (%.1f%%)' % (an, am, 100.0 * am / (30 * an))) + cur.execute('''SELECT r.ornament_id, COUNT(*) 回合, SUM(r.free_spin_count>0) FS回合 + FROM game_olympus_round r WHERE r.box_id <= 20 AND r.ornament_id >= 12 + GROUP BY r.ornament_id ORDER BY r.ornament_id''') + print('normal/ante 100x+ 桶(应 回合=FS回合):', cur.fetchall()) + cur.execute('''SELECT r.box_id, COUNT(*) 回合, SUM(r.free_spin_count>0) FS回合 + FROM game_olympus_round r WHERE r.box_id <= 20 + GROUP BY r.box_id ORDER BY r.box_id''') + print('normal/ante 每箱 FS 回合数:', cur.fetchall()) + + # 爆奖档位:只有小/中/大,且只落在免费旋转的旋末帧 + cur.execute('''SELECT bigwin_level, COUNT(*) FROM game_olympus_step + GROUP BY bigwin_level ORDER BY bigwin_level''') + print('爆奖档位分布:', cur.fetchall()) + cur.execute('''SELECT COUNT(*) FROM game_olympus_step + WHERE bigwin_level IS NOT NULL AND (is_final <> 1 OR free_spin_id = 0)''') + print('爆奖档位落在基础旋/非末帧的行数(应=0):', cur.fetchone()[0]) + + # 盘面绝不能有空格(前端会显示成空白格子) + cur.execute('''SELECT COUNT(*) FROM game_olympus_step WHERE grid_state LIKE '%""%' ''') + print('盘面空白格行数(应=0):', cur.fetchone()[0]) + + # 池子构成(每桶应≈77) + cur.execute('''SELECT box_id, MIN(c), MAX(c) FROM + (SELECT box_id, ornament_id, COUNT(*) c FROM game_olympus_round + GROUP BY box_id, ornament_id) t GROUP BY box_id ORDER BY box_id''') + print('每箱桶内回合数 min/max:', sorted({(r[1], r[2]) for r in cur.fetchall()})) + + # 配比是否为后台调好的曲线(非均分) + cur.execute('''SELECT box_id, SUM(count) FROM game_olympus_box_ornament + GROUP BY box_id, audience ORDER BY box_id LIMIT 3''') + print('box_ornament 前3箱×(身份) sum(count):', cur.fetchall()) + + +def fsstats(): + """免费旋转观感统计(按桶):每局"有消除的旋数"、"炸弹并入累计器的次数"、局末累计倍数中位数。 + 玩家真正感受到的是"按后台配比加权"后的那一行 —— 低倍桶占比高时观感就被低倍桶决定。""" + import statistics + from collections import defaultdict, Counter + from olympus.config import ORNAMENTS + _orn = {o['id']: o['name'] for o in ORNAMENTS} + box_id = int(sys.argv[2]) if len(sys.argv) > 2 else 21 + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('''SELECT r.round_id, r.ornament_id, s.free_spin_id, s.base_score, + s.accumulator_before, s.accumulator_after + FROM game_olympus_step s JOIN game_olympus_round r USING(round_id) + WHERE r.box_id=%s AND s.free_spin_id>0 ORDER BY s.round_id, s.id''', (box_id,)) + rows = cur.fetchall() + cur.execute('''SELECT ornament_id, count FROM game_olympus_box_ornament + WHERE box_id=%s AND audience=%s''', (box_id, 'common')) + cfg = {int(a): int(b) for a, b in cur.fetchall()} + cur.execute('SELECT ornament_id, AVG(actual_mul) FROM game_olympus_round WHERE box_id=%s GROUP BY ornament_id', (box_id,)) + avg = {int(a): float(b) for a, b in cur.fetchall()} + spins = {} + for rid, orn, fs, base, ab, aa in rows: + d = spins.setdefault((rid, orn, fs), {'elim': False, 'ab': 0.0, 'aa': 0.0}) + if float(base or 0) > 0: + d['elim'] = True + d['ab'] = float(ab or 0) + d['aa'] = float(aa or 0) + per = defaultdict(Counter) + rounds_of = defaultdict(set) + acc_of = {} + for (rid, orn, fs), d in spins.items(): + c = per[orn] + c['spins'] += 1 + rounds_of[orn].add(rid) + acc_of[rid] = max(acc_of.get(rid, 0.0), d['aa']) + if d['elim']: + c['elim'] += 1 + if d['aa'] > d['ab']: + c['merges'] += 1 + total = sum(cfg.values()) or 1 + print('box%d 免费旋转观感(桶 抽中率 | 每局消除旋数 | 每局炸弹并入 | 局末累计中位 | 该桶实际均值)' % box_id) + w_elim = w_merges = 0.0 + for orn in sorted(per): + c = per[orn] + n = len(rounds_of[orn]) or 1 + elim_per, merge_per = c['elim'] / n, c['merges'] / n + accs = [acc_of[r] for r in rounds_of[orn]] + w = cfg.get(orn, 0) / total + w_elim += w * elim_per + w_merges += w * merge_per + print(' %2d %6sx %5.1f%% | %6.1f 旋 | %6.2f 次 | %8.0f | %8.2f' % ( + orn, _orn.get(orn, '?'), 100 * w, elim_per, merge_per, + statistics.median(accs) if accs else 0, avg.get(orn, 0))) + print(' ── 玩家加权(按配比):每局 %.1f 旋有消除、%.2f 次炸弹并入' % (w_elim, w_merges)) + + +def fsdry(): + """免费旋转"连续空转"统计:一局里最长连续几旋一个符号都没消(玩家观感=一直白转)。 + + dry = 该旋所有步的 base_score 都是 0(scatter 赔付不算消除:S 不参与消除)。 + 只统计有 FS 的回合;1000x 封顶截断的回合里被丢掉的旋按缺失处理,不参与连击计数。 + """ + from collections import Counter, defaultdict + from olympus.config import ORNAMENTS + _orn = {o['id']: o['name'] for o in ORNAMENTS} + box_id = int(sys.argv[2]) if len(sys.argv) > 2 else None + sql = '''SELECT r.round_id, r.box_id, r.ornament_id, s.free_spin_id, + SUM(s.base_score > 0) elim + FROM game_olympus_step s JOIN game_olympus_round r USING(round_id) + WHERE s.free_spin_id > 0''' + args = () + if box_id: + sql += ' AND r.box_id=%s' + args = (box_id,) + sql += ''' GROUP BY r.round_id, r.box_id, r.ornament_id, s.free_spin_id + ORDER BY r.round_id, s.free_spin_id''' + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute(sql, args) + rows = cur.fetchall() + cur.execute('SELECT box_id, box_type FROM game_olympus_box') + btype = {int(a): str(b) for a, b in cur.fetchall()} + cur.execute("""SELECT box_id, ornament_id, count FROM game_olympus_box_ornament + WHERE audience='common'""") + cfg = defaultdict(dict) + for b, o, c in cur.fetchall(): + cfg[int(b)][int(o)] = int(c) + + per_round, meta = defaultdict(list), {} + for rid, b, o, fs, elim in rows: + per_round[int(rid)].append((int(fs), float(elim or 0) > 0)) + meta[int(rid)] = (int(b), int(o)) + + # rid -> (box, orn, 旋数, 有消除旋数, 最长连空) + stat = {} + for rid, seq in per_round.items(): + run = mx = 0 + for _, e in sorted(seq): + run = 0 if e else run + 1 + mx = max(mx, run) + stat[rid] = (meta[rid][0], meta[rid][1], len(seq), sum(e for _, e in seq), mx) + + def show(tag, rids, weight_by_cfg=False): + """weight_by_cfg:≥4 连空的概率按后台配比 count 加权(= 玩家实际遇到的概率)""" + if not rids: + return [] + dist, wsum, p4 = Counter(), 0.0, 0.0 + spins = elim = 0 + worst = [] + for rid in rids: + b, o, n, e, mx = stat[rid] + w = (cfg.get(b, {}).get(o, 0) or 1) if weight_by_cfg else 1 + wsum += w + spins += n + elim += e + dist[min(mx, 6)] += 1 # 6 及以上并作一格,否则 >6 的局不进任何格、占比会莫名少一截 + if mx >= 4: + p4 += w + worst.append((mx, rid)) + n = len(rids) + worst.sort(reverse=True) + print('%-12s 局=%5d 旋/局=%5.1f 有消除=%4.1f/局(%4.1f%%) | 最长连空 0~6+ : %s | ≥4连空 %5.1f%%' + % (tag, n, spins / n, elim / n, 100.0 * elim / max(1, spins), + ' '.join('%d%s:%.0f%%' % (k, '+' if k == 6 else '', 100.0 * dist[k] / n) + for k in range(7)), + 100.0 * p4 / wsum)) + return worst + + by_mode = defaultdict(list) + for rid, (b, o) in meta.items(): + by_mode[btype.get(b, '?')].append(rid) + print('=== 免费旋转"连续空转"(dry = 该旋 base_score 全 0;≥4连空 一列买 FS 按配比加权)===') + all_worst = [] + for mode in ('normal', 'ante', 'buy_fs'): + rids = by_mode.get(mode) or [] + if not rids: + continue + all_worst += [(mx, rid, mode, meta[rid]) for mx, rid in show(mode, rids)[:5]] + by_orn = defaultdict(list) # 同一模式的 10 个箱合并看桶 + for rid in rids: + by_orn[meta[rid][1]].append(rid) + for o in sorted(by_orn): + show(' %s %s' % (mode, _orn.get(o, o)), by_orn[o], weight_by_cfg=True) + all_worst.sort(reverse=True) + print('--- 最差局(最长连空 / round_id / 模式 / 桶)---') + for mx, rid, mode, (b, o) in all_worst[:12]: + print(' 连空%d旋 round=%d %s box%d 桶%s' % (mx, rid, mode, b, _orn.get(o, o))) + + +if __name__ == '__main__': + cmd = sys.argv[1] if len(sys.argv) > 1 else 'check' + globals()[cmd]() diff --git a/olympus-py/_reset_boxes.py b/olympus-py/_reset_boxes.py new file mode 100644 index 0000000..edd10b1 --- /dev/null +++ b/olympus-py/_reset_boxes.py @@ -0,0 +1,21 @@ +"""一次性重置:清空箱子相关数据(box_id 改从 1 起、总回合调整时用)。 +保留 game_olympus_ornament(桶定义) 与 game_olympus_prize_*(返奖饰品)。 +用法: python _reset_boxes.py +""" +import olympus_db as db + +# 子表在前(无外键,仅为清晰) +TABLES = [ + 'game_olympus_step', + 'game_olympus_round', + 'game_olympus_box_ornament', + 'game_olympus_box_template', + 'game_olympus_box', +] + +with db.get_conn() as conn: + cur = conn.cursor() + for t in TABLES: + cur.execute(f'TRUNCATE TABLE {t}') + print('truncated', t) +print('reset done. max_round_id =', db.get_max_round_id()) diff --git a/olympus-py/generate_rounds.py b/olympus-py/generate_rounds.py new file mode 100644 index 0000000..bf59432 --- /dev/null +++ b/olympus-py/generate_rounds.py @@ -0,0 +1,295 @@ +""" +Olympus 回合生成运行入口 — 只负责回合库存的多样性 + +职责说明: + - Python 端: 为模式允许的每个饰品桶预生成足够数量的回合(库存),保证多样性 + - 后台端: 通过配置各桶的数量分布(game_olympus_box_ornament.count)控制箱子 + 抽完的实时 RTP 是否达到目标,本脚本不做 RTP 收敛 + +用法: + python generate_rounds.py # 干跑: 每模式每桶生成20个验证多样性 + python generate_rounds.py --all --per-box 200 --write # 全部模式×档位建箱写库(box_id从1,每箱总回合200) + python generate_rounds.py --all --alloc config --write # 池子按后台桶配比分配(推荐):每桶 = 配比 ×1.5 + python generate_rounds.py --box-type normal --bet 1 --per-box 200 --write # 单箱写库 +""" +import argparse +import time +from collections import Counter + +from olympus.config import MODES, MODE_ORNAMENT_IDS, ORNAMENTS, RTP, AUDIENCES +from olympus_simulator import fill_distribution + +_ORN = {o['id']: o for o in ORNAMENTS} +ALLOC_K = 1.5 # --alloc config 时,每桶池子 = 配比 × 该系数(留余量给"不重复发") +# 每桶池子下限。只对"配比 × ALLOC_K < 下限"的桶起作用,也就是**配比 ≤13 的桶(6.5x 以上)**: +# 这些桶条数少、盘面又显眼(有赢奖/大奖动画),是玩家真正会"看着眼熟"的那几个,所以 20 → 60 +# 把它们的复现间隔拉长 3 倍(实测 10x 桶 5,455 抽 → 16,364 抽、100x 桶 6 万 → 18 万抽), +# 而 0x~4x 这些"量大但没人记住"的桶走 ALLOC_K、完全不受影响 —— 池子总量只 +8%。 +ALLOC_FLOOR = 60 # 每桶池子下限 + +# 并行写库用:round_id 按"该箱池子的实际条数"连续切段(池子条数由配比唯一决定,能事前算准,见 planned_rounds); +# 步数取决于随机级联、事前算不准,只能给每箱预留一段固定 id 空间。 +# 注意要按**最重的箱**留:buy_fs 每箱 ~84,000 步(每局都是 15~30 旋的 FS),normal/ante 只有 ~1.1w 步(大多 1~2 步的基础旋)。 +BOX_STEP_BAND = 200_000 + + +def read_config_counts(box_id): + """读该箱现有桶配比(各身份取最大值)。没有配置时返回 {}。""" + import olympus_db as db + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('''SELECT ornament_id, MAX(count) FROM game_olympus_box_ornament + WHERE box_id=%s GROUP BY ornament_id''', (box_id,)) + return {int(a): int(b) for a, b in cur.fetchall()} + + +def build_counts_from_config(mode_code, box_id, k=ALLOC_K): + """按后台桶配比分配池子:每桶回合数 = max(下限, 配比 × k)。 + 池子 ≥ 配比 → 一个 cycle 内每个回合最多发一次(配合 Java 侧"不重复发")。""" + cfg = read_config_counts(box_id) + counts = {} + for i in MODE_ORNAMENT_IDS[mode_code]: + c = cfg.get(i, 0) + if c > 0: + counts[i] = max(ALLOC_FLOOR, int(round(c * k))) + return counts + + +def build_counts(mode_code, per_bucket): + """每个可用桶生成相同数量的回合库存,后台再按需配比""" + return {i: per_bucket for i in MODE_ORNAMENT_IDS[mode_code]} + + +def build_counts_total(mode_code, total): + """每箱固定总回合数,跨该模式可用桶均分(余数分给靠前的桶),保证每桶≥1""" + ids = list(MODE_ORNAMENT_IDS[mode_code]) + n = len(ids) + base, rem = divmod(total, n) + return {i: base + (1 if idx < rem else 0) for idx, i in enumerate(ids)} + + +def seq_box_id(mode_code, bet): + """全局顺序 box_id(从 1 开始):按模式顺序×档位顺序编号""" + idx = 0 + for mc, mode in MODES.items(): + for b in mode.base_bets: + idx += 1 + if mc == mode_code and b == bet: + return idx + return idx + + +def report(mode_code, counts, rounds): + total_target = sum(counts.values()) + dist = Counter(rd['ornament_id'] for rd, _ in rounds) + print(f'[{mode_code}] rounds={len(rounds)}/{total_target}') + for i in sorted(counts): + hits = [rd for rd, _ in rounds if rd['ornament_id'] == i] + if hits: + muls = [rd['actual_mul'] for rd in hits] + casc = [rd['total_cascades'] for rd in hits] + fs = sum(1 for rd in hits if rd['free_spin_count'] > 0) + extra = (f'倍率{min(muls):.2f}~{max(muls):.2f} ' + f'步数{min(casc)}~{max(casc)} FS回合{fs}') + else: + extra = '' + print(f' 桶{i:>2} {_ORN[i]["name"]:>6}: {dist.get(i, 0):>5}/{counts[i]:>5} {extra}') + capped = sum(1 for _, ss in rounds if any(s['win_capped'] for s in ss)) + overmax = sum(1 for rd, _ in rounds if rd['actual_mul'] > 1000) + print(f' 封顶截断={capped} 超1000={overmax}') + + +def dry_run(per_bucket=20): + for mode_code in MODES: + counts = build_counts(mode_code, per_bucket) + t0 = time.time() + rounds, _ = fill_distribution({'box_id': 0, 'box_type': mode_code}, counts) + report(mode_code, counts, rounds) + print(f' 耗时 {time.time() - t0:.1f}s\n') + + +def derive_cycle_rounds(box_id, fixed_sum, default_cr=0.15): + """一轮抽多少抽 N = round(固定池 /(1-收敛比例)),对齐 Java deriveTotalRounds。 + box.total_ornaments 的语义是"一轮总抽数",不是池子大小(抽完补池时 Java 也会回写这个值)。""" + import olympus_db as db + cr = default_cr + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('SELECT rtp_converge_ratio FROM game_olympus_box WHERE box_id=%s', (box_id,)) + row = cur.fetchone() + if row and row[0] is not None: + cr = float(row[0]) + cr = min(max(cr, 0.0), 0.9) + return int(round(fixed_sum / (1.0 - cr))) + + +def write_box(mode_code, bet, box_id, per_box, alloc='flat', round_base=None, step_base=None, planned=None): + import olympus_db as db + mode = MODES[mode_code] + if bet not in mode.base_bets: + raise SystemExit(f'模式 {mode_code} 不支持档位 {bet},可选: {mode.base_bets}') + price = float(mode.price_for(bet)) + counts = build_counts_from_config(mode_code, box_id) if alloc == 'config' else {} + if not counts: + counts = build_counts_total(mode_code, per_box) + total_rounds = sum(counts.values()) + config_counts = read_config_counts(box_id) or dict(counts) + box = {'box_id': box_id, 'box_type': mode_code, 'bet': bet, + 'name': f'{mode.name}-bet{bet}', 'price': price, + 'total_ornaments': derive_cycle_rounds(box_id, sum(config_counts.values())), + 'rtp': RTP} + print(f'生成 box={box_id} {box["name"]} price={price} ' + f'分配方式={alloc} 池子{total_rounds}条 / 一轮{box["total_ornaments"]}抽 ...') + t0 = time.time() + rounds, gen = fill_distribution(box, counts) + report(mode_code, counts, rounds) + if planned is not None and len(rounds) != planned: + raise SystemExit(f'box={box_id}: 实际生成 {len(rounds)} 条 ≠ 预留 id 段 {planned} 条,' + f'id 段会错位 —— 重跑一次(配比刚改过就先 --clear)') + + round_base = db.get_max_round_id() if round_base is None else round_base + step_base = db.get_max_step_id() if step_base is None else step_base + rounds_data, steps_data = [], [] + rid, sid = round_base, step_base + for rd, steps in rounds: + rid += 1 + rd['round_id'] = rid + rounds_data.append(rd) + for s in steps: + sid += 1 + s['id'] = sid + s['round_id'] = rid + steps_data.append(s) + if len(steps_data) > BOX_STEP_BAND: + raise SystemExit(f'box={box_id}: {len(steps_data)} 步超出每箱预留段 {BOX_STEP_BAND},' + f'把 BOX_STEP_BAND 调大再跑') + + db.insert_box(box) + # box_ornament:已有后台配比则原样写回(池子形状 ≠ 配比,别把配比覆盖成池子形状); + # 首次建箱才用实际生成量做种子 + keep = read_config_counts(box_id) or dict(gen) + db.write_box_distributions(box_id, {aud: dict(keep) for aud in AUDIENCES}) + db.write_rounds_and_steps(box_id, rounds_data, steps_data) + print(f'写库完成: {len(rounds_data)} rounds / {len(steps_data)} steps, ' + f'耗时 {time.time() - t0:.1f}s\n') + + +def all_boxes(): + """(box_id, mode_code, bet) 顺序编号,与旧 --all 完全一致(1..30)""" + boxes = [] + box_id = 0 + for mode_code, mode in MODES.items(): + for bet in mode.base_bets: + box_id += 1 + boxes.append((box_id, mode_code, bet)) + return boxes + + +def _box_task(task): + """worker 入口:跑一箱(id 段由任务自带,互不重叠)""" + box_id, mode_code, bet, per_box, alloc, r_base, s_base, planned = task + write_box(mode_code, bet, box_id, per_box, alloc, r_base, s_base, planned) + return box_id + + +def planned_rounds(mode_code, box_id, per_box, alloc): + """这箱这一轮会生成多少条回合:池子条数 = Σ max(ALLOC_FLOOR, 配比×ALLOC_K),由配比唯一决定。 + 用它给每箱切一段**连续**的 round_id(并行写库不会撞主键,id 也不会有空洞)。""" + counts = build_counts_from_config(mode_code, box_id) if alloc == 'config' else {} + if not counts: + counts = build_counts_total(mode_code, per_box) + return sum(counts.values()) + + +def clear_pool_tables(): + """TRUNCATE 回合池两张表:清空全部 30 箱的旧回合,id 从 1 重排。 + 注意 game_olympus_record(战绩)里的 round_id 会因此指向新池子里的同号回合 —— 战绩金额本身不受影响。""" + import olympus_db as db + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('TRUNCATE TABLE game_olympus_step') + cur.execute('TRUNCATE TABLE game_olympus_round') + print('已 TRUNCATE game_olympus_step / game_olympus_round(旧池子清空,round_id 从 1 重排)') + + +def _existing_rounds(box_id): + import olympus_db as db + with db.get_conn() as conn: + cur = conn.cursor() + cur.execute('SELECT COUNT(*) FROM game_olympus_round WHERE box_id=%s', (box_id,)) + return int(cur.fetchone()[0]) + + +def run_all(alloc, per_box, workers, clear=False, resume=False): + """--all:顺序(workers=1)或按箱并行写库。 + 生成是纯 CPU 活(多线程没用,要进程);各箱 round_id 段连续不重叠,所以并行也不会撞主键。 + --resume:跳过已经写好的箱(条数 == 计划条数),只补剩下的 —— id 段仍按整轮 30 箱排,所以布局和整轮跑一致。 + 进程数:本机只有 4 个物理核(8 逻辑),而且 VS Code/IDEA/Docker 常驻,建议 1~2。""" + import olympus_db as db + if clear: + clear_pool_tables() + cursor, step_base0 = 0, 0 + else: + cursor, step_base0 = db.get_max_round_id(), db.get_max_step_id() + tasks = [] + for bid, mc, bet in all_boxes(): + n = planned_rounds(mc, bid, per_box, alloc) + tasks.append((bid, mc, bet, per_box, alloc, + cursor, step_base0 + bid * BOX_STEP_BAND, n)) + cursor += n + if resume: + left = [t for t in tasks if _existing_rounds(t[0]) != t[7]] + print(f'--resume:已写好 {len(tasks) - len(left)} 箱,待生成 {len(left)} 箱') + tasks = left + if not tasks: + print('没有需要生成的箱') + return + print(f'共 {len(tasks)} 箱,round_id 段 {tasks[0][5] + 1} ~ {cursor}(连续、不重叠)') + if workers <= 1: + t0 = time.time() + for done, t in enumerate(tasks, 1): + _box_task(t) + print(f'--- [{done}/{len(tasks)}] box={t[0]} 完成 累计 {time.time() - t0:.1f}s') + return + import multiprocessing as mp + t0 = time.time() + print(f'并行 {workers} 进程 ...') + with mp.Pool(processes=workers) as pool: + for done, bid in enumerate(pool.imap_unordered(_box_task, tasks), 1): + print(f'--- [{done}/{len(tasks)}] box={bid} 完成 累计 {time.time() - t0:.1f}s') + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--box-type', choices=list(MODES), help='模式') + ap.add_argument('--bet', type=int, help='基础下注档位') + ap.add_argument('--per-bucket', type=int, default=20, help='干跑每桶回合数(仅 dry-run 用)') + ap.add_argument('--per-box', type=int, default=200, help='每箱总回合数(--alloc flat 用,跨桶均分)') + ap.add_argument('--alloc', choices=['flat', 'config'], default='flat', + help='flat=跨桶均分(旧行为);config=按后台桶配比×1.5 分配(池子≥配比)') + ap.add_argument('--write', action='store_true', help='写入数据库') + ap.add_argument('--all', action='store_true', help='全部模式×档位,box_id 从 1 顺序编号') + ap.add_argument('--workers', type=int, default=1, + help='并行进程数(生成是纯 CPU 活,多线程没用;建议 ≤3,DB 连接数限制)') + ap.add_argument('--clear', action='store_true', + help='先 TRUNCATE 回合池两张表(全部箱),round_id 从 1 重排;只配合 --all 用') + ap.add_argument('--resume', action='store_true', + help='只补还没写好的箱(跳过已写完的),id 段仍按整轮 30 箱排') + args = ap.parse_args() + + if args.clear and not args.all: + raise SystemExit('--clear 会清空全部箱的回合池,只配合 --all 用') + if not args.write: + dry_run(min(args.per_bucket, 20)) + return + if args.all: + run_all(args.alloc, args.per_box, args.workers, args.clear, args.resume) + elif args.box_type and args.bet: + write_box(args.box_type, args.bet, seq_box_id(args.box_type, args.bet), + args.per_box, args.alloc) + else: + raise SystemExit('--write 需要 --box-type + --bet,或 --all') + + +if __name__ == '__main__': + main() diff --git a/olympus-py/olympus/__init__.py b/olympus-py/olympus/__init__.py new file mode 100644 index 0000000..2502f0e --- /dev/null +++ b/olympus-py/olympus/__init__.py @@ -0,0 +1,36 @@ +"""Olympus 电子游戏规则核心 (0.1.0)。""" + +from .config import ( + SYMBOL_RULES, + SCATTER_CODE, + SCATTER_PAYOUTS, + MODES, + DEFAULT_MODE, + MAX_WIN_MULTIPLIER, + GameMode, +) + +__version__ = "0.1.0" + +__all__ = [ + # config + "SYMBOL_RULES", + "SCATTER_CODE", + "SCATTER_PAYOUTS", + "MODES", + "DEFAULT_MODE", + "MAX_WIN_MULTIPLIER", + "GameMode", + # engine + "OlympusEngine", + "RuleViolation", + # models + "Bomb", + "CascadeResult", + "Elimination", + "SessionState", + "SpinResult", +] + +from .engine import OlympusEngine, RuleViolation +from .models import Bomb, CascadeResult, Elimination, SessionState, SpinResult diff --git a/olympus-py/olympus/config.py b/olympus-py/olympus/config.py new file mode 100644 index 0000000..2eee350 --- /dev/null +++ b/olympus-py/olympus/config.py @@ -0,0 +1,311 @@ +from __future__ import annotations + +from dataclasses import dataclass +from decimal import Decimal +from typing import Mapping, Tuple + + +@dataclass(frozen=True) +class SymbolRule: + code: str + name: str + payouts: Mapping[int, Decimal] + + def payout_for(self, count: int) -> Decimal: + for threshold in sorted(self.payouts, reverse=True): + if count >= threshold: + return self.payouts[threshold] + return Decimal("0") + + +# 赔付倍数以 1 倍下注为基准,数值对齐官方赔付表(8-9 / 10-11 / 12-30 三档) +SYMBOL_RULES = { + "CROWN": SymbolRule("CROWN", "皇冠", {12: Decimal("50"), 10: Decimal("25"), 8: Decimal("10")}), + "HOURGLASS": SymbolRule("HOURGLASS", "沙漏", {12: Decimal("25"), 10: Decimal("10"), 8: Decimal("2.5")}), + "RING": SymbolRule("RING", "环", {12: Decimal("15"), 10: Decimal("5"), 8: Decimal("2")}), + "CHALICE": SymbolRule("CHALICE", "杯赛", {12: Decimal("12"), 10: Decimal("2"), 8: Decimal("1.5")}), + "RED_GEM": SymbolRule("RED_GEM", "红宝石", {12: Decimal("10"), 10: Decimal("1.5"), 8: Decimal("1")}), + "PURPLE_GEM": SymbolRule("PURPLE_GEM", "紫宝石", {12: Decimal("8"), 10: Decimal("1.2"), 8: Decimal("0.8")}), + "YELLOW_GEM": SymbolRule("YELLOW_GEM", "黄宝石", {12: Decimal("5"), 10: Decimal("1"), 8: Decimal("0.5")}), + "GREEN_GEM": SymbolRule("GREEN_GEM", "绿宝石", {12: Decimal("4"), 10: Decimal("0.9"), 8: Decimal("0.4")}), + "BLUE_GEM": SymbolRule("BLUE_GEM", "蓝宝石", {12: Decimal("2"), 10: Decimal("0.75"), 8: Decimal("0.25")}), +} + +SCATTER_CODE = "SCATTER" +# 6/5/4 个 SCATTER 的赔付倍数(1 倍下注基准,与皇冠系列量表同源) +SCATTER_PAYOUTS = { + 6: Decimal("100"), + 5: Decimal("5"), + 4: Decimal("3"), +} +SCATTER_FREE_SPIN_TRIGGER = 4 # 基础盘 4+ SCATTER 触发免费旋转 +SCATTER_RETRIGGER_TRIGGER = 3 # 免费旋转中 3+ SCATTER 触发重触发 +FREE_SPINS_AWARDED = 15 # 首次触发赠送次数 +FREE_SPIN_RETRIGGER_COUNT = 5 # 每次重触发追加次数 + +MULTIPLIER_VALUES = frozenset({ + 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 25, 50, 100, 250, 500, 1000, +}) + +MIN_WIN_COUNT = 8 +MAX_SYMBOL_COUNT = 30 + +# 单局最高赢奖封顶:1000 倍基础下注(最高 1000x 饰品) +MAX_WIN_MULTIPLIER = Decimal("1000") + + +# ============================================================ +# 游戏模式:互斥规则 +# - 底注(ante)开启时禁用购买免费旋转;购买免费旋转(buy_fs)时禁用底注效果 +# - 只有一个购买免费旋转档(无 super 模式) +# ============================================================ + +@dataclass(frozen=True) +class GameMode: + code: str + name: str + cost_multiplier: int # 实际扣费 = 基础下注 × cost_multiplier + base_bets: Tuple[int, ...] # 可选基础下注档位 + ante_boost: bool # 底注效果(SCATTER 概率提升) + guaranteed_free_spins: bool # 保证直接进入免费旋转 + can_buy_free_spins: bool # 是否允许购买免费旋转 + + def price_for(self, base_bet: int) -> Decimal: + if base_bet not in self.base_bets: + raise ValueError(f"模式 {self.code} 不支持下注档位 {base_bet}") + return Decimal(base_bet) * self.cost_multiplier + + +_NORMAL_BETS = (1, 3, 5, 10, 20, 30, 50, 100, 200, 300) +_BUY_FS_BETS = _NORMAL_BETS # 10 档,实际价格 50 ~ 15000 + +MODES = { + # 普通旋转:实际扣费 = 1 3 5 10 20 30 50 100 200 300 + "normal": GameMode( + code="normal", name="普通旋转", cost_multiplier=1, + base_bets=_NORMAL_BETS, + ante_boost=False, guaranteed_free_spins=False, can_buy_free_spins=True, + ), + # 底注:实际扣费 = 2 6 10 20 40 60 100 200 400 600;禁止购买免费旋转 + "ante": GameMode( + code="ante", name="底注", cost_multiplier=2, + base_bets=_NORMAL_BETS, + ante_boost=True, guaranteed_free_spins=False, can_buy_free_spins=False, + ), + # 购买免费旋转:实际扣费 = 50 150 250 500 1000 1500 2500 5000 10000 15000;无底注效果 + # 售价 50 倍(原 20 倍):局均赔付 = 售价 × RTP = 40 倍底注 → 后台一键重算会把配比压到 + # 25x/40x/60x 桶(FS 预算 22~57),每局才演得出 6~10 次消除 + 4~6 次炸弹累计; + # 20 倍时局均只有 16 倍,配比被迫压在 10x/16x 桶(预算 7~13),动画"钱不够"。 + "buy_fs": GameMode( + code="buy_fs", name="购买免费旋转", cost_multiplier=50, + base_bets=_BUY_FS_BETS, + ante_boost=False, guaranteed_free_spins=True, can_buy_free_spins=False, + ), +} + +DEFAULT_MODE = "normal" + + +# ============================================================ +# 饰品桶定义(DB 写入用):最高 1000x,对齐封顶规则 +# 设计原则:区间连续无缝覆盖 0~1370(封顶前理论值域); +# expect_mul 严格等于 (range_low + range_high) / 2; +# 低倍段密集(高频小奖)、高倍段递宽。 +# 注:顶桶 id=17 区间 630~1370(中点=期望=1000),但实际发放受封顶约束: +# 超过 1000 倍的部分不再给奖,一律按 1000 倍发放(引擎 MAX_WIN_MULTIPLIER 截断)。 +# ============================================================ +ORNAMENTS = [ + {'id': 1, 'name': '0x', 'expect_mul': 0.0, 'range_low': 0.00, 'range_high': 0.00}, + {'id': 2, 'name': '0.5x', 'expect_mul': 0.5, 'range_low': 0.01, 'range_high': 1.00}, + {'id': 3, 'name': '1.5x', 'expect_mul': 1.5, 'range_low': 1.00, 'range_high': 2.00}, + {'id': 4, 'name': '2.5x', 'expect_mul': 2.5, 'range_low': 2.00, 'range_high': 3.00}, + {'id': 5, 'name': '4x', 'expect_mul': 4.0, 'range_low': 3.00, 'range_high': 5.00}, + {'id': 6, 'name': '6.5x', 'expect_mul': 6.5, 'range_low': 5.00, 'range_high': 8.00}, + {'id': 7, 'name': '10x', 'expect_mul': 10.0, 'range_low': 8.00, 'range_high': 12.00}, + {'id': 8, 'name': '16x', 'expect_mul': 16.0, 'range_low': 12.00, 'range_high': 20.00}, + {'id': 9, 'name': '25x', 'expect_mul': 25.0, 'range_low': 20.00, 'range_high': 30.00}, + {'id': 10, 'name': '40x', 'expect_mul': 40.0, 'range_low': 30.00, 'range_high': 50.00}, + {'id': 11, 'name': '60x', 'expect_mul': 60.0, 'range_low': 50.00, 'range_high': 70.00}, + {'id': 12, 'name': '100x', 'expect_mul': 100.0, 'range_low': 70.00, 'range_high': 130.00}, + {'id': 13, 'name': '150x', 'expect_mul': 150.0, 'range_low': 130.00, 'range_high': 170.00}, + {'id': 14, 'name': '200x', 'expect_mul': 200.0, 'range_low': 170.00, 'range_high': 230.00}, + {'id': 15, 'name': '300x', 'expect_mul': 300.0, 'range_low': 230.00, 'range_high': 370.00}, + {'id': 16, 'name': '500x', 'expect_mul': 500.0, 'range_low': 370.00, 'range_high': 630.00}, + {'id': 17, 'name': '1000x', 'expect_mul': 1000.0, 'range_low': 630.00, 'range_high': 1370.00}, +] + + +# ============================================================ +# 回合生成器参数 +# ============================================================ +GRID_ROWS = 5 +GRID_COLS = 6 +GRID_SIZE = GRID_ROWS * GRID_COLS # 30 + +# 短码映射(网格/步骤 JSON 存储用,对齐 sweet 风格) +SYMBOL_SHORT = { + 'CROWN': 'A', 'HOURGLASS': 'B', 'RING': 'C', 'CHALICE': 'D', 'RED_GEM': 'E', + 'PURPLE_GEM': 'F', 'YELLOW_GEM': 'G', 'GREEN_GEM': 'H', 'BLUE_GEM': 'I', +} +SHORT_TO_SYMBOL = {v: k for k, v in SYMBOL_SHORT.items()} +SCATTER_SHORT = 'S' +BOMB_SHORT = 'M' + +# 普通符号随机权重(赔率越高权重越低) +SYMBOL_WEIGHTS = {'A': 5, 'B': 6, 'C': 7, 'D': 8, 'E': 9, 'F': 10, 'G': 10, 'H': 10, 'I': 12} + +# 炸弹(乘数符号)参数 +BOMB_CELL_PROBABILITY = 0.04 # 每格出现炸弹概率 ≈1个/屏 +BOMB_VALUES = sorted(MULTIPLIER_VALUES) +BOMB_WEIGHT_POWER = 0.5 # 值越大权重越低: w = 1/v^power + +# 炸弹品质(按乘数值分 5 档,对应绿/蓝/紫/红/彩虹 5 种球体) +BOMB_QUALITIES = [ + # (quality_code, 名称, 等级, 包含乘数值) + ('GREEN', '绿色炸弹', 1, (2, 3, 4, 5, 6)), + ('BLUE', '蓝色炸弹', 2, (8, 10, 12, 15)), + ('PURPLE', '紫色炸弹', 3, (20, 25, 50)), + ('RED', '红色炸弹', 4, (100, 250)), + ('RAINBOW', '彩虹炸弹', 5, (500, 1000)), +] +# value -> quality_code 快速查表 +BOMB_VALUE_QUALITY = {v: code for code, _, _, vals in BOMB_QUALITIES for v in vals} + + +def bomb_quality(value: int) -> str: + """炸弹乘数值 → 品质编码(GREEN/BLUE/PURPLE/RED/RAINBOW)""" + return BOMB_VALUE_QUALITY[value] + +# 免费旋转触发/散布(底注概率翻倍,互斥规则:buy_fs 保证触发) +BASE_FS_TRIGGER_PROB = {'normal': 0.005, 'ante': 0.010} +BASE_NEARMISS_SCATTER = { + 'normal': {0: 0.88, 1: 0.08, 2: 0.03, 3: 0.01}, + 'ante': {0: 0.74, 1: 0.16, 2: 0.07, 3: 0.03}, +} +FS_TRIGGER_SCATTER = {4: 0.80, 5: 0.15, 6: 0.05} # 触发盘落 S 数 +FREESPIN_SCATTER = { # FS 每旋落 S 数(≥3 → +5) + 'default': {0: 0.70, 1: 0.15, 2: 0.07, 3: 0.05, 4: 0.025, 5: 0.005}, + 'buy_fs': {0: 0.78, 1: 0.13, 2: 0.06, 3: 0.02, 4: 0.008, 5: 0.002}, +} +MAX_FREE_SPINS = 30 # 重触发总次数上限 + +# ============================================================ +# 免费旋转"每旋保底消除"(仅 buy_fs) +# 动机:15 旋原来 86.8% 的旋完全空转(45% 强制空转 + 自然填充 82.9% 凑不出 8 连) +# 做法:本旋没消成时按概率补一组最小档消除(8 连低价值符号 = 0.25 倍底注),消完再按概率续链 +# 预算:补桶时按桶目标反推本局 FS 预算 —— 按剩余旋数自平衡,花完即停 +# 注:normal/ante 的自然触发盘不走这套(仍按原 45% 空转 + 自然),避免打穿 0x~2.5x 低倍桶 +# ============================================================ +FS_SPIN_WIN_PROB = 0.60 # 自然阶段:每旋"至少消一波"的概率 +# 有预算时(buy_fs / 高倍桶补桶)不再按"剩余预算÷剩余旋数"算每旋概率 —— 扣预算扣的是**已被累计 +# 倍数放大**的赢额,累计器一涨几旋就把预算烧穿 → 后半局 p=0 一直空转到结束(实测买 FS 有 88% 的局 +# 出现 ≥4 旋连空、51.7% 的局尾巴一直空转)。改成:一局能买几次消除由预算反推 +# (k ≈ √(预算 / FS_GUARANTEE_COST)),每次消除后**重抽下一次的位置**(间隔随机,见 +# `olympus_simulator._fs_next_target`)—— 整局消除次数不变,但不再"每 4 旋必消"。 +FS_GUARANTEE_COST = 0.45 # 每多一次"保底消除+炸弹累计"的边际赔付(实测 ≈0.45×k) +# 随机间隔的兜底:连续空转越久,越可能被强制补一次保底消除(**软**上限,不是硬闸)—— +# 硬上限(到第 N 旋必消)会留下新的固定天花板(实测硬顶 6 时有 42% 的局"最长连空正好 6"), +# 所以按概率递增:dry=5/6 时 0.5/0.85,dry≥8 必补。配额提前用完时靠它兜住长空尾。 +# 实测(box21 整池):最长连空 2~8 旋散开、单值最高 42%(等距版是 87% 都卡在 4),≥7 旋占 2.5%。 +FS_DRY_FORCE_PROB = {5: 0.50, 6: 0.85} +FS_DRY_FORCE_HARD = 8 # 到这个旋数无条件补(概率表兜不到时) +# 连消概率:越低,同一份预算能覆盖的"有消除的旋"越多(0.45 时预算集中在少数旋上, +# 一局只剩 1~2 旋有消除;0.10 时同样的钱能铺到 4~10 旋) +FS_CASCADE_CONTINUE_PROB = 0.10 +FS_CASCADE_MAX = 5 # 单旋最大消除波数 +FS_GUARANTEE_COMBOS = ( # 保底消除组合 (符号, 个数, 赔付倍数@1倍底注),最小档为主 + ('I', 8, 0.25), ('I', 8, 0.25), ('H', 8, 0.40), ('G', 8, 0.50), +) + +# 免费旋转里的炸弹(buy_fs):**每一旋都可能出现、每屏最多 1 颗**。 +# ① 每一旋都可能出现:前面几旋的消除盘面上也看得见炸弹,倍数从第 1~2 旋就开始涨; +# ② 每屏最多 1 颗:一屏堆 2~4 颗会"一次消除好几颗一起爆",看不出"消一次爆一颗"的节奏; +# ③ 炸弹**有大有小**,但按该局的档位配:低倍档(< FS_BIG_BOMB_TARGET)用 2~8 的小球 +# (累计倍数作用到每一次消除:0.4×2 + 0.5×4 = 2.8 这种滚雪球,大球一合并后面每次消除 +# 都被放大几十倍,低倍档的预算立刻见底),高倍档(≥100x)用全量 2~1000 —— 所以 +# "大炸弹"照样存在,只是它出现的那些局自然落在高倍桶。 +FS_SCREEN_BOMB_PROB = 0.08 +FS_SMALL_BOMB_MAX = 8 +FS_BIG_BOMB_TARGET = 100 +FS_MAX_BOMBS_PER_SPIN = 1 + +# "只能靠免费旋转赔出来"的目标倍数下限:normal/ante 的基础旋不出炸弹,最多只能凑到 +# A12(50) + B10(10) ≈ 60x(自然多组 75x 已属极罕见),所以目标 ≥80x 的桶(100x/150x/200x) +# 只能生成"自然触发 FS"的回合来装 —— 这也是这两个模式里玩家能进 FS 的现实来源。 +FS_ONLY_TARGET_MUL = 80.0 + +# 目标 RTP 与各模式可用饰品桶 +RTP = 0.8 +MODE_ORNAMENT_IDS = { + 'normal': list(range(1, 14)), # 0x ~ 150x + 'ante': list(range(1, 15)), # 0x ~ 200x + 'buy_fs': list(range(7, 18)), # 10x ~ 1000x(保证进 FS;原 5 起=4x 起,低倍桶预算装不下 FS 动画) +} +# 桶"实际发出期望"的**回退值**:桶 17 展示 1000x(区间 630~1370),但单局封顶 1000 → +# 真正发出去的是 min(原始分, 1000): +# · 理论值(假设原始分在 630~1370 均匀分布):E[min(x,1000)] = 907.5 +# · 实测值(buy_fs 10 箱 × 桶17 全部回合 = 200 个):均值 834.6、中位 821.3、27.5% 触顶 1000 +# —— 生成端种的大奖目标集中在 1000 附近、不是均匀分布,所以实测低于理论值。 +# 注意:配比(calcRounds)与后台「目标RTP」现在优先用**该箱池内 AVG(actual_mul)**(见 +# _recalc_boxes.load_pool_expect / Java loadPoolExpect),这个覆盖值只在"池子里一条回合都没有" +# 时兜底 —— 它比名义 1000 诚实(高估约 0.5~1.6 个百分点)。 +PAYOUT_EXPECT_OVERRIDE = {17: 835.0} + +_ORN_EXPECT = {o['id']: float(o['expect_mul']) for o in ORNAMENTS} + + +def payout_expect(orn_id: int) -> float: + """桶的"实际发出期望"回退值:受封顶影响的桶用实测覆盖值,其余用名义 expect_mul。 + + 用于:池子里还没有该桶回合时的兜底、以及模拟/报表里的名义口径对比。 + **抽回合时的 payoutCap 过滤仍用名义值**(Java OlympusRoundPicker 读 DB 的 expect_mul, + 偏保守,保留安全余量)。""" + return PAYOUT_EXPECT_OVERRIDE.get(orn_id, _ORN_EXPECT.get(orn_id, 0.0)) + + +# ============================================================ +# 多身份爆率(audience):每种身份一套独立的饰品桶数量分布,实现差异化 RTP。 +# 回合库存池(round/step)完全共享,仅“抽哪个桶”的概率按身份区分。 +# 与现有宝箱 tt_user 对齐:user_type 01主播 / 02普通 / 03机器人;is_blank_account=1 白号。 +# 身份解析优先级(Java 开局):白号最高优先,压过 user_type: +# is_blank_account==1 → white;user_type=01 → anchor;03 → robot;其余 → common。 +# 白号池(white):独立 count/used,抽取只递减自己的 used、独立补桶, +# 永不消耗 common/anchor/robot 库存;白号 bet/win 不计水位、赢奖不进真实经济。 +# 机器人暂不细分幸运/非幸运档(后续视 2v2 需求再扩展 robot_unlucky)。 +# ============================================================ +AUDIENCE_COMMON = 'common' # 普通用户(对齐 user_type=02 及招商/卡商/超管等其他) +AUDIENCE_ANCHOR = 'anchor' # 主播(user_type=01) +AUDIENCE_ROBOT = 'robot' # 机器人(user_type=03) +AUDIENCE_WHITE = 'white' # 白号(is_blank_account=1,压过 user_type) +AUDIENCES = (AUDIENCE_COMMON, AUDIENCE_ANCHOR, AUDIENCE_ROBOT, AUDIENCE_WHITE) +DEFAULT_AUDIENCE = AUDIENCE_COMMON + + +# ============================================================ +# 爆奖动画(小/中/大 3 档):**只在免费旋转里判档**,按"单旋赢额"独立判档, +# 达标即在该旋末帧盖章,同档一局可多次;normal/ante 的基础旋一律 null(不播大奖动画)。 +# 两种情况两套阈值(单位:单旋赢额 ÷ 基础下注): +# ① 自然免费旋转(normal/ante 摇出 4+ S 进入):5 / 25 / 100 +# 两个模式的 FS 预算与旋数完全一致 → 共用一套;实测 FS 内约 15% 的旋 ≥5、 +# 约 4% 的旋 ≥25、约 1.7% 的旋 ≥100(约 1/4 的 FS 局能见到"大") +# ② 购买免费旋转(buy_fs):20 / 60 / 200 +# 买 FS 的赢额分散在 15+ 旋里,单旋阈值整体下调;实测约 3.5% 的旋 ≥20、 +# 约 0.9% 的旋 ≥60、约 0.1% 的旋 ≥200(约 2% 的买 FS 局能见到"大") +# 编码 small < middle < big:前端按这三个值播小/中/大动画,null 表示不播 +# ============================================================ +BIGWIN_LEVELS = ('small', 'middle', 'big') +BIGWIN_TIERS = { + 'normal': (5, 25, 100), + 'ante': (5, 25, 100), # 与 normal 同一口径:都是"自然触发的 FS",整局规模相同 + 'buy_fs': (20, 60, 200), +} + + +def bigwin_level(mode_code: str, spin_mul: float): + """单旋赢额(基础注倍数)→ 爆奖档位编码 small/middle/big;未达最低档返回 None""" + tiers = BIGWIN_TIERS[mode_code] + level = None + for name, threshold in zip(BIGWIN_LEVELS, tiers): + if spin_mul >= threshold: + level = name + return level diff --git a/olympus-py/olympus/engine.py b/olympus-py/olympus/engine.py new file mode 100644 index 0000000..e9f8ce8 --- /dev/null +++ b/olympus-py/olympus/engine.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +from collections import Counter +from decimal import Decimal +from typing import Iterable, Mapping, Sequence + +from .config import ( + DEFAULT_MODE, + MAX_SYMBOL_COUNT, + MAX_WIN_MULTIPLIER, + MIN_WIN_COUNT, + MODES, + MULTIPLIER_VALUES, + SYMBOL_RULES, + GameMode, +) +from .models import Bomb, CascadeResult, Elimination, SessionState, SpinResult + + +class RuleViolation(ValueError): + pass + + +class OlympusEngine: + def __init__( + self, + bet: Decimal | int | str = Decimal("1"), + mode: str = DEFAULT_MODE, + ) -> None: + if mode not in MODES: + raise RuleViolation(f"未知模式: {mode},可选: {sorted(MODES)}") + self.mode: GameMode = MODES[mode] + self.bet = Decimal(str(bet)) + if self.bet <= 0: + raise RuleViolation("bet 必须大于 0") + # 档位校验:整数档位必须在模式允许列表内(非整数 bet 仅用于单测/模拟,不校验) + if self.bet == self.bet.to_integral_value() and int(self.bet) not in self.mode.base_bets: + raise RuleViolation( + f"模式 {self.mode.code} 不支持下注档位 {int(self.bet)},可选: {self.mode.base_bets}" + ) + self.cost = self.bet * self.mode.cost_multiplier # 实际扣费 + self.max_win = self.bet * MAX_WIN_MULTIPLIER # 单局封顶 1000x 基础下注 + self.state = SessionState() + self._cascades: list[CascadeResult] | None = None + self._symbol_counts: Counter[str] = Counter() + self._spin_base_win = Decimal("0") + self._bombs: dict[str, Bomb] = {} + self._is_free_spin = False + + def buy_free_spins_allowed(self) -> bool: + """当前模式是否允许购买免费旋转(底注与购买互斥)。""" + return self.mode.can_buy_free_spins + + def ante_boost_active(self) -> bool: + """当前模式是否启用底注效果(购买免费旋转模式下强制关闭)。""" + return self.mode.ante_boost + + def begin_spin(self, is_free_spin: bool = False) -> int: + """is_free_spin=True 表示免费旋转:炸弹进入累计器;基础旋转炸弹仅对本旋生效。""" + if self._cascades is not None: + raise RuleViolation("当前回合尚未结束") + self._cascades = [] + self._symbol_counts.clear() + self._spin_base_win = Decimal("0") + self._bombs.clear() + self._is_free_spin = is_free_spin + return self.state.next_spin_index + + def record_cascade( + self, + symbols: Mapping[str, Sequence[int]] | None = None, + bombs: Iterable[Bomb] = (), + ) -> CascadeResult: + if self._cascades is None: + raise RuleViolation("请先调用 begin_spin") + + eliminations = [] + cascade_base_win = Decimal("0") + for symbol, raw_positions in (symbols or {}).items(): + if symbol not in SYMBOL_RULES: + raise RuleViolation(f"未知符号: {symbol}") + positions = tuple(raw_positions) + count = len(positions) + if count < MIN_WIN_COUNT or count > MAX_SYMBOL_COUNT: + raise RuleViolation(f"{symbol} 消除数量必须在 {MIN_WIN_COUNT}-{MAX_SYMBOL_COUNT} 之间") + if len(set(positions)) != count: + raise RuleViolation(f"{symbol} 的消除位置不能重复") + + payout_multiplier = SYMBOL_RULES[symbol].payout_for(count) + elimination = Elimination( + symbol=symbol, + count=count, + positions=positions, + payout_multiplier=payout_multiplier, + ) + eliminations.append(elimination) + cascade_base_win += payout_multiplier * self.bet + self._symbol_counts[symbol] += count + + current_bombs = [] + for bomb in bombs: + if bomb.value not in MULTIPLIER_VALUES: + raise RuleViolation(f"不支持的炸弹倍数: {bomb.value}") + existing = self._bombs.get(bomb.bomb_id) + if existing is not None and existing != bomb: + raise RuleViolation(f"炸弹 {bomb.bomb_id} 的数据前后不一致") + if existing is None: + self._bombs[bomb.bomb_id] = bomb + current_bombs.append(bomb) + + self._spin_base_win += cascade_base_win + result = CascadeResult( + cascade_index=len(self._cascades) + 1, + eliminations=tuple(eliminations), + bombs=tuple(current_bombs), + base_win=cascade_base_win, + accumulated_symbols=dict(self._symbol_counts), + ) + self._cascades.append(result) + return result + + def end_spin(self) -> SpinResult: + if self._cascades is None: + raise RuleViolation("请先调用 begin_spin") + + multiplier_before = self.state.accumulated_multiplier + has_win = self._spin_base_win > 0 + bombs_added = sum(bomb.value for bomb in self._bombs.values()) if has_win else 0 + if self._is_free_spin: + # 免费旋转:炸弹并入累计器,赢额乘“累计+本旋新炸弹” + multiplier_after = multiplier_before + bombs_added + effective_multiplier = max(1, multiplier_after) if has_win else 0 + else: + # 基础旋转:无累计器,炸弹仅乘本旋赢额,不留存 + multiplier_after = multiplier_before + effective_multiplier = max(1, bombs_added) if has_win else 0 + total_win = self._spin_base_win * effective_multiplier + + # 封顶:整局累计赢奖不超过 1000 倍基础下注(最高 1000x 饰品) + remaining = self.max_win - self.state.total_win + if remaining < 0: + remaining = Decimal("0") + capped = total_win > remaining + if capped: + total_win = remaining + + result = SpinResult( + spin_index=self.state.next_spin_index, + cascades=tuple(self._cascades), + base_win=self._spin_base_win, + multiplier_before=multiplier_before, + bombs_added=bombs_added, + multiplier_after=multiplier_after, + effective_multiplier=effective_multiplier, + total_win=total_win, + accumulated_symbols=dict(self._symbol_counts), + win_capped=capped, + ) + self.state.spins.append(result) + self.state.next_spin_index += 1 + self.state.accumulated_multiplier = multiplier_after + self.state.total_win += total_win + self._cascades = None + return result + + def reset_session(self) -> None: + if self._cascades is not None: + raise RuleViolation("当前回合尚未结束") + self.state.reset() diff --git a/olympus-py/olympus/models.py b/olympus-py/olympus/models.py new file mode 100644 index 0000000..ac55c16 --- /dev/null +++ b/olympus-py/olympus/models.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from decimal import Decimal +from typing import Dict, List, Tuple + + +@dataclass(frozen=True) +class Bomb: + bomb_id: str + value: int + position: int | None = None + + +@dataclass(frozen=True) +class Elimination: + symbol: str + count: int + positions: Tuple[int, ...] + payout_multiplier: Decimal + + +@dataclass(frozen=True) +class CascadeResult: + cascade_index: int + eliminations: Tuple[Elimination, ...] + bombs: Tuple[Bomb, ...] + base_win: Decimal + accumulated_symbols: Dict[str, int] + + +@dataclass(frozen=True) +class SpinResult: + spin_index: int + cascades: Tuple[CascadeResult, ...] + base_win: Decimal + multiplier_before: int + bombs_added: int + multiplier_after: int + effective_multiplier: int + total_win: Decimal + accumulated_symbols: Dict[str, int] + win_capped: bool = False # 本回合赢奖因 1000x 封顶被截断 + + +@dataclass +class SessionState: + next_spin_index: int = 1 + accumulated_multiplier: int = 0 + total_win: Decimal = Decimal("0") + spins: List[SpinResult] = field(default_factory=list) + + def reset(self) -> None: + self.next_spin_index = 1 + self.accumulated_multiplier = 0 + self.total_win = Decimal("0") + self.spins.clear() diff --git a/olympus-py/olympus_db.py b/olympus-py/olympus_db.py new file mode 100644 index 0000000..7426f29 --- /dev/null +++ b/olympus-py/olympus_db.py @@ -0,0 +1,229 @@ +""" +Olympus 数据库操作模块 + +用法: + python olympus_db.py # 建表 + 写入饰品桶种子数据 +连接参数可用环境变量覆盖: OLY_DB_HOST / OLY_DB_PORT / OLY_DB_USER / OLY_DB_PASSWORD / OLY_DB_NAME +默认对齐本地开发库 (127.0.0.1:23306 changzhou,见 docker/compose/docker-compose.yml)。 +""" +import json +import os +import time +from contextlib import contextmanager +from pathlib import Path + +import pymysql + +from olympus.config import ORNAMENTS + +DB_CONFIG = { + 'host': os.environ.get('OLY_DB_HOST', '127.0.0.1'), + 'port': int(os.environ.get('OLY_DB_PORT', '23306')), + 'user': os.environ.get('OLY_DB_USER', 'changzhou'), + 'password': os.environ.get('OLY_DB_PASSWORD', 'iTSJSPPZM3LSGAPC'), + 'database': os.environ.get('OLY_DB_NAME', 'changzhou'), + 'charset': 'utf8mb4', + 'local_infile': True, +} + +SCHEMA_FILE = Path(__file__).resolve().parent / 'schema.sql' + + +@contextmanager +def get_conn(): + conn = pymysql.connect(**DB_CONFIG) + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + finally: + conn.close() + + +# ============================================================ +# 建表(执行 schema.sql 中所有语句,注释行自动跳过) +# ============================================================ + +def _load_schema_statements(): + text = SCHEMA_FILE.read_text(encoding='utf-8') + lines = [ln for ln in text.splitlines() if not ln.strip().startswith('--')] + statements = [s.strip() for s in '\n'.join(lines).split(';') if s.strip()] + return statements + + +def create_tables(): + """建表 + 写入饰品桶种子数据(幂等,可重复执行)""" + stmts = _load_schema_statements() + with get_conn() as conn: + cur = conn.cursor() + for sql in stmts: + cur.execute(sql) + print(f'OK: executed {len(stmts)} statements (tables + ornament seed).') + + +def verify_tables(): + """校验所有 olympus 表已存在并输出行数""" + with get_conn() as conn: + cur = conn.cursor() + cur.execute("SHOW TABLES LIKE 'game_olympus_%'") + tables = [row[0] for row in cur.fetchall()] + for t in sorted(tables): + cur.execute(f"SELECT COUNT(*) FROM {t}") + print(f'{t}: {cur.fetchone()[0]} rows') + return tables + + +# ============================================================ +# Ornament 操作 +# ============================================================ + +def insert_ornaments(): + """按 config.ORNAMENTS 覆盖写入饰品桶(与 schema.sql 种子一致)""" + with get_conn() as conn: + cur = conn.cursor() + for o in ORNAMENTS: + cur.execute(""" + INSERT INTO game_olympus_ornament (id, game_type, expect_mul, name, range_low, range_high) + VALUES (%s,'OLYMPUS',%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE expect_mul=VALUES(expect_mul), name=VALUES(name), + range_low=VALUES(range_low), range_high=VALUES(range_high) + """, (o['id'], o['expect_mul'], o['name'], o['range_low'], o['range_high'])) + + +# ============================================================ +# Box 操作 +# ============================================================ + +def insert_box(box): + with get_conn() as conn: + cur = conn.cursor() + cur.execute(""" + INSERT INTO game_olympus_box (box_id, game_type, box_type, bet, name, price, + total_ornaments, rtp, sort, status) + VALUES (%s,'OLYMPUS',%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE box_type=VALUES(box_type), + bet=VALUES(bet), name=VALUES(name), + price=VALUES(price), total_ornaments=VALUES(total_ornaments), + rtp=VALUES(rtp) + """, (box['box_id'], box['box_type'], box['bet'], box['name'], + box['price'], box['total_ornaments'], box['rtp'], + box.get('sort', 0), box.get('status', '1'))) + + +def query_boxes(): + with get_conn() as conn: + cur = conn.cursor(pymysql.cursors.DictCursor) + cur.execute("SELECT * FROM game_olympus_box WHERE status='1' ORDER BY box_id") + return cur.fetchall() + + +# ============================================================ +# Round / Step 批量写入(单连接清空+写入,死锁自动重试) +# ============================================================ + +def get_max_round_id(): + with get_conn() as conn: + cur = conn.cursor() + cur.execute("SELECT COALESCE(MAX(round_id), 0) FROM game_olympus_round") + return cur.fetchone()[0] + + +def get_max_step_id(): + with get_conn() as conn: + cur = conn.cursor() + cur.execute("SELECT COALESCE(MAX(id), 0) FROM game_olympus_step") + return cur.fetchone()[0] + + +def _j(v, default='[]'): + if v is None: + return default + return v if isinstance(v, str) else json.dumps(v, ensure_ascii=False) + + +def write_box_distribution(box_id, counts, audience='common'): + """单连接:按身份清空并写入饰品桶分布(只动该 audience,不影响其他身份)""" + for attempt in range(5): + try: + with get_conn() as conn: + cur = conn.cursor() + cur.execute( + "DELETE FROM game_olympus_box_ornament WHERE box_id=%s AND audience=%s", + (box_id, audience)) + if counts: + sql = ("INSERT INTO game_olympus_box_ornament" + " (box_id, ornament_id, audience, count, used)" + " VALUES (%s,%s,%s,%s,0)") + rows = [(box_id, oid, audience, cnt) + for oid, cnt in sorted(counts.items()) if cnt > 0] + cur.executemany(sql, rows) + return + except Exception as e: + if 'Deadlock' in str(e) and attempt < 4: + time.sleep(0.5 * (attempt + 1)) + continue + raise + + +def write_box_distributions(box_id, counts_by_audience): + """一次写入多套身份分布。counts_by_audience: {audience: {ornament_id: count}}""" + for audience, counts in counts_by_audience.items(): + write_box_distribution(box_id, counts, audience) + + +def write_rounds_and_steps(box_id, rounds_data, steps_data): + """单连接:清空旧回合+步骤,批量写入新数据""" + for attempt in range(5): + try: + with get_conn() as conn: + cur = conn.cursor() + cur.execute("""DELETE FROM game_olympus_step WHERE round_id IN + (SELECT round_id FROM game_olympus_round WHERE box_id=%s)""", (box_id,)) + cur.execute("DELETE FROM game_olympus_round WHERE box_id=%s", (box_id,)) + if rounds_data: + sql_r = """INSERT INTO game_olympus_round + (round_id, box_id, ornament_id, actual_mul, free_spin_count, total_cascades) + VALUES (%s,%s,%s,%s,%s,%s)""" + rows_r = [(rd['round_id'], rd['box_id'], rd['ornament_id'], + rd['actual_mul'], rd['free_spin_count'], rd['total_cascades']) + for rd in rounds_data] + cur.executemany(sql_r, rows_r) + if steps_data: + sql_s = """INSERT INTO game_olympus_step + (id, round_id, free_spin_id, cascade_seq, grid_state, + multiplier_detail, symbol_links, accumulated_symbols, spin_winners, + removed_positions, drop_data, fill_data, + scatter_trigger, retrigger_info, base_score, win_amount, + accumulator_before, accumulator_after, spin_win, win_capped, + bigwin_level, is_final) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""" + rows_s = [(s['id'], s['round_id'], s['free_spin_id'], s['cascade_seq'], + _j(s['grid_state']), + _j(s.get('multiplier_detail')), + _j(s.get('symbol_links')), + _j(s.get('accumulated_symbols'), default='{}'), + _j(s.get('spin_winners')), + _j(s.get('removed_positions')), + _j(s.get('drop_data')), + _j(s.get('fill_data')), + _j(s.get('scatter_trigger'), default=None), + _j(s.get('retrigger_info'), default=None), + s['base_score'], s['win_amount'], + s['accumulator_before'], s['accumulator_after'], + s['spin_win'], s.get('win_capped', 0), + s.get('bigwin_level'), s['is_final']) + for s in steps_data] + cur.executemany(sql_s, rows_s) + return + except Exception as e: + if 'Deadlock' in str(e) and attempt < 4: + time.sleep(0.5 * (attempt + 1)) + continue + raise + + +if __name__ == '__main__': + create_tables() + verify_tables() diff --git a/olympus-py/olympus_simulator.py b/olympus-py/olympus_simulator.py new file mode 100644 index 0000000..1da3985 --- /dev/null +++ b/olympus-py/olympus_simulator.py @@ -0,0 +1,805 @@ +""" +Olympus 回合生成器核心 — 正向生成 + +流程: 生成 cascade → 自然累计 → 总分落桶 +核心规则(与 olympus/engine.py 对齐): + 1. 一旋转内多次消除,中间步只记基础分,末帧统一引爆炸弹 + 2. 炸弹倍数跨旋转累计(accumulator 贯穿整个回合:基础旋转 + 免费旋转) + 3. 每步 accumulated_symbols 记录本旋转累计消除符号 + 4. 落桶用理论总分(最高 1370),发放封顶 1000(末步截断 + win_capped) +""" +import json +import random +from collections import Counter + +from olympus.config import ( + SYMBOL_RULES, SHORT_TO_SYMBOL, SCATTER_SHORT, BOMB_SHORT, + SYMBOL_WEIGHTS, BOMB_CELL_PROBABILITY, BOMB_VALUES, BOMB_WEIGHT_POWER, + GRID_ROWS, GRID_COLS, GRID_SIZE, MIN_WIN_COUNT, + SCATTER_PAYOUTS, SCATTER_FREE_SPIN_TRIGGER, SCATTER_RETRIGGER_TRIGGER, + FREE_SPINS_AWARDED, FREE_SPIN_RETRIGGER_COUNT, MAX_FREE_SPINS, + BASE_FS_TRIGGER_PROB, BASE_NEARMISS_SCATTER, FS_TRIGGER_SCATTER, FREESPIN_SCATTER, + FS_SPIN_WIN_PROB, FS_CASCADE_CONTINUE_PROB, FS_CASCADE_MAX, FS_GUARANTEE_COMBOS, + FS_GUARANTEE_COST, FS_DRY_FORCE_PROB, FS_DRY_FORCE_HARD, + FS_SMALL_BOMB_MAX, FS_BIG_BOMB_TARGET, FS_SCREEN_BOMB_PROB, FS_MAX_BOMBS_PER_SPIN, + FS_ONLY_TARGET_MUL, + ORNAMENTS, MAX_WIN_MULTIPLIER, bomb_quality, bigwin_level, +) + +REGULAR_SHORTS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] +MAX_WIN = float(MAX_WIN_MULTIPLIER) # 发放封顶 1000 +BUCKET_CEIL = max(o['range_high'] for o in ORNAMENTS) # 落桶理论上限 1370 + +# 保底消除预算(1 倍底注口径):最小档 0.25,连消后期望 1/(1-p) 波 +FS_FLOOR_UNIT = min(c[2] for c in FS_GUARANTEE_COMBOS) +FS_FLOOR_SPEND = FS_FLOOR_UNIT / (1.0 - FS_CASCADE_CONTINUE_PROB) # ≈0.45/旋 +FS_FLOOR_BUDGET_CAP = FREE_SPINS_AWARDED * FS_FLOOR_SPEND # ≈6.8/局 + + +# ==================================================================== +# 工具 +# ==================================================================== + +def _payout(short, cnt): + return float(SYMBOL_RULES[SHORT_TO_SYMBOL[short]].payout_for(cnt)) + + +def _weighted_sym(exclude=()): + syms = [s for s in REGULAR_SHORTS if s not in exclude] + wts = [SYMBOL_WEIGHTS[s] for s in syms] + r = random.uniform(0, sum(wts)) + a = 0.0 + for s, w in zip(syms, wts): + a += w + if r <= a: + return s + return syms[-1] + + +def _weighted_choice(dist): + r = random.random() + cum, last = 0.0, None + for k, p in dist.items(): + last = k + cum += p + if r <= cum: + return k + return last + + +def _bucket(score): + """理论总分落桶(score 可到 1370+,超出归顶桶)""" + if score <= 0: + return 1 + if score > BUCKET_CEIL: + return ORNAMENTS[-1]['id'] + for o in ORNAMENTS: + if o['range_low'] <= score <= o['range_high']: + return o['id'] + for o in ORNAMENTS[1:]: + if score <= o['range_high']: + return o['id'] + return ORNAMENTS[-1]['id'] + + +def _gen_bomb_value(max_val=None): + opts = [v for v in BOMB_VALUES if max_val is None or v <= max_val] + wts = [1.0 / (v ** BOMB_WEIGHT_POWER) for v in opts] + r = random.uniform(0, sum(wts)) + a = 0.0 + for v, w in zip(opts, wts): + a += w + if r <= a: + return v + return opts[-1] + + +def _bomb_combos(target): + """列出 ≤3 颗炸弹、和最接近 target 的所有组合:(误差, 组合),按误差升序""" + out = [] + vals = BOMB_VALUES + for a in vals: + out.append((abs(a - target), [a])) + if a > target * 1.5: + break + for a in vals: + if a > target: + break + for b in vals: + s = a + b + if s > target * 1.5: + break + out.append((abs(s - target), [a, b])) + for a in vals: + if a > target: + break + for b in vals: + if a + b > target: + break + for c in vals: + s = a + b + c + if s > target * 1.5: + break + out.append((abs(s - target), [a, b, c])) + out.sort(key=lambda x: (x[0], len(x[1]))) + return out + + +def _best_bombs(target): + """确定性最优(只用于评估方案误差)""" + combos = _bomb_combos(target) + return combos[0][1] if combos else [2] + + +def _decompose_best(target): + """在"接近最优"的组合里随机挑一个,并强烈偏向单颗 —— 一屏一颗, + 玩家才看得出"消一次爆一颗、倍数一颗一颗涨";改前每次都返回同一套 + (比如某些目标永远是 [3] 或 [3,6]),看起来"消除必定带 3 和 6 倍炸弹"。""" + combos = _bomb_combos(target) + if not combos: + return [2] + best_err = combos[0][0] + pool = [c for e, c in combos if e <= best_err + max(0.5, best_err * 0.15)] + weights = [1.0 / (len(c) ** 3) for c in pool] # 1 颗 : 2 颗 : 3 颗 = 1 : 1/8 : 1/27 + return random.choices(pool, weights=weights)[0] + + +# ==================================================================== +# 网格 +# ==================================================================== + +def _empty_grid(): + return [[''] * GRID_COLS for _ in range(GRID_ROWS)] + + +def _count(grid): + c = Counter() + for row in grid: + for cell in row: + if cell: + c[cell] += 1 + return c + + +def _winners(grid): + w = {} + for sym, cnt in _count(grid).items(): + if sym in (BOMB_SHORT, SCATTER_SHORT, ''): + continue + if cnt >= MIN_WIN_COUNT: + pay = _payout(sym, cnt) + if pay > 0: + w[sym] = (cnt, pay) + return w + + +def _no_extra(grid, allowed): + """打散计划外的 ≥8 同符号组""" + backup = [s for s in REGULAR_SHORTS if s not in allowed] or ['I'] + for _ in range(50): + dirty = False + for sym, cnt in _count(grid).items(): + if sym in (BOMB_SHORT, SCATTER_SHORT, '') or sym in allowed: + continue + if cnt >= MIN_WIN_COUNT: + excess = cnt - (MIN_WIN_COUNT - 1) + for r in range(GRID_ROWS): + for c in range(GRID_COLS): + if grid[r][c] == sym and excess > 0: + grid[r][c] = random.choice(backup) + excess -= 1 + dirty = True + if not dirty: + break + + +def _fill(grid, mul_map, allow_bomb, track=False, exclude_syms=(), + bomb_prob=BOMB_CELL_PROBABILITY, bomb_max=None, max_bombs=None): + """填充空格。allow_bomb 时按 bomb_prob 概率放炸弹(bomb_max 限制炸弹值、max_bombs 限制每屏颗数)。 + track 返回填充数据(仅普通符号)""" + filled = [] if track else None + placed = len(mul_map) + for r in range(GRID_ROWS): + for c in range(GRID_COLS): + if grid[r][c] == '': + pos = r * GRID_COLS + c + if allow_bomb and random.random() < bomb_prob \ + and (max_bombs is None or placed < max_bombs): + grid[r][c] = BOMB_SHORT + mul_map[pos] = _gen_bomb_value(bomb_max) + placed += 1 + else: + sym = _weighted_sym(exclude_syms) + grid[r][c] = sym + if track: + filled.append({"pos": pos + 1, "symbol": sym}) + return filled + + +def _collapse(grid, remove, mul_map): + """消除指定位置(0-based),重力下落。炸弹/散布随位置移动,mul_map 同步重映射""" + rs = set(remove) + drops = [] + ng = _empty_grid() + pos_remap = {} + for c in range(GRID_COLS): + surviving = [] + for r in range(GRID_ROWS - 1, -1, -1): + p = r * GRID_COLS + c + if p not in rs: + surviving.append((p, grid[r][c])) + for i, (old_pos, sym) in enumerate(surviving): + new_r = GRID_ROWS - 1 - i + new_pos = new_r * GRID_COLS + c + ng[new_r][c] = sym + pos_remap[old_pos] = new_pos + if new_pos != old_pos: + drops.append({"from": old_pos + 1, "to": new_pos + 1, "symbol": sym}) + new_mul = {pos_remap[p]: v for p, v in mul_map.items() if p in pos_remap} + return ng, drops, new_mul + + +def _place_scatter(grid, count, protect=()): + cand = [(r, c) for r in range(GRID_ROWS) for c in range(GRID_COLS) + if grid[r][c] not in ({SCATTER_SHORT, BOMB_SHORT} | set(protect))] + for r, c in random.sample(cand, min(count, len(cand))): + grid[r][c] = SCATTER_SHORT + + +def _count_scatter(grid): + return sum(1 for row in grid for cell in row if cell == SCATTER_SHORT) + + +def _force_small_win(grid, prefer=()): + """就地补一组最小档消除(默认 8 连低价值符号),不覆盖已有 S/M 格。 + + prefer = 这一波"刚落下 / 刚补位"的格子(0 基)。保底消除优先种在这些格子上: + 它们本来就有 drop/fill 记录,前端按动画数据落地就能与 grid_state 完全一致; + 种在**没移动过**的格子上则没有任何动画记录,前端只能靠 grid_state 兜底。""" + sym, cnt, _ = random.choice(FS_GUARANTEE_COMBOS) + free = [p for p in range(GRID_SIZE) + if grid[p // GRID_COLS][p % GRID_COLS] not in (SCATTER_SHORT, BOMB_SHORT)] + if len(free) < cnt: + return + fresh = [p for p in prefer if p in set(free)] + if len(fresh) >= cnt: + picks = random.sample(fresh, cnt) + else: + rest = [p for p in free if p not in set(fresh)] + picks = fresh + random.sample(rest, cnt - len(fresh)) + for p in picks: + grid[p // GRID_COLS][p % GRID_COLS] = sym + _no_extra(grid, {sym}) + + +def _scatter_payout(count): + if count >= SCATTER_FREE_SPIN_TRIGGER: + return float(SCATTER_PAYOUTS.get(min(count, 6), 0)) + return 0.0 + + +# ==================================================================== +# 强制中奖(bias 补桶用) +# ==================================================================== + +# (基础分, 组合) 按基础分降序 +_BASE_OPTIONS = [ + (60.0, [('A', 12), ('B', 10)]), + (50.0, [('A', 12)]), + (35.0, [('A', 10), ('B', 10)]), + (25.0, [('A', 10)]), + (12.5, [('A', 8), ('B', 8)]), + (10.0, [('A', 8)]), + (5.0, [('C', 10)]), + (3.5, [('C', 8), ('D', 8)]), + (2.5, [('B', 8)]), + (2.0, [('C', 8)]), + (1.5, [('D', 8)]), + (1.0, [('E', 8)]), + (0.8, [('F', 8)]), + (0.5, [('G', 8)]), + (0.4, [('H', 8)]), + (0.25, [('I', 8)]), +] + + +def _plan_for(target): + """全局搜索 (基础组合, 炸弹列表),使 base × max(1, Σ炸弹) 最接近 target。 + + 在"误差在最优 +5 个百分点以内"的若干方案里随机挑一个 —— 改前永远返回同一套最优解, + 所以同一档位的回合盘面/炸弹老是那几个("某个符号消除必定带着 3 和 6 倍炸弹")。""" + if target <= 0: + return [], [] + plans = [] + for base, combo in _BASE_OPTIONS: + plans.append((abs(base - target) / target, combo, None)) # 方案A:无炸弹 + m = target / base + if m >= 2: # 方案B:配炸弹 + achieved = base * sum(_best_bombs(m)) + plans.append((abs(achieved - target) / target, combo, m)) + plans.sort(key=lambda p: p[0]) + best_err = plans[0][0] + pool = [p for p in plans if p[0] <= best_err + 0.05] + _, combo, m = random.choice(pool) + return combo, ([] if m is None else _decompose_best(m)) + + +def _plan_base_only(target): + """只挑基础组合(不植炸弹):在接近最优的组合里随机。acc 已经提供倍数时用这个。""" + if target <= 0: + return [('I', 8)] + cands = sorted((abs(base - target) / target, combo) for base, combo in _BASE_OPTIONS) + best = cands[0][0] + pool = [c for e, c in cands if e <= best + 0.05] + return random.choice(pool) + + +def _force_win(grid, target, mul_map, acc=0.0, allow_bomb=True): + """把 target 当"本旋最终赢额"来造奖:先按当前累计倍数 acc 折算需要的基础分。 + + 买单旋赢额 = 基础分 × max(1, acc + 本旋炸弹),所以: + · acc 已经 >1 时只挑基础分、**不再植炸弹**(否则 acc 和炸弹的倍数会被算两次, + 实测把 10 倍目标打成 40~50 倍); + · 该模式不允许炸弹(普通旋转/底注)时同理只挑基础分; + · 其余情况(buy_fs 且 acc=0)按原逻辑植炸弹,大奖局(100 倍以上)靠这一步才能到。""" + need_base = target / max(1.0, acc) if acc > 0 else target + if need_base >= 0.25: + if acc > 1 or not allow_bomb: + combo, bombs = _plan_base_only(need_base), [] + else: + combo, bombs = _plan_for(need_base) + else: + combo, bombs = [('I', 8)], [] # acc 本身已超过目标:只给最小档,不再加炸弹 + used = set() + win_syms = set() + for sym, cnt in combo: + win_syms.add(sym) + free = [p for p in range(GRID_SIZE) if p not in used] + for p in random.sample(free, min(cnt, len(free))): + r, c = divmod(p, GRID_COLS) + grid[r][c] = sym + used.add(p) + free = [p for p in range(GRID_SIZE) if p not in used] + for v in bombs: + if not free: + break + p = free.pop(random.randrange(len(free))) + r, c = divmod(p, GRID_COLS) + grid[r][c] = BOMB_SHORT + mul_map[p] = v + used.add(p) + for p in range(GRID_SIZE): + r, c = divmod(p, GRID_COLS) + if grid[r][c] == '': + grid[r][c] = _weighted_sym(exclude=win_syms) + _no_extra(grid, win_syms) + return win_syms + + +# ==================================================================== +# 核心:一次旋转的 cascade 链 +# ==================================================================== + +def _spin(mode_code, bias=None, acc=0, is_free_spin=False, target_scatter=0, + allow_bomb=True, win_prob=0.0, continue_prob=0.0, cascade_max=0, + bomb_prob=None, bomb_max=None, max_bombs=None): + """ + 一次旋转(含多次 cascade)。 + 炸弹跨 cascade 保留不消除;末帧统一结算: total = base_sum * max(1, acc+bomb_sum) + acc 只在本旋转有消除时累加炸弹(与 engine.end_spin 一致)。 + allow_bomb=False 时初始盘也不出炸弹;级联补位一律不出。 + **炸弹只存在于免费旋转内**:普通旋转/底注的基础旋(含进 FS 的那张触发盘)一律不出炸弹; + 三个模式进 FS 后规则完全一致(炸弹 + 累计器 + 每旋保底消除)。 + win_prob/continue_prob:本盘凑不出消除时按概率补最小档消除(首盘/级联各自取一个), + cascade_max 限制单旋最大波数 —— 让 FS 里"每旋都在消"。 + 返回 (steps, spin_total, acc_out, scatter_count) + """ + if not is_free_spin: + allow_bomb = False + steps = [] + seq = 0 + mul_map = {} # {pos0: bomb_val} 跨 cascade 累积 + if bomb_prob is None: + bomb_prob = BOMB_CELL_PROBABILITY + # 本旋是不是"大奖旋"(补桶植入目标额的那一旋):只有它按累计倍数结算 + plant_spin = isinstance(bias, (int, float)) and bias > 0 + + grid = None + prev_win_pos = [] + sym_acc = Counter() # 本旋转累计消除符号 + spin_winners = [] # 落库留档用(接口已不下发,面板由 symbol_links 汇总) + spin_links = [] # 本旋累积连线上报:[{symbol,count,loc,base_score,tumble}],新旋清空 + total_base = 0.0 + recorded = 0.0 # 已记入前置步骤的赢额 + removed, drops, fills = [], [], [] + acc_before_spin = acc + + while True: + seq += 1 + if seq == 1: + grid = _empty_grid() + if isinstance(bias, (int, float)) and bias > 0: + win_syms = _force_win(grid, float(bias), mul_map, acc, allow_bomb) + else: + _fill(grid, mul_map, allow_bomb, bomb_prob=bomb_prob, bomb_max=bomb_max, + max_bombs=max_bombs) + if bias == 'zero': + _no_extra(grid, set()) + if target_scatter > 0: + _place_scatter(grid, target_scatter, protect=set(_winners(grid))) + removed, drops, fills = [], [], [] + else: + removed = [p + 1 for p in prev_win_pos] + grid, drops, mul_map = _collapse(grid, prev_win_pos, mul_map) + fills = _fill(grid, mul_map, allow_bomb=False, track=True) + + # 保底消除:本盘自然凑不出 8 连时,按概率补一组最小档(首盘用 win_prob,级联用 continue_prob) + prob = win_prob if seq == 1 else continue_prob + if prob > 0 and (not cascade_max or seq < cascade_max) \ + and random.random() < prob and not _winners(grid): + _force_small_win(grid, prefer=[f['pos'] - 1 for f in fills]) + + # 保底消除 / 打散会覆盖"刚落下"或"刚补位"的格子 —— 这两份动画数据里的 symbol 按**最终盘面** + # 回填,前端按"消→落→补"重建出来的盘面才与 grid_state 完全一致(否则会画出不对的符号)。 + if drops: + drops = [dict(d, symbol=grid[(d['to'] - 1) // GRID_COLS][(d['to'] - 1) % GRID_COLS]) + for d in drops] + if fills: + fills = [dict(f, symbol=grid[(f['pos'] - 1) // GRID_COLS][(f['pos'] - 1) % GRID_COLS]) + for f in fills] + + w = _winners(grid) + is_final = (len(w) == 0) + + base = 0.0 + win_pos = [] + mul_det = [{"pos": p + 1, "val": v, "quality": bomb_quality(v)} + for p, v in sorted(mul_map.items())] + bomb_sum = sum(mul_map.values()) + + # symbol_links = **本旋累积**的连线上报(每波消除往上追加,tumble 标明是旋内第几波)—— + # 它同时充当"获胜者面板"(前端按 symbol 汇总即可),所以 spin_winners 字段已删。 + for sym, (cnt, pay) in sorted(w.items()): + pos_list = [r * GRID_COLS + c for r in range(GRID_ROWS) + for c in range(GRID_COLS) if grid[r][c] == sym][:cnt] + base += pay + win_pos.extend(pos_list) + sym_acc[sym] += cnt + spin_links.append({ + 'symbol': sym, 'count': cnt, + 'loc': [p + 1 for p in pos_list], + 'base_score': round(pay, 4), + 'tumble': seq, # 本旋第几波消除(= 本步 tumbleNum) + }) + total_base += base + + # 末帧:统一引爆(一旋转内不管几次消除,消完才爆) + # 累计器仅免费旋转有效;基础旋转炸弹只乘本旋赢额、不留存 + scatter_win = 0.0 + scatter_count = 0 + acc_after = acc + if is_final: + scatter_count = _count_scatter(grid) + scatter_win = _scatter_payout(scatter_count) + if total_base > 0: + if is_free_spin: + acc_after = acc + bomb_sum # FS:炸弹并入累计器,跨旋传递 + # 本旋赢额 = 基础分 × max(1, 累计 + 本旋炸弹) —— 累计倍数作用到每一次消除 + # (玩家要的就是这个滚雪球:0.4×2 + 0.5×4 = 2.8) + effective = max(1, acc_after) + else: + acc_after = acc # 基础旋:无累计器 + effective = max(1, bomb_sum) + win_amt = total_base * effective - recorded + scatter_win + else: + win_amt = scatter_win # 无消除→炸弹作废、累计不变 + else: + win_amt = base # 中间步只记基础分 + recorded += win_amt if not is_final else 0.0 + + # scatter 触发/重触发(仅末帧) + scatter_trigger, retrigger_info = None, None + if is_final: + if is_free_spin and scatter_count >= SCATTER_RETRIGGER_TRIGGER: + retrigger_info = {'scatterCount': scatter_count, + 'extraSpins': FREE_SPIN_RETRIGGER_COUNT, + 'scatterWin': round(scatter_win, 2)} + elif not is_free_spin and scatter_count >= SCATTER_FREE_SPIN_TRIGGER: + scatter_trigger = {'scatterCount': scatter_count, + 'freeSpins': FREE_SPINS_AWARDED, + 'scatterWin': round(scatter_win, 2)} + + steps.append({ + 'cascade_seq': seq, 'free_spin_id': 0, + 'grid_state': [row[:] for row in grid], + 'multiplier_detail': json.dumps(mul_det), + 'symbol_links': json.dumps(spin_links, ensure_ascii=False), # 本旋累积(含 tumble) + 'accumulated_symbols': json.dumps(dict(sym_acc)), # 仅落库留档,不下发 + 'spin_winners': json.dumps(spin_winners, ensure_ascii=False), # 仅落库留档,不下发 + 'removed_positions': json.dumps(removed), # 仅落库留档,不下发 + 'drop_data': json.dumps(drops), + 'fill_data': json.dumps(fills if fills else []), + 'scatter_trigger': json.dumps(scatter_trigger) if scatter_trigger else None, + 'retrigger_info': json.dumps(retrigger_info) if retrigger_info else None, + 'base_score': round(base, 2), # 仅落库留档,不下发 + 'win_amount': round(win_amt, 2), + 'accumulator_before': round(acc_before_spin, 2), + 'accumulator_after': round(acc_after if is_final else acc, 2), + 'spin_win': 0, 'win_capped': 0, 'bigwin_level': None, + 'is_final': 1 if is_final else 0, + }) + + if is_final: + total = round(sum(s['win_amount'] for s in steps), 2) + return steps, total, acc_after, scatter_count + prev_win_pos = win_pos + + +# ==================================================================== +# 免费旋转会话(15 次起,≥3S 重触发 +5,上限 30) +# ==================================================================== + + +def _fs_next_target(spin_no, wins, total_spins, quota): + """下一旋"该保底消除"的位置:间隔随机,但均值 = 剩余旋数 / 剩余配额。 + + 为什么要随机:等距铺(每 quota/总局数 旋必消一次)会让玩家几局就看穿"到第 4 旋肯定消"—— + 实测等距版本有 87% 的局"最长连空正好 4 旋"。这里改成每次消除后重抽下一次的位置: + 间隔 = round(均值 + δ),δ ∈ {-2..2} 权重 {1,2,3,2,1}(均值 0,所以整局的消除次数不变), + 玩家看到的空转长度在 1~5 旋之间跳,而不是恒定 4。剩余配额为 0 时返回 None(后面只靠自然消)。 + """ + left_spins = total_spins - spin_no + left_quota = quota - wins + if left_spins <= 0 or left_quota <= 0: + return None + delta = random.choices((-2, -1, 0, 1, 2), weights=(1, 2, 3, 2, 1))[0] + return spin_no + max(1, int(round(left_spins / left_quota + delta))) + + +def _free_spin_session(mode_code, slot_target=None, acc=0, fs_budget=None): + """免费旋转会话(15 次起,≥3S 重触发 +5,上限 30)。 + + **三个模式同一套规则**(炸弹 + 累计器 + 每旋保底消除),区别只在"这一局值多少钱": + · buy_fs:价格换来桶目标,拆成「保底消除预算 fs_budget + 一旋大奖 slot_target」; + · normal/ante 自然触发:没有价格可拆 → 只给一份同档保底预算、不植大奖旋, + 整局赔付落在哪由自然结果决定。 + fs_budget 按"配额 + 随机间隔"花:预算能买 k 次消除(k ≈ √(预算/FS_GUARANTEE_COST)), + 每次消除后重抽下一次的位置(间隔随机、均值 = 剩余旋数/剩余配额),自然消除也算一次进度; + 空转太久再按 FS_DRY_FORCE_PROB 软兜底 —— 旧写法按"剩余预算÷剩余旋数"算每旋概率,累计倍数 + 一涨就把预算烧穿,后半局十几旋连空;等距铺法又会让玩家看穿"到第 4 旋肯定消"(见 _fs_next_target)。 + slot_target 只作用于其中随机一旋(它按累计倍数结算,不占保底配额)。 + """ + total_spins = FREE_SPINS_AWARDED + fs_index = 0 + all_steps = [] + fs_total = 0.0 + sc_dist = FREESPIN_SCATTER.get(mode_code, FREESPIN_SCATTER['default']) + budget = fs_budget + # 炸弹大小按档位配:低倍局用小球(预算买得起多次"消除+爆"),高倍局用全量(2~1000 的大球) + target_mul = float(slot_target) if isinstance(slot_target, (int, float)) else 0.0 + spin_bomb_max = None if target_mul >= FS_BIG_BOMB_TARGET else FS_SMALL_BOMB_MAX + bias_slot = random.randrange(FREE_SPINS_AWARDED) if slot_target else -1 + # 保底配额:预算 ÷ 单次消除边际赔付再开方 = 这一局"买得起"几次消除 + quota = None if budget is None else max(1, int((max(0.0, budget) / FS_GUARANTEE_COST) ** 0.5)) + wins = 0 # 已消除的旋数(自然 + 保底) + dry = 0 # 已经连续空转了几旋(兜底用) + target = None if quota is None else _fs_next_target(0, 0, total_spins, quota) + while fs_index < total_spins: + fs_index += 1 + is_target_slot = (fs_index - 1) == bias_slot + spin_bias = slot_target if is_target_slot else None + forced = False + if spin_bias is None and quota is not None: + # 到点了 / 空转太久被软兜底抽中 → 本旋必补一次保底消除 + forced = target is not None and fs_index >= target + if not forced and dry >= FS_DRY_FORCE_HARD: + forced = True + elif not forced and dry in FS_DRY_FORCE_PROB: + forced = random.random() < FS_DRY_FORCE_PROB[dry] + p = 1.0 if forced else 0.0 + else: + p = FS_SPIN_WIN_PROB + sc = _weighted_choice(sc_dist) + # 已到 30 旋上限:这一旋不再落 3+ 个 S —— 否则会写出"extraSpins=5 但没有旋可加"的 + # retrigger_info(玩家白看一次 +5,前端还得自己夹 30)。上限之前的那一旋照常触发第 3 次 +5。 + if total_spins >= MAX_FREE_SPINS: + sc = min(sc, SCATTER_RETRIGGER_TRIGGER - 1) + spin_steps, spin_score, acc, sc_count = _spin( + mode_code, spin_bias, acc, is_free_spin=True, target_scatter=sc, + win_prob=p, continue_prob=FS_CASCADE_CONTINUE_PROB, + cascade_max=FS_CASCADE_MAX, + bomb_prob=FS_SCREEN_BOMB_PROB, bomb_max=spin_bomb_max, + max_bombs=FS_MAX_BOMBS_PER_SPIN) + if sc_count >= SCATTER_RETRIGGER_TRIGGER and total_spins < MAX_FREE_SPINS: + total_spins += FREE_SPIN_RETRIGGER_COUNT + # 目标大奖旋的花费由目标额单独控制,不占用保底预算 + if budget is not None and not is_target_slot: + budget -= spin_score + for s in spin_steps: + s['free_spin_id'] = fs_index + fs_total += spin_score + all_steps.extend(spin_steps) + # 进度:消了(不管自然还是保底)就重新抽下一次的位置 —— 整局消除次数仍由配额决定 + if spin_bias is None and quota is not None: + if any(float(s['base_score']) > 0 for s in spin_steps): + wins += 1 + dry = 0 + target = _fs_next_target(fs_index, wins, total_spins, quota) + else: + dry += 1 + return all_steps, fs_total + + +# ==================================================================== +# 主入口 +# ==================================================================== + +def generate_round(box, bias=None): + """ + 生成一个完整回合。box: {'box_id','box_type',...} bias: None/'zero'/目标倍率(float) + 返回 (round_data, steps, orn_id) + """ + mode_code = box['box_type'] + buy_fs = mode_code == 'buy_fs' + # 目标高于"基础旋(无炸弹)能赔到的上限"时,只能生成自然触发 FS 的回合来装这个桶: + # normal/ante 的 100x/150x/200x 桶没有别的来源(基础旋不出炸弹,最多 ~60x)。 + must_free_spins = isinstance(bias, (int, float)) and not buy_fs \ + and float(bias) >= FS_ONLY_TARGET_MUL + is_fs = False + if buy_fs: + is_fs = True # 购买模式保证进 FS(互斥:无底注效果) + elif must_free_spins: + is_fs = True + elif mode_code in BASE_FS_TRIGGER_PROB and bias is None: + is_fs = random.random() < BASE_FS_TRIGGER_PROB[mode_code] + + if is_fs: + # 触发盘:三模式都固定/摇出 SCATTER,且**不出炸弹**(炸弹只存在于免费旋转内) + trigger_sc = 4 if buy_fs else _weighted_choice(FS_TRIGGER_SCATTER) + base_steps, base_total, _, _ = _spin( + mode_code, None, 0, is_free_spin=False, target_scatter=trigger_sc, + allow_bomb=False) + # FS 预算:买 FS / 高倍桶(bias=桶目标)拆成「保底消除预算 + 一旋大奖」; + # normal/ante 自然摇出来的 FS(bias=None)没有价格可拆 → 只给同档保底预算、不植大奖旋, + # 整局赔付由自然消除 + 炸弹累计决定(实测约 80x、5 旋有消除,落在模式自己的桶里)。 + fs_budget, slot_target = None, None + if isinstance(bias, (int, float)) and bias > 0: + budget = max(0.0, float(bias) - float(base_total)) + if budget <= FS_FLOOR_BUDGET_CAP: + fs_budget = budget # 低倍局:全靠保底小消 + else: + fs_budget = FS_FLOOR_BUDGET_CAP # 保底小消是平价结算(×1),预算可以给足 + slot_target = budget - fs_budget # 其余给"大奖旋"(它按累计倍数结算) + elif not buy_fs: + fs_budget = FS_FLOOR_BUDGET_CAP + # 炸弹累计器仅免费旋转内生效:FS 会话从 0 起累,触发盘炸弹不带入 + fs_steps, fs_total = _free_spin_session( + mode_code, slot_target=slot_target, acc=0, fs_budget=fs_budget) + steps = base_steps + fs_steps + else: + nearmiss = _weighted_choice(BASE_NEARMISS_SCATTER.get(mode_code, {0: 1.0})) + steps, _, _, _ = _spin(mode_code, bias, 0, + is_free_spin=False, target_scatter=nearmiss) + + # 理论总分(落桶用,可到 1370+) + raw_total = round(sum(s['win_amount'] for s in steps), 2) + orn_id = _bucket(raw_total) + + # 发放截断 + 口径重写:累计到 1000 即终止,动画=到账零偏差。 + # win_amount = **本旋累计到本步的赢额**(该旋末步就是这一旋的总额); + # spin_win = **本局累计到本步的赢额**(最后一步 = 整局实付 = totalWin) + # 前端因此不需要自己相加:本步增量 = 与上一步相减,本旋总额 = 本旋末步的 win_amount。 + raw_increments = [s['win_amount'] for s in steps] + cumul, spin_cumul, capped_steps, hit = 0.0, {}, [], False + for s, inc in zip(steps, raw_increments): + if cumul + inc >= MAX_WIN: # 封顶:本步补足到 1000 并终止 + inc = round(MAX_WIN - cumul, 2) + s['win_capped'] = 1 + s['is_final'] = 1 + hit = True + key = s['free_spin_id'] + spin_cumul[key] = round(spin_cumul.get(key, 0.0) + inc, 2) + cumul = round(cumul + inc, 2) + s['win_amount'] = spin_cumul[key] # 本旋累计(末步即该旋总额) + s['spin_win'] = cumul # 本局累计 + capped_steps.append(s) + if hit: + break + if hit: + steps = capped_steps + paid_total = round(cumul, 2) + + # 爆奖动画(小/中/大):**只有免费旋转里才判档** —— normal/ante 靠自然触发 FS、 + # buy_fs 本身即 FS;基础旋(free_spin_id=0)一律 null,不播大奖动画。 + # 单旋赢额 = 该旋末帧的 win_amount;旋与旋互不影响,同档一局可多次播放。 + for s in steps: + if s['is_final'] and s['free_spin_id'] > 0: + s['bigwin_level'] = bigwin_level(mode_code, float(s['win_amount'])) + + fs_cnt = len({s['free_spin_id'] for s in steps if s['free_spin_id'] > 0}) + rd = { + 'box_id': box['box_id'], 'ornament_id': orn_id, + 'actual_mul': round(paid_total, 4), # 实付倍率 ≤1000 + 'free_spin_count': fs_cnt, + 'total_cascades': len(steps), + } + return rd, steps, orn_id + + +# ==================================================================== +# 分布补偿:两阶段填桶 +# ==================================================================== + +def _bias_for(orn_id): + if orn_id == 1: + return 'zero' + o = next(x for x in ORNAMENTS if x['id'] == orn_id) + return float(o['expect_mul']) + + +def _calibrate_bias(box, orn_id, samples=8): + """给某个桶在线找一个"更容易撞上"的 bias(只改传给生成器的目标值,不动任何规则)。 + + 为什么需要:`_bias_for` 用的是桶的名义 expect_mul,但 buy_fs 的整局赔付被 FS 会话抬高了 —— + 实测 bias=10(桶 7 名义 10x)时实际赔付 min 14 / 中位 58,**永远撞不进 8~12**,命中率仅 3%, + stage2 只能白跑几万次(单箱 5~10 分钟)。这里拿几个候选值各试几局,挑命中率最高的那个。""" + target = _bias_for(orn_id) + best, best_hit = target, -1 + for mult in (1.0, 0.5, 0.3, 0.15, 0.08): + b = target * mult + hit = 0 + for _ in range(samples): + _, _, oid = generate_round(box, b) + hit += (oid == orn_id) + if hit > best_hit: + best, best_hit = b, hit + if hit == samples: # 已经全中,不用再试更小/更大的 + break + return best + + +def fill_distribution(box, target_counts, max_rounds=None): + """阶段1 自由生成积累常见桶;阶段2 按缺额 bias 补桶。返回 (rounds, 实际分布) + + 阶段2 的尝试上限按 total×60:保底消除改成"随机间隔"之后,低倍桶(10x/16x)的命中率大约减半 + (同样的钱摊到整局、摊到累计倍数高的旋上更贵),尝试数不够会填不满池子(实测 ×30 时 + 10x 桶只填到 178/297,×60 满池)。 + 某个桶饿死时(试了 4 倍于它条数的次数还没填满)会重新校准它的 bias —— 只对慢桶生效, + 命中率本来就高的桶走原来的名义值,不额外花钱。""" + gen = Counter() + rounds = [] + total_target = sum(target_counts.values()) + bias_use = {} # orn_id -> 校准后的 bias + tries = Counter() # orn_id -> 已尝试次数 + + def _accept(oid): + if oid in target_counts and gen[oid] < target_counts[oid]: + gen[oid] += 1 + return True + return False + + free_budget = min(total_target * 3, 12000) + for _ in range(free_budget): + if sum(gen.values()) >= total_target: + break + rd, steps, oid = generate_round(box, None) + if _accept(oid): + rounds.append((rd, steps)) + + max_att = (max_rounds or total_target * 60) + for _ in range(max_att): + deficit = [(oid, target_counts[oid] - gen[oid]) + for oid in target_counts if gen[oid] < target_counts[oid]] + if not deficit: + break + # 按缺口加权随机选桶,避免“最难桶”饿死其他桶 + need = random.choices([d[0] for d in deficit], + weights=[d[1] for d in deficit])[0] + tries[need] += 1 + if need not in bias_use and tries[need] > target_counts[need] * 4: + bias_use[need] = _calibrate_bias(box, need) + rd, steps, oid = generate_round(box, bias_use.get(need, _bias_for(need))) + if _accept(oid): + rounds.append((rd, steps)) + + return rounds, dict(gen) diff --git a/olympus-py/pyproject.toml b/olympus-py/pyproject.toml new file mode 100644 index 0000000..5c8fb9c --- /dev/null +++ b/olympus-py/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "olympus" +version = "0.1.0" +description = "Olympus match-3 game engine with bomb accumulator rules." +readme = "README.md" +requires-python = ">=3.9" +license = {text = "Proprietary"} + +[tool.setuptools.packages.find] +where = ["."] +include = ["olympus*"] +namespaces = false diff --git a/olympus-py/schema.sql b/olympus-py/schema.sql new file mode 100644 index 0000000..1bae08f --- /dev/null +++ b/olympus-py/schema.sql @@ -0,0 +1,240 @@ +-- ============================================================ +-- Olympus 电子游戏模块 全量建表 DDL +-- 表前缀: game_olympus_ 游戏类型: OLYMPUS +-- 设计对齐 sweet 体系,差异点: +-- 1. 饰品桶最高 1000x(单局封顶 1000 倍基础下注) +-- 2. step 表自带 accumulator_before/after(跨回合炸弹累计)与 win_capped +-- 3. record 表自带结算/补发字段(settled 等),无需后期 ALTER +-- 4. 模式仅三种: normal / ante / buy_fs(互斥,无 super 模式) +-- ============================================================ + +-- 1. 箱子配置(模式 × 下注档位) +CREATE TABLE IF NOT EXISTS game_olympus_box ( + box_id BIGINT NOT NULL COMMENT 'PK', + game_type VARCHAR(16) NOT NULL DEFAULT 'OLYMPUS', + box_type VARCHAR(32) NOT NULL COMMENT 'normal|ante|buy_fs', + bet DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '基础下注', + name VARCHAR(50) DEFAULT NULL, + price DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '实际扣费 = bet × CM (normal=1/ante=2/buy_fs=20)', + total_ornaments INT NOT NULL DEFAULT '0' COMMENT '总回合数', + rtp DECIMAL(5,4) NOT NULL DEFAULT '0.8000' COMMENT '目标返奖率', + rtp_converge_ratio DECIMAL(4,3) NULL COMMENT '一轮收敛回合占比,空=默认0.15', + sort INT DEFAULT '0', + status VARCHAR(1) DEFAULT '1' COMMENT '1启用 0禁用', + del_flag VARCHAR(1) DEFAULT '0', + remark VARCHAR(200) DEFAULT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (box_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus箱子配置'; + +-- 2. 饰品桶定义(0x ~ 1000x,共17档) +CREATE TABLE IF NOT EXISTS game_olympus_ornament ( + id BIGINT NOT NULL COMMENT 'PK 1-17', + game_type VARCHAR(16) NOT NULL DEFAULT 'OLYMPUS', + expect_mul DECIMAL(8,2) NOT NULL COMMENT '期望倍率(最高1000)', + name VARCHAR(50) DEFAULT NULL, + range_low DECIMAL(8,2) DEFAULT NULL COMMENT '落桶区间下限(含)', + range_high DECIMAL(8,2) DEFAULT NULL COMMENT '落桶区间上限(含)', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus饰品桶定义'; + +-- 3. 箱子-饰品桶分布(库存配额,剩余 = count - used) +-- audience 多身份爆率:每种身份(common/anchor/robot)一套独立分布,回合库存池共享 +CREATE TABLE IF NOT EXISTS game_olympus_box_ornament ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + box_id BIGINT NOT NULL, + ornament_id INT NOT NULL, + audience VARCHAR(16) NOT NULL DEFAULT 'common' COMMENT '身份维度 common普通/anchor主播/robot机器人', + count INT NOT NULL DEFAULT '0' COMMENT '分配回合数', + used INT NOT NULL DEFAULT '0' COMMENT '已消耗回合数,剩余=count-used', + PRIMARY KEY (id), + UNIQUE KEY uk_box_ornament_aud (box_id, ornament_id, audience) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus箱子饰品分布(多身份)'; + +-- 4. 预生成回合 +CREATE TABLE IF NOT EXISTS game_olympus_round ( + round_id BIGINT NOT NULL COMMENT 'PK', + box_id BIGINT NOT NULL, + ornament_id INT NOT NULL COMMENT '落入的饰品桶 1-17', + actual_mul DECIMAL(10,4) NOT NULL DEFAULT '0.0000' COMMENT '实际总倍率(≤1000)', + free_spin_count INT NOT NULL DEFAULT '0' COMMENT '免费旋转总次数(15+重触发×5)', + total_cascades INT NOT NULL DEFAULT '0' COMMENT '总步数', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (round_id), + INDEX idx_box_ornament (box_id, ornament_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus预生成回合'; + +-- 5. 回合步骤(级联/翻滚明细) +CREATE TABLE IF NOT EXISTS game_olympus_step ( + id BIGINT NOT NULL COMMENT 'PK', + round_id BIGINT NOT NULL, + free_spin_id INT NOT NULL DEFAULT '0' COMMENT '0=基础旋转, >0=第N次免费旋转', + cascade_seq INT NOT NULL DEFAULT '0' COMMENT '本旋转内第几次翻滚', + grid_state JSON DEFAULT NULL COMMENT '6×5网格最终状态', + multiplier_detail JSON DEFAULT NULL COMMENT '炸弹符号 [{"pos":int,"val":int},...]', + symbol_links JSON DEFAULT NULL COMMENT '中奖明细 [{"symbol","count","loc",...}]', + accumulated_symbols JSON DEFAULT NULL COMMENT '本旋转累计消除符号 {"CROWN":9,...}', + spin_winners JSON DEFAULT NULL COMMENT '获胜者面板:本旋逐波追加 [{"symbol","count","win"},...] 新旋清空', + removed_positions JSON DEFAULT NULL COMMENT '消除位置 [pos,...]', + drop_data JSON DEFAULT NULL COMMENT '下落数据 [{"from":int,"to":int,"symbol":"A"},...]', + fill_data JSON DEFAULT NULL COMMENT '填充数据 [{"pos":int,"symbol":"A"},...]', + scatter_trigger JSON DEFAULT NULL COMMENT '基础盘触发免费旋转 {scatterCount,freeSpins(15),scatterWin}', + retrigger_info JSON DEFAULT NULL COMMENT '免费旋转重触发 {scatterCount,extraSpins(5),scatterWin}', + base_score DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '本步基础分(未乘炸弹)', + win_amount DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '本步赢额(回合末帧含炸弹结算)', + accumulator_before DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '本步前累计炸弹倍数', + accumulator_after DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '本步后累计炸弹倍数(跨回合传递)', + spin_win DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '整局累计赢额', + win_capped TINYINT NOT NULL DEFAULT '0' COMMENT '本步赢额被1000x封顶截断 0否1是', + bigwin_level VARCHAR(16) DEFAULT NULL COMMENT '爆奖档位(小/中/大,只在免费旋转里判档,按单旋赢额盖在该旋末帧,同档可多次): small/middle/big', + is_final TINYINT DEFAULT '0' COMMENT '本旋转末帧', + PRIMARY KEY (id), + INDEX idx_round (round_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus回合步骤'; + +-- 6. 游戏记录(含结算/补发字段,建表即带全) +CREATE TABLE IF NOT EXISTS game_olympus_record ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + user_id BIGINT NOT NULL, + box_id BIGINT NOT NULL, + round_id BIGINT NOT NULL, + cost DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '扣费 = bet × CM', + total_win DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '总赢额(结算时填,≤bet×1000)', + settled TINYINT NOT NULL DEFAULT '0' COMMENT '是否已结算 0未1已(幂等守卫)', + mode_type VARCHAR(20) DEFAULT NULL COMMENT '游戏模式 normal/ante/buy_fs', + auto_dismantle TINYINT DEFAULT 0 COMMENT '开局自动分解快照 0关1开', + reward_type VARCHAR(10) DEFAULT NULL COMMENT '发放类型 gold/ornament', + settled_win DECIMAL(10,2) DEFAULT 0.00 COMMENT '本局价值(金币金额=饰品价格)', + prize_id BIGINT DEFAULT NULL COMMENT '饰品ID game_olympus_prize_ornament.id', + prize_name VARCHAR(100) DEFAULT NULL COMMENT '饰品名称', + prize_image_url VARCHAR(500) DEFAULT NULL COMMENT '饰品图片URL', + prize_level_img VARCHAR(500) DEFAULT NULL COMMENT '品质图片URL', + settle_source VARCHAR(20) DEFAULT NULL COMMENT '结算来源 start=开局已结算(动画还在播) / finish=末帧播完(已结束,进战绩列表)', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + INDEX idx_user_time (user_id, create_time), + INDEX idx_box_time (box_id, create_time), + INDEX idx_settled_time (settled, create_time) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Olympus游戏记录'; + +-- 7. 用户统计 +CREATE TABLE IF NOT EXISTS game_olympus_user ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + user_id BIGINT NOT NULL, + total_spins BIGINT NOT NULL DEFAULT '0', + total_cost DECIMAL(12,2) NOT NULL DEFAULT '0.00', + total_win DECIMAL(12,2) NOT NULL DEFAULT '0.00', + total_free_spins INT NOT NULL DEFAULT '0', + max_win_mul DECIMAL(10,4) NOT NULL DEFAULT '0.0000' COMMENT '历史最高倍率(≤1000)', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uk_user (user_id) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Olympus用户统计'; + +-- 8. 每日盈亏 +CREATE TABLE IF NOT EXISTS game_olympus_win ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + date DATE NOT NULL, + total_spins BIGINT NOT NULL DEFAULT '0', + total_cost DECIMAL(14,2) NOT NULL DEFAULT '0.00', + total_win DECIMAL(14,2) NOT NULL DEFAULT '0.00', + net_profit DECIMAL(14,2) NOT NULL DEFAULT '0.00', + player_count INT NOT NULL DEFAULT '0', + PRIMARY KEY (id), + UNIQUE KEY uk_date (date) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Olympus每日盈亏'; + +-- 9. 返奖饰品定义(展示用饰品,按赢额区间匹配) +CREATE TABLE IF NOT EXISTS game_olympus_prize_ornament ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + game_type VARCHAR(16) NOT NULL DEFAULT 'OLYMPUS', + name VARCHAR(100) DEFAULT NULL COMMENT '饰品名称', + image_url VARCHAR(500) DEFAULT NULL COMMENT '饰品图片URL', + grade INT DEFAULT NULL COMMENT '品质等级 1-5', + exterior_name VARCHAR(100) DEFAULT NULL COMMENT '品质名称 崭新出厂/略有磨损/久经沙场/破损不堪/战痕累累', + sort INT DEFAULT '0', + status VARCHAR(1) DEFAULT '1', + is_fallback TINYINT DEFAULT '0' COMMENT '兜底饰品 1是(仅展示不覆盖金额) 0否', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Olympus返奖饰品'; + +-- 10. 返奖饰品区间(与 prize_ornament 一一对应,区间连续覆盖) +CREATE TABLE IF NOT EXISTS game_olympus_prize_range ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + prize_id BIGINT NOT NULL COMMENT 'game_olympus_prize_ornament.id', + win_low DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '返奖区间下限(含)', + win_high DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '返奖区间上限(含)', + PRIMARY KEY (id), + UNIQUE KEY uk_prize (prize_id) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Olympus返奖区间'; + +-- 11. 箱子饰品分布模板(抽干补桶依据,按身份区分) +CREATE TABLE IF NOT EXISTS game_olympus_box_template ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + box_id BIGINT NOT NULL COMMENT '所属箱子ID', + audience VARCHAR(16) NOT NULL DEFAULT 'common' COMMENT '身份维度 common/anchor/robot', + name VARCHAR(100) NOT NULL COMMENT '模板名称(同一箱子+身份内唯一)', + total_rounds INT NOT NULL DEFAULT 0 COMMENT '总回合数 = items 各 count 之和', + items_json TEXT NOT NULL COMMENT '饰品分布JSON: [{"ornamentId":1,"count":100},...]', + is_active TINYINT NOT NULL DEFAULT 0 COMMENT '是否生效模板(抽干补桶依据) 1是 0否', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uk_box_aud_name (box_id, audience, name), + KEY idx_box_aud_active (box_id, audience, is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus箱子饰品分布模板(多身份)'; + +-- 12. 奖池水位调整日志(注水/抽水/重置审计) +CREATE TABLE IF NOT EXISTS game_olympus_reward_log ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', + box_id BIGINT DEFAULT NULL COMMENT '箱子ID(批量/重置为汇总,可空)', + box_name VARCHAR(100) DEFAULT NULL COMMENT '箱子名/汇总描述', + audience VARCHAR(16) NOT NULL DEFAULT 'common' COMMENT '身份 common/anchor/robot/white', + type TINYINT NOT NULL COMMENT '1注水 2抽水 3重置', + amount DECIMAL(14,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额(绝对值)', + balance_before DECIMAL(14,2) DEFAULT NULL COMMENT '调整前水位', + balance_after DECIMAL(14,2) DEFAULT NULL COMMENT '调整后水位', + pool_balance DECIMAL(14,2) DEFAULT NULL COMMENT '操作后总池余额', + operator VARCHAR(64) DEFAULT NULL COMMENT '操作人', + remark VARCHAR(200) DEFAULT NULL COMMENT '备注', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_create (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus奖池水位调整日志'; + +-- ============================================================ +-- 种子数据:饰品桶 1-17(0x ~ 1000x) +-- ============================================================ +INSERT INTO game_olympus_ornament (id, game_type, expect_mul, name, range_low, range_high) VALUES + (1, 'OLYMPUS', 0.00, '0x', 0.00, 0.00), + (2, 'OLYMPUS', 0.50, '0.5x', 0.01, 1.00), + (3, 'OLYMPUS', 1.50, '1.5x', 1.00, 2.00), + (4, 'OLYMPUS', 2.50, '2.5x', 2.00, 3.00), + (5, 'OLYMPUS', 4.00, '4x', 3.00, 5.00), + (6, 'OLYMPUS', 6.50, '6.5x', 5.00, 8.00), + (7, 'OLYMPUS', 10.00, '10x', 8.00, 12.00), + (8, 'OLYMPUS', 16.00, '16x', 12.00, 20.00), + (9, 'OLYMPUS', 25.00, '25x', 20.00, 30.00), + (10, 'OLYMPUS', 40.00, '40x', 30.00, 50.00), + (11, 'OLYMPUS', 60.00, '60x', 50.00, 70.00), + (12, 'OLYMPUS', 100.00, '100x', 70.00, 130.00), + (13, 'OLYMPUS', 150.00, '150x', 130.00, 170.00), + (14, 'OLYMPUS', 200.00, '200x', 170.00, 230.00), + (15, 'OLYMPUS', 300.00, '300x', 230.00, 370.00), + (16, 'OLYMPUS', 500.00, '500x', 370.00, 630.00), + (17, 'OLYMPUS', 1000.00, '1000x', 630.00, 1370.00) +ON DUPLICATE KEY UPDATE expect_mul=VALUES(expect_mul), name=VALUES(name), + range_low=VALUES(range_low), range_high=VALUES(range_high); + +-- ============================================================ +-- 附:Java 服务上线时执行(先不执行,接入后台时再放开) +-- ============================================================ +-- 维护开关 +-- INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +-- VALUES ('Olympus维护', 'olympusMaintenance', '0', 'Y', 'admin', NOW(), 'Olympus维护开关 0正常 1维护'); + +-- 未结算兜底补发定时任务(系统监控>定时任务) +-- INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) +-- VALUES ('Olympus未结算补发', 'OLYMPUS', 'olympusSettleTask.scheduledSettle()', '0 */10 * * * ?', '1', '0', '0', 'admin', NOW(), '每10分钟扫描超35分钟未结算记录并补发金币/饰品'); diff --git a/olympus-py/simulate_cycle.py b/olympus-py/simulate_cycle.py new file mode 100644 index 0000000..71b1424 --- /dev/null +++ b/olympus-py/simulate_cycle.py @@ -0,0 +1,176 @@ +"""复刻 Java 抽回合链路,模拟每个箱子抽完一个 cycle 的实时 RTP。 + +复刻的 Java 逻辑(OlympusRoundPicker / OlympusGameService / OlympusDistributionService): + 1. 固定池 fixedSum = Σ 配比 count(按身份),N = round(fixedSum/(1-收敛比例)), + fixedRounds = round(N×(1-收敛比例));固定阶段 drawn < fixedRounds + 2. 固定阶段:在"remaining>0 且 expect_mul×bet ≤ payoutCap"的桶里按 remaining 加权抽; + 桶内取 40 条候选,优先取 actual_mul×bet ≤ payoutCap 的(取不到就退回候选第一条);该桶 used+1 + 3. 收敛阶段:desiredNextMul = (N×目标倍率 − 已派彩) / (N − drawn), + 取期望最接近 desired 的 3 个桶(随机其一),桶内取 actual_mul 最接近 min(desired, capMul) 的一条 + (优先 ≤ capMul),不消耗 used + 4. 水位:payoutCap = max(0, 水位 + 本局成本) × rtp;水位 += 成本 − 派彩(白号不参与水位 → 无上限) + 5. drawn ≥ N → cycle 结束:resetCycle + refillFromConfig(有生效模板则按模板重写 count,否则 used 归零) + +用法: + python simulate_cycle.py # 全部 30 箱 × common 身份,跑 3 个 cycle + python simulate_cycle.py --cycles 5 --audience white + python simulate_cycle.py --box 21 +""" +import argparse +import random +from collections import defaultdict + +import olympus_db as db +from olympus.config import ORNAMENTS, MODES, payout_expect + +# 抽回合的 payoutCap 过滤用**名义** expect_mul(对齐 Java OlympusRoundPicker,偏保守); +# 统计里的"名义 RTP"用 payout_expect(对齐后台「目标RTP」列的封顶修正口径)。 +_ORN_MUL = {o['id']: o['expect_mul'] for o in ORNAMENTS} +_ORN_PAY = {o['id']: payout_expect(o['id']) for o in ORNAMENTS} +CANDIDATE_LIMIT = 40 + + +def load_boxes(box_id=None): + with db.get_conn() as conn: + cur = conn.cursor() + sql = 'SELECT box_id, box_type, bet, rtp, rtp_converge_ratio, total_ornaments FROM game_olympus_box' + if box_id: + sql += ' WHERE box_id=%s' % int(box_id) + cur.execute(sql + ' ORDER BY box_id') + boxes = [dict(zip(('box_id', 'box_type', 'bet', 'rtp', 'cr', 'total'), r)) for r in cur.fetchall()] + cur.execute('SELECT box_id, ornament_id, audience, count FROM game_olympus_box_ornament') + cfg = defaultdict(dict) + for b, o, a, c in cur.fetchall(): + cfg[(int(b), a)][int(o)] = int(c) + cur.execute('SELECT box_id, ornament_id, actual_mul FROM game_olympus_round') + rounds = defaultdict(lambda: defaultdict(list)) + for b, o, m in cur.fetchall(): + rounds[int(b)][int(o)].append(float(m)) + # 生效模板(抽完重填的依据) + cur.execute('''SELECT box_id, audience, items_json FROM game_olympus_box_template + WHERE is_active=1 ORDER BY id''') + tpl = {} + import json + for b, a, items in cur.fetchall(): + tpl[(int(b), a)] = {int(i['ornamentId']): int(i['count']) + for i in json.loads(items) if int(i.get('count', 0)) > 0} + for b in boxes: + b['cm'] = float(MODES[b['box_type']].cost_multiplier) + return boxes, cfg, rounds, tpl + + +def _weighted(cands, weights): + total = sum(weights) + r = random.uniform(0, total) + acc = 0.0 + for c, w in zip(cands, weights): + acc += w + if r <= acc: + return c + return cands[-1] + + +def simulate(box, cfg, rounds, tpl, audience, cycles=3, cr_override=None): + cm, rtp = box['cm'], float(box['rtp']) + cr = float(cr_override if cr_override is not None else (box['cr'] or 0.15)) + fixed_sum = sum(cfg.values()) + n_total = round(fixed_sum / (1.0 - cr)) + fixed_rounds = round(n_total * (1.0 - cr)) + target_mul = rtp * cm + capped = (audience != 'white') # 白号 payoutCap=null + stats = [] + counts = dict(cfg) + cumulative = {'drawn': 0, 'mul': 0.0} + balance = 0.0 # 箱子水位:跨 cycle 持久(= Σ投注 − Σ返奖) + for cyc in range(1, cycles + 1): + used = defaultdict(int) + drawn, mul_sum = 0, 0.0 + rtp_fixed = None + while drawn < n_total: + cap_mul = max(0.0, balance + cm) * rtp if capped else float('inf') + remaining = {o: max(0, c - used[o]) for o, c in counts.items()} + if drawn < fixed_rounds and sum(remaining.values()) > 0: + elig = [o for o, r in remaining.items() + if r > 0 and _ORN_MUL[o] <= cap_mul] or \ + [o for o, r in remaining.items() if r > 0] + orn = _weighted(elig, [remaining[o] for o in elig]) + pool = rounds[box['box_id']].get(orn) or [0.0] + cands = random.sample(pool, min(CANDIDATE_LIMIT, len(pool))) + mul = next((m for m in cands if m <= cap_mul), cands[0]) + used[orn] += 1 + else: + desired = max(0.0, (n_total * target_mul - mul_sum) / max(1, n_total - drawn)) + allowed = [o for o, c in counts.items() if _ORN_MUL[o] <= cap_mul] or list(counts) + allowed.sort(key=lambda o: abs(_ORN_MUL[o] - desired)) + orn = random.choice(allowed[:3]) + pool = rounds[box['box_id']].get(orn) or [0.0] + eff = min(desired, cap_mul) + mul = sorted(pool, key=lambda m: (0 if m <= cap_mul else 1, abs(m - eff)))[0] + drawn += 1 + mul_sum += mul + balance += cm - mul + cumulative['drawn'] += 1 + cumulative['mul'] += mul + if drawn == fixed_rounds: + rtp_fixed = mul_sum / drawn / cm + # cycle 结束 → 按模板重填(无模板则 used 归零、count 不变) + tpl_counts = tpl.get((box['box_id'], audience)) + refilled = '模板' if tpl_counts else 'used归零' + if tpl_counts: + counts = {o: c for o, c in tpl_counts.items() if c > 0} + stats.append({ + 'cycle': cyc, + 'rtp_fixed': rtp_fixed, + 'rtp_end': mul_sum / drawn / cm, + 'balance_end': balance, + 'refill': refilled, + }) + return { + 'box_id': box['box_id'], 'box_type': box['box_type'], 'audience': audience, + 'rtp': rtp, 'cr': cr, 'n_total': n_total, 'fixed_rounds': fixed_rounds, + 'fixed_sum': fixed_sum, 'nominal': sum(c * _ORN_PAY.get(o, 0) for o, c in cfg.items()) / fixed_sum / cm, + 'cycles': stats, + 'cum_rtp': cumulative['mul'] / cumulative['drawn'] / cm, + } + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--box', type=int) + ap.add_argument('--audience', default='common') + ap.add_argument('--cycles', type=int, default=3) + ap.add_argument('--cr', type=float, default=None, help='覆盖收敛比例(测敏感度用,不写库)') + ap.add_argument('--seed', type=int, default=2026) + args = ap.parse_args() + random.seed(args.seed) + boxes, cfg, rounds, tpl = load_boxes(args.box) + print('身份=%s cycle 数=%d seed=%d 收敛比例=%s' % ( + args.audience, args.cycles, args.seed, + ('箱配置' if args.cr is None else args.cr))) + print('箱 模式 配比sum N 固定池 名义RTP 各cycle(固定池末→整轮末) 累计RTP') + rows = [] + for b in boxes: + c = cfg.get((b['box_id'], args.audience), {}) + if not c: + continue + r = simulate(b, c, rounds, tpl, args.audience, args.cycles, args.cr) + rows.append(r) + per = ' '.join('%.1f→%.1f' % (s['rtp_fixed'] * 100, s['rtp_end'] * 100) for s in r['cycles']) + flag = '✓' if abs(r['cum_rtp'] - r['rtp']) < 0.03 else '✗' + print('%2d %-7s %5d %4d %5d %6.1f%% %-34s %6.1f%% %s' % ( + r['box_id'], r['box_type'], r['fixed_sum'], r['n_total'], r['fixed_rounds'], + r['nominal'] * 100, per, r['cum_rtp'] * 100, flag)) + bad = [r for r in rows if abs(r['cum_rtp'] - r['rtp']) >= 0.03] + print('\n达标(±3个百分点): %d/%d' % (len(rows) - len(bad), len(rows))) + if bad: + print('未达标:', [(r['box_id'], round(r['cum_rtp'] * 100, 1), round(r['rtp'] * 100, 1)) for r in bad]) + if rows: + r = rows[0] + print('\n样例 box%d 逐 cycle 明细(%s):' % (r['box_id'], r['audience'])) + for s in r['cycles']: + print(' cycle%d: 固定池结束 %.1f%% 整轮结束 %.1f%% 抽完水位=%.0f 倍底注 重填=%s' % ( + s['cycle'], s['rtp_fixed'] * 100, s['rtp_end'] * 100, s['balance_end'], s['refill'])) + + +if __name__ == '__main__': + main() diff --git a/olympus-py/tests/__init__.py b/olympus-py/tests/__init__.py new file mode 100644 index 0000000..bc842e9 --- /dev/null +++ b/olympus-py/tests/__init__.py @@ -0,0 +1 @@ +"""Olympus tests.""" diff --git a/olympus-py/tests/test_engine.py b/olympus-py/tests/test_engine.py new file mode 100644 index 0000000..d5357a7 --- /dev/null +++ b/olympus-py/tests/test_engine.py @@ -0,0 +1,292 @@ +"""Olympus 引擎单元测试(unittest)。不依赖第三方库,可直接 `python tests/test_engine.py` 运行。""" + +from __future__ import annotations + +import sys +import unittest +from decimal import Decimal +from pathlib import Path + +# 支持直接 `python tests/test_engine.py` 运行(无需先安装为包) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from olympus import OlympusEngine, RuleViolation +from olympus.config import MODES, SYMBOL_RULES +from olympus.models import Bomb + + +class TestSymbolRule(unittest.TestCase): + """符号赔付校验:8/10/12 三档取值正确。""" + + def test_symbol_payout_tiers(self) -> None: + crown = SYMBOL_RULES["CROWN"] + self.assertEqual(crown.payout_for(7), Decimal("0")) + self.assertEqual(crown.payout_for(8), Decimal("10")) + self.assertEqual(crown.payout_for(9), Decimal("10")) + self.assertEqual(crown.payout_for(10), Decimal("25")) + self.assertEqual(crown.payout_for(11), Decimal("25")) + self.assertEqual(crown.payout_for(12), Decimal("50")) + self.assertEqual(crown.payout_for(30), Decimal("50")) + + +class TestSingleCascade(unittest.TestCase): + """单回合单次消除:无炸弹时有效倍数为 1。""" + + def test_one_cascade_no_bomb(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + engine.record_cascade(symbols={"BLUE_GEM": list(range(8))}, bombs=[]) + spin = engine.end_spin() + self.assertEqual(spin.base_win, Decimal("0.25")) + self.assertEqual(spin.effective_multiplier, 1) + self.assertAlmostEqual(float(spin.total_win), 0.25, places=2) + + def test_one_cascade_with_bomb(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + engine.record_cascade( + symbols={"CROWN": [0, 1, 2, 7, 8, 9, 14, 15, 16]}, # 9 个 → 10x + bombs=[Bomb("b1", 2)], + ) + spin = engine.end_spin() + self.assertEqual(spin.base_win, Decimal("10")) + self.assertEqual(spin.effective_multiplier, 2) + self.assertEqual(spin.total_win, Decimal("20")) + + +class TestMultiCascadeWithinSpin(unittest.TestCase): + """一回合多次消除:炸弹只在整回合结束后统一引爆,本回合中间不逐次乘算。""" + + def test_bomb_only_applies_after_all_cascades(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + c1 = engine.record_cascade( + symbols={"BLUE_GEM": list(range(8))}, bombs=[Bomb("m1", 3)] + ) + c2 = engine.record_cascade( + symbols={"GREEN_GEM": list(range(8, 16))}, bombs=[] + ) + # 级联阶段自身的 base_win 不应被乘数放大 + self.assertEqual(c1.base_win, Decimal("0.25")) + self.assertEqual(c2.base_win, Decimal("0.4")) + spin = engine.end_spin() + # 回合基础总赢额 = 两次消除之和(未乘算) + self.assertEqual(spin.base_win, Decimal("0.65")) + # 炸弹在回合结束后才统一生效 + self.assertEqual(spin.effective_multiplier, 3) + self.assertEqual(spin.total_win, Decimal("1.95")) + + def test_symbols_are_accumulated_across_cascades(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + engine.record_cascade(symbols={"BLUE_GEM": list(range(8))}, bombs=[Bomb("m1", 3)]) + c2 = engine.record_cascade(symbols={"BLUE_GEM": list(range(8, 16))}, bombs=[]) + # 同一符号在本回合内跨级联累计记录(8 + 8 = 16) + self.assertEqual(c2.accumulated_symbols.get("BLUE_GEM"), 16) + spin = engine.end_spin() + self.assertEqual(spin.accumulated_symbols.get("BLUE_GEM"), 16) + + +class TestCrossSpinAccumulator(unittest.TestCase): + """炸弹累计器:仅免费旋转生效;基础旋转炸弹只乘本旋、不留存。""" + + def test_base_spin_does_not_accumulate(self) -> None: + """基础旋转:炸弹乘本旋赢额,但不进累计器。""" + engine = OlympusEngine(bet="1") + engine.begin_spin() # 默认基础旋转 + engine.record_cascade(symbols={"RED_GEM": list(range(8))}, bombs=[Bomb("m1", 5)]) + s1 = engine.end_spin() + self.assertEqual(s1.effective_multiplier, 5) # 本旋生效 + self.assertEqual(s1.total_win, Decimal("5")) # 1 * 5 + self.assertEqual(s1.multiplier_after, 0) # 不留存 + # 第二次基础旋转:只吃自己的炸弹 + engine.begin_spin() + engine.record_cascade(symbols={"PURPLE_GEM": list(range(8, 16))}, bombs=[Bomb("m2", 3)]) + s2 = engine.end_spin() + self.assertEqual(s2.multiplier_before, 0) + self.assertEqual(s2.effective_multiplier, 3) + self.assertEqual(s2.total_win, Decimal("2.4")) # 0.8 * 3 + + def test_accumulator_carries_across_free_spins(self) -> None: + """免费旋转:炸弹并入累计器并跨旋传递。""" + engine = OlympusEngine(bet="1") + engine.begin_spin(is_free_spin=True) + engine.record_cascade(symbols={"RED_GEM": list(range(8))}, bombs=[Bomb("m1", 5)]) + s1 = engine.end_spin() + self.assertEqual(s1.multiplier_before, 0) + self.assertEqual(s1.multiplier_after, 5) + self.assertEqual(s1.total_win, Decimal("5")) # 1 * 5 + + engine.begin_spin(is_free_spin=True) + engine.record_cascade(symbols={"PURPLE_GEM": list(range(8, 16))}, bombs=[Bomb("m2", 3)]) + s2 = engine.end_spin() + self.assertEqual(s2.multiplier_before, 5) + self.assertEqual(s2.multiplier_after, 8) + self.assertEqual(s2.total_win, Decimal("6.4")) # PURPLE_GEM 8个=0.8x, 0.8 * 8 = 6.4 + + def test_fs_spin_without_win_keeps_accumulator_unchanged(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin(is_free_spin=True) + engine.record_cascade(symbols={"RED_GEM": list(range(8))}, bombs=[Bomb("m1", 5)]) + engine.end_spin() + + # 第二次 FS 旋转没有任何消除(即使传入炸弹也无效,不计入累计) + engine.begin_spin(is_free_spin=True) + s2 = engine.end_spin() + self.assertEqual(s2.base_win, Decimal("0")) + self.assertEqual(s2.effective_multiplier, 0) + self.assertEqual(s2.multiplier_after, 5) # 累计值保持不变 + + def test_bomb_discarded_when_cascade_has_no_win(self) -> None: + """无消除的旋转即使出现炸弹,也不应计入累计(消除是炸弹生效的前提)。""" + engine = OlympusEngine(bet="1") + engine.begin_spin(is_free_spin=True) + engine.record_cascade(symbols={}, bombs=[Bomb("m1", 10)]) + spin = engine.end_spin() + self.assertEqual(spin.base_win, Decimal("0")) + self.assertEqual(spin.multiplier_after, 0) + + +class TestStateGuards(unittest.TestCase): + """状态机误用与非法输入的防御性校验。""" + + def test_non_positive_bet_raises(self) -> None: + with self.assertRaises(RuleViolation): + OlympusEngine(bet="0") + + def test_record_cascade_without_begin_raises(self) -> None: + engine = OlympusEngine(bet="1") + with self.assertRaises(RuleViolation): + engine.record_cascade(symbols={"CROWN": list(range(8))}) + + def test_begin_spin_twice_raises(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + with self.assertRaises(RuleViolation): + engine.begin_spin() + + def test_invalid_symbol_raises(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + with self.assertRaises(RuleViolation): + engine.record_cascade(symbols={"UNKNOWN": list(range(8))}) + + def test_below_min_win_count_raises(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + with self.assertRaises(RuleViolation): + engine.record_cascade(symbols={"CROWN": list(range(7))}) + + def test_duplicate_positions_raises(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + with self.assertRaises(RuleViolation): + engine.record_cascade(symbols={"CROWN": [0, 0, 1, 2, 3, 4, 5, 6]}) + + def test_invalid_bomb_value_raises(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + with self.assertRaises(RuleViolation): + engine.record_cascade(symbols={"CROWN": list(range(8))}, bombs=[Bomb("m1", 7)]) + + def test_inconsistent_bomb_redefinition_raises(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + engine.record_cascade(symbols={"CROWN": list(range(8))}, bombs=[Bomb("m1", 5)]) + with self.assertRaises(RuleViolation): + engine.record_cascade(symbols={}, bombs=[Bomb("m1", 10)]) + + def test_reset_session_while_spin_active_raises(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + with self.assertRaises(RuleViolation): + engine.reset_session() + + +class TestGameModes(unittest.TestCase): + """模式互斥与下注档位。""" + + def test_mode_bet_tiers_and_prices(self) -> None: + # 普通:1 3 5 10 20 30 50 100 200 300(扣费=下注) + normal = MODES["normal"] + self.assertEqual(normal.base_bets, (1, 3, 5, 10, 20, 30, 50, 100, 200, 300)) + self.assertEqual(normal.price_for(20), Decimal("20")) + # 底注:扣费 = 2 6 10 20 40 60 100 200 400 600 + ante = MODES["ante"] + self.assertEqual( + [int(ante.price_for(b)) for b in ante.base_bets], + [2, 6, 10, 20, 40, 60, 100, 200, 400, 600], + ) + # 购买免费旋转:扣费 = 50 150 250 500 1000 1500 2500 5000 10000 15000(10 档,50 倍) + buy = MODES["buy_fs"] + self.assertEqual( + [int(buy.price_for(b)) for b in buy.base_bets], + [50, 150, 250, 500, 1000, 1500, 2500, 5000, 10000, 15000], + ) + + def test_mode_mutual_exclusion(self) -> None: + # 底注模式禁止购买免费旋转 + ante_engine = OlympusEngine(bet="1", mode="ante") + self.assertFalse(ante_engine.buy_free_spins_allowed()) + self.assertTrue(ante_engine.ante_boost_active()) + # 购买免费旋转模式禁用底注效果 + buy_engine = OlympusEngine(bet="1", mode="buy_fs") + self.assertFalse(buy_engine.ante_boost_active()) + self.assertFalse(buy_engine.buy_free_spins_allowed()) + self.assertTrue(buy_engine.mode.guaranteed_free_spins) + # 普通模式允许购买 + normal_engine = OlympusEngine(bet="1", mode="normal") + self.assertTrue(normal_engine.buy_free_spins_allowed()) + self.assertFalse(normal_engine.ante_boost_active()) + + def test_engine_cost_uses_mode_multiplier(self) -> None: + self.assertEqual(OlympusEngine(bet="20", mode="normal").cost, Decimal("20")) + self.assertEqual(OlympusEngine(bet="20", mode="ante").cost, Decimal("40")) + self.assertEqual(OlympusEngine(bet="20", mode="buy_fs").cost, Decimal("1000")) + + def test_invalid_mode_raises(self) -> None: + with self.assertRaises(RuleViolation): + OlympusEngine(bet="1", mode="buy_super_fs") + + def test_bet_not_in_mode_tiers_raises(self) -> None: + # normal 没有 7 档 + with self.assertRaises(RuleViolation): + OlympusEngine(bet="7", mode="normal") + # buy_fs 没有 40 档 + with self.assertRaises(RuleViolation): + OlympusEngine(bet="40", mode="buy_fs") + + +class TestMaxWinCap(unittest.TestCase): + """单局赢奖封顶 1000 倍基础下注。""" + + def test_win_capped_at_1000x(self) -> None: + engine = OlympusEngine(bet="1") + engine.begin_spin() + # CROWN 12个=50x,配 1000 倍炸弹 → 未封顶前 50000x + engine.record_cascade( + symbols={"CROWN": list(range(12))}, bombs=[Bomb("m1", 1000)] + ) + spin = engine.end_spin() + self.assertTrue(spin.win_capped) + self.assertEqual(spin.total_win, Decimal("1000")) + self.assertEqual(engine.state.total_win, Decimal("1000")) + + def test_cap_applies_to_session_cumulative(self) -> None: + engine = OlympusEngine(bet="1") + # 第 1 次 FS 旋转拿 900x(基础 10x × 炸弹 90),累计器 → 90 + engine.begin_spin(is_free_spin=True) + engine.record_cascade(symbols={"CROWN": list(range(8))}, bombs=[Bomb("m1", 50), Bomb("m2", 25), Bomb("m3", 15)]) + s1 = engine.end_spin() + self.assertEqual(s1.total_win, Decimal("900")) + self.assertFalse(s1.win_capped) + # 第 2 次 FS 旋转再赢(无新炸弹,吃累计 90)→ 只能再拿 100x,整局顶格 1000x + engine.begin_spin(is_free_spin=True) + engine.record_cascade(symbols={"CROWN": list(range(8))}, bombs=[]) + s2 = engine.end_spin() + self.assertTrue(s2.win_capped) + self.assertEqual(engine.state.total_win, Decimal("1000")) + + +if __name__ == "__main__": + unittest.main() diff --git a/olympus-py/tests/test_simulator.py b/olympus-py/tests/test_simulator.py new file mode 100644 index 0000000..3eeb1e8 --- /dev/null +++ b/olympus-py/tests/test_simulator.py @@ -0,0 +1,263 @@ +"""回合生成器规则校验(unittest)。不依赖第三方库,可直接 `python tests/test_simulator.py` 运行。""" + +from __future__ import annotations + +import json +import random +import sys +import unittest +from collections import Counter +from pathlib import Path + +# 支持直接 `python tests/test_simulator.py` 运行(无需先安装为包) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from olympus.config import BIGWIN_LEVELS, BIGWIN_TIERS, BOMB_SHORT, SCATTER_SHORT +import olympus_simulator as sim +from olympus_simulator import generate_round + + +def _cells(step): + return [c for row in step['grid_state'] for c in row] + + +class TestBuyFsTriggerSpin(unittest.TestCase): + """buy_fs 触发盘:首盘固定 4 个 SCATTER,且整盘不出炸弹(炸弹只在免费旋转内生效)。""" + + def test_trigger_pages(self) -> None: + for _ in range(100): + _, steps, _ = generate_round({'box_id': 0, 'box_type': 'buy_fs'}) + trigger = [s for s in steps if s['free_spin_id'] == 0] + self.assertTrue(trigger) + for s in trigger: + cells = _cells(s) + self.assertEqual(cells.count(SCATTER_SHORT), 4) + self.assertEqual(cells.count(BOMB_SHORT), 0) + + +class TestBuyFsFreeSpinHits(unittest.TestCase): + """buy_fs 免费旋转"每旋保底消除":自然阶段命中率应远高于旧实现的 ~13%(旧版 45% 强制空转)。""" + + def test_natural_phase_hit_rate(self) -> None: + random.seed(12345) + box = {'box_id': 0, 'box_type': 'buy_fs'} + total = hit = 0 + for _ in range(60): + _, steps, _ = generate_round(box, None) + spins = Counter(s['free_spin_id'] for s in steps if s['free_spin_id'] > 0) + total += len(spins) + hit += sum(1 for v in spins.values() if v > 1) + self.assertGreater(hit / total, 0.45) + + +class TestBombOnlyInFreeSpins(unittest.TestCase): + """炸弹只存在于免费旋转内: + · normal/ante 的基础旋(含进 FS 的触发盘、含补桶植入)一律不出炸弹; + · 三个模式一旦进 FS 规则一致(炸弹 + 累计器),normal/ante 靠它演 100x/150x/200x 桶。""" + + def test_normal_ante_base_spin_never_emit_bomb(self) -> None: + random.seed(20260822) + for mode in ('normal', 'ante'): + box = {'box_id': 0, 'box_type': mode} + bombs = 0 + for bias in (None, 'zero', 0.5, 10.0, 150.0): + for _ in range(400): + _, steps, _ = generate_round(box, bias) + bombs += sum(_cells(s).count(BOMB_SHORT) + for s in steps if s['free_spin_id'] == 0) + self.assertEqual(bombs, 0, '%s 基础旋不该出炸弹' % mode) + + def test_normal_ante_free_spins_have_bomb(self) -> None: + random.seed(11) + for mode in ('normal', 'ante'): + box = {'box_id': 0, 'box_type': mode} + grids = bombs = 0 + for _ in range(400): + # 目标 150x ≥ FS_ONLY_TARGET_MUL → 该模式的 150x 桶只能靠自然触发 FS 的回合来装 + _, steps, _ = generate_round(box, 150.0) + for s in steps: + if s['free_spin_id'] > 0: + grids += 1 + bombs += _cells(s).count(BOMB_SHORT) + self.assertGreater(grids, 0, '%s 高倍桶应该是 FS 回合' % mode) + self.assertGreater(bombs / (30.0 * grids), 0.02, + '%s 的 FS 盘面应该有炸弹' % mode) + + def test_high_target_round_enters_free_spins(self) -> None: + """目标高到基础旋赔不出来(≥80x)时,生成器必须改用自然触发 FS 的回合。""" + for mode in ('normal', 'ante'): + _, steps, _ = generate_round({'box_id': 0, 'box_type': mode}, 150.0) + self.assertTrue(any(s['free_spin_id'] > 0 for s in steps)) + trigger = [s for s in steps if s['free_spin_id'] == 0][-1] + self.assertGreaterEqual(_cells(trigger).count(SCATTER_SHORT), 4) + self.assertEqual(_cells(trigger).count(BOMB_SHORT), 0) + + def test_buy_fs_still_has_bomb_in_free_spins(self) -> None: + random.seed(7) + box = {'box_id': 0, 'box_type': 'buy_fs'} + bombs = 0 + for _ in range(200): + _, steps, _ = generate_round(box, None) + bombs += sum(_cells(s).count(BOMB_SHORT) for s in steps if s['free_spin_id'] > 0) + self.assertGreater(bombs, 0) + + +class TestBigWinLevels(unittest.TestCase): + """爆奖动画只有小/中/大 3 档,而且只在免费旋转里判档: + · 基础旋(含触发盘,`free_spin_id=0`)恒为 null —— 这两个模式的普通旋转不播大奖动画; + · FS 旋的档位 = 该旋末帧 winAmount 与新阈值(两种情况两套)比对的结果。""" + + def test_tiers_are_three_and_two_cases(self) -> None: + self.assertEqual(BIGWIN_LEVELS, ('small', 'middle', 'big')) + self.assertEqual(BIGWIN_TIERS['normal'], BIGWIN_TIERS['ante']) # 自然 FS 共用一套 + self.assertNotEqual(BIGWIN_TIERS['normal'], BIGWIN_TIERS['buy_fs']) + for tiers in BIGWIN_TIERS.values(): + self.assertEqual(len(tiers), len(BIGWIN_LEVELS)) + + def test_only_free_spin_final_frames_get_level(self) -> None: + random.seed(4242) + cases = (('normal', None), ('ante', None), ('buy_fs', None), + ('normal', 150.0), ('ante', 200.0)) + for mode, bias in cases: + box = {'box_id': 0, 'box_type': mode} + for _ in range(120): + _, steps, _ = generate_round(box, bias) + for s in steps: + if s['free_spin_id'] == 0 or not s['is_final']: + self.assertIsNone(s['bigwin_level'], + '%s 基础旋/非末帧不该有爆奖档' % mode) + continue + want = None + for name, threshold in zip(BIGWIN_LEVELS, BIGWIN_TIERS[mode]): + if float(s['win_amount']) >= threshold: + want = name + self.assertEqual(s['bigwin_level'], want) + + +class TestFreeSpinFloorSpread(unittest.TestCase): + """保底消除"随机间隔":既不能出现长空尾,也不能变成固定节奏。 + + 口径:dry = 该旋所有 cascade 步 `base_score` 全 0(scatter 赔付不算消除,S 不参与消除)。 + 两版历史(都别退回去): + · 最初按"剩余预算÷剩余旋数"算概率、扣预算扣的是被累计倍数放大后的赢额 → 买 FS 87.9% 的局 + ≥4 旋连空、51.7% 的局尾巴一直空转、单局最长 29 旋; + · 修成"等距铺"后虽然没长空尾,但 87% 的局"最长连空正好 4 旋",玩家几局就看穿"到第 4 旋肯定消"。""" + + @staticmethod + def _max_dry(steps) -> int: + per = {} + for s in steps: + if s['free_spin_id'] > 0: + per[s['free_spin_id']] = per.get(s['free_spin_id'], 0.0) + float(s['base_score']) + run = mx = 0 + for k in sorted(per): + run = 0 if per[k] > 0 else run + 1 + mx = max(mx, run) + return mx + + def test_dry_run_stays_short(self) -> None: + random.seed(20260916) + for bias in (25.0, None): + runs = [self._max_dry(generate_round({'box_id': 0, 'box_type': 'buy_fs'}, bias)[1]) + for _ in range(80)] + avg, worst = sum(runs) / len(runs), max(runs) + self.assertLess(avg, 7, 'bias=%s 平均最长连空 %.1f 太长(旧实现 9.6)' % (bias, avg)) + self.assertLessEqual(worst, 9, 'bias=%s 出现 %d 旋死尾巴(旧实现最长 29)' % (bias, worst)) + + def test_gap_is_not_a_fixed_cadence(self) -> None: + """最长连空的取值必须散开:等距版 87% 的局都卡在同一个值。""" + random.seed(4242) + runs = [self._max_dry(generate_round({'box_id': 0, 'box_type': 'buy_fs'}, 25.0)[1]) + for _ in range(120)] + top, cnt = Counter(runs).most_common(1)[0] + self.assertGreaterEqual(len(set(runs)), 3, '最长连空只有 %s,节奏太固定' % sorted(set(runs))) + self.assertLess(cnt / len(runs), 0.7, '最长连空过于集中:%.0f%% 都是 %d 旋' % (100 * cnt / len(runs), top)) + + +class TestTumbleGeometry(unittest.TestCase): + """每一步都要能按"消掉上一步的连线 → 重力下落 → 补位"复刻出 `grid_state`: + 前端据此播动画,任何一格对不上/漏掉都会表现成画错符号或空白格子。""" + + def _check_round(self, steps) -> None: + rows, cols = 5, 6 + for i in range(1, len(steps)): + prev, cur = steps[i - 1], steps[i] + drops = json.loads(cur['drop_data'] or '[]') + fills = json.loads(cur['fill_data'] or '[]') + if prev['free_spin_id'] != cur['free_spin_id']: + # 新的一旋:整盘重发,不该带下落/补位 + self.assertEqual(drops, []) + self.assertEqual(fills, []) + continue + links = json.loads(prev['symbol_links'] or '[]') + removed = {p - 1 for l in links if l.get('tumble') == prev['cascade_seq'] + for p in l['loc']} + old = prev['grid_state'] + board = [['' if (r * cols + c) in removed else old[r][c] for c in range(cols)] + for r in range(rows)] + nb = [[''] * cols for _ in range(rows)] + exp_drops = [] + for c in range(cols): + surv = [(r, board[r][c]) for r in range(rows - 1, -1, -1) if board[r][c] != ''] + for k, (r, sym) in enumerate(surv): + nr = rows - 1 - k + nb[nr][c] = sym + if nr != r: + exp_drops.append({'from': r * cols + c + 1, 'to': nr * cols + c + 1, + 'symbol': sym}) + key = lambda d: (d['from'], d['to']) + self.assertEqual(sorted(exp_drops, key=key), sorted(drops, key=key)) + # 下落目标的 symbol 必须是"这一波结束后"的符号(= grid_state 同位置) + for d in drops: + p = d['to'] - 1 + self.assertEqual(d['symbol'], cur['grid_state'][p // cols][p % cols]) + # 补位必须正好盖住所有空位,且符号与最终盘面一致 + empty = sorted(r * cols + c for c in range(cols) for r in range(rows) + if nb[r][c] == '') + self.assertEqual(sorted(f['pos'] - 1 for f in fills), empty) + for f in fills: + p = f['pos'] - 1 + self.assertEqual(f['symbol'], cur['grid_state'][p // cols][p % cols]) + nb[p // cols][p % cols] = f['symbol'] + self.assertEqual(nb, cur['grid_state'], '复刻盘面 ≠ grid_state') + + def test_all_modes(self) -> None: + random.seed(31337) + for mode, biases in (('normal', (None, 'zero', 20.0, 150.0)), + ('ante', (None, 100.0, 200.0)), + ('buy_fs', (None, 25.0, 500.0))): + for bias in biases: + for _ in range(60): + _, steps, _ = generate_round({'box_id': 0, 'box_type': mode}, bias) + self._check_round(steps) + + +class TestFreeSpinCap(unittest.TestCase): + """30 旋上限(15 + 5×3):到上限之后不得再落 3+ 个 S。 + + 旧实现只在"加旋"那行有闸门,写 retrigger_info 的那行没有 —— 到 30 旋后照样摇出 3+ 个 S、 + 照样写 extraSpins=5,玩家白看 "+5" 提示(全库 650 个这种帧)。这里用"每旋必落 5 个 S"的 + 极端分布把上限逼出来:应恰好 30 旋、恰好 3 次重触发,上限之后的旋 S 数 ≤2。 + """ + + def test_no_retrigger_beyond_cap(self) -> None: + random.seed(20260919) + original = sim.FREESPIN_SCATTER + sim.FREESPIN_SCATTER = {'default': {5: 1.0}, 'buy_fs': {5: 1.0}} + try: + for mode in ('normal', 'ante', 'buy_fs'): + steps, _ = sim._free_spin_session(mode) + spins = {s['free_spin_id'] for s in steps} + retrigger = [s for s in steps if s.get('retrigger_info')] + self.assertEqual(len(spins), 30, f'{mode}: 旋数应为 30,实际 {len(spins)}') + self.assertEqual(len(retrigger), 3, f'{mode}: 重触发应为 3 次,实际 {len(retrigger)}') + for s in steps: + if s['is_final'] and s['free_spin_id'] > 3: + self.assertLessEqual(_cells(s).count(SCATTER_SHORT), 2, + f'{mode}: 上限之后(第 {s["free_spin_id"]} 旋)又落了 3+ 个 S') + finally: + sim.FREESPIN_SCATTER = original + + +if __name__ == '__main__': + unittest.main() diff --git a/package.sh b/package.sh new file mode 100644 index 0000000..5791f9f --- /dev/null +++ b/package.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +# ========== 配置项 ========== +JAVA_HOME="/project/jdk-21.0.10" +MAVEN_PATH="/project/apache-maven-3.6.3/bin/mvn" +# =========================== + +export JAVA_HOME="$JAVA_HOME" +export PATH="$JAVA_HOME/bin:$PATH" + +echo "--- 环境检查 ---" +java -version 2>&1 | head -1 +$MAVEN_PATH -version 2>&1 | head -1 +echo "----------------" + +git pull && $MAVEN_PATH clean && $MAVEN_PATH package -Dmaven.test.skip=true +cp ./ruoyi-admin/target/ruoyi-admin.jar ./ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..235899c --- /dev/null +++ b/pom.xml @@ -0,0 +1,336 @@ + + + 4.0.0 + + com.ruoyi + ruoyi + 4.8.2 + + ruoyi + http://www.ruoyi.vip + 若依管理系统 + + + 4.8.2 + UTF-8 + UTF-8 + 21 + 3.1.1 + 4.0.3 + 2.1.0 + 4.0.1 + 2.1.0 + 1.2.28 + 8.1.0 + 1.21 + 3.0.0 + 2.3.3 + 1.5.5 + 2.1.1 + 2.0.53 + 6.10.0 + 2.21.0 + 3.2.2 + 4.1.2 + 2.3 + 0.9.1 + 3.0.2 + + + + + + + io.micrometer + micrometer-bom + 1.16.3 + pom + import + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + com.alibaba + druid-spring-boot-4-starter + ${druid.version} + + + + + pro.fessional + kaptcha + ${kaptcha.version} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis-spring-boot.version} + + + + + nl.basjes.parse.useragent + yauaa + ${yauaa.version} + + + eu.bitwalker + UserAgentUtils + ${bitwalker.version} + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper.boot.version} + + + + + com.github.oshi + oshi-core + ${oshi.version} + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + + + io.springfox + springfox-boot-starter + ${swagger.version} + + + + + + + + + + + commons-io + commons-io + ${commons.io.version} + + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + + + commons-collections + commons-collections + ${commons.collections.version} + + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson.version} + + + + + io.jsonwebtoken + jjwt + ${jwt.version} + + + + + pro.fessional + kaptcha + ${kaptcha.version} + + + + + cloud.tianai.captcha + tianai-captcha-springboot-starter + ${tianai-captcha.version} + + + + + com.ruoyi + ruoyi-quartz + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-generator + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-framework + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-system + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-common + ${ruoyi.version} + + + + com.ruoyi + skins-service + 4.8.2 + + + + + + + ruoyi-admin + ruoyi-framework + ruoyi-system + ruoyi-quartz + ruoyi-generator + ruoyi-common + skins-model + skins-service + skins-promo + + pom + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + true + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + + + + public + aliyun nexus + https://maven.aliyun.com/repository/public + + true + + + + + + + public + aliyun nexus + https://maven.aliyun.com/repository/public + + true + + + false + + + + + \ No newline at end of file diff --git a/promo-ui2/.browserslistrc b/promo-ui2/.browserslistrc new file mode 100644 index 0000000..e7c12fd --- /dev/null +++ b/promo-ui2/.browserslistrc @@ -0,0 +1,5 @@ +Android 4.1 +IOS 7.1 +Chrome > 31 +ff > 31 +ie >= 11 \ No newline at end of file diff --git a/promo-ui2/.env.base b/promo-ui2/.env.base new file mode 100644 index 0000000..40b2645 --- /dev/null +++ b/promo-ui2/.env.base @@ -0,0 +1,28 @@ +# 环境 +VITE_NODE_ENV=development + +# 接口前缀 +VITE_API_BASE_PATH=http://localhost:28081 + +# 打包路径 +VITE_BASE_PATH=/ + +# 标题 +VITE_APP_TITLE=推广后台 + +# 是否全量引入element-plus样式 +VITE_USE_ALL_ELEMENT_PLUS_STYLE=true + +# 是否开启mock +VITE_USE_MOCK=true + +# 是否使用在线图标 +VITE_USE_ONLINE_ICON=true + +# 是否隐藏全局设置按钮 +VITE_HIDE_GLOBAL_SETTING=true + +# ==================== AES 加密开关 ==================== +# 注意:pnpm run dev 使用 --mode base,实际加载的是本文件! +# false=关闭(方便调试),true=开启(需与后端 aes.enabled 保持一致) +VITE_USE_AES_ENCRYPT=false diff --git a/promo-ui2/.env.dev b/promo-ui2/.env.dev new file mode 100644 index 0000000..9a03b60 --- /dev/null +++ b/promo-ui2/.env.dev @@ -0,0 +1,45 @@ +# 环境 +VITE_NODE_ENV=production + +# 接口前缀 +VITE_API_BASE_PATH= + +# 打包路径 +VITE_BASE_PATH=/dist-dev/ + +# 是否删除debugger +VITE_DROP_DEBUGGER=false + +# 是否删除console.log +VITE_DROP_CONSOLE=false + +# 是否sourcemap +VITE_SOURCEMAP=true + +# 输出路径 +VITE_OUT_DIR=dist-dev + +# 标题 +VITE_APP_TITLE=ElementAdmin + +# 是否包分析 +VITE_USE_BUNDLE_ANALYZER=false + +# 是否全量引入element-plus样式 +VITE_USE_ALL_ELEMENT_PLUS_STYLE=false + +# 是否开启mock +VITE_USE_MOCK=true + +# 是否切割css +VITE_USE_CSS_SPLIT=true + +# 是否使用在线图标 +VITE_USE_ONLINE_ICON=true + +# 是否隐藏全局设置按钮 +VITE_HIDE_GLOBAL_SETTING=true + +# ==================== AES 加密开关 ==================== +# false=关闭(方便开发调试),true=开启(生产环境必须) +VITE_USE_AES_ENCRYPT=false diff --git a/promo-ui2/.env.gitee b/promo-ui2/.env.gitee new file mode 100644 index 0000000..5536574 --- /dev/null +++ b/promo-ui2/.env.gitee @@ -0,0 +1,41 @@ +# 环境 +VITE_NODE_ENV=production + +# 接口前缀 +VITE_API_BASE_PATH= + +# 打包路径 +VITE_BASE_PATH=/vue-element-plus-admin/ + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 输出路径 +VITE_OUT_DIR=dist-pro + +# 标题 +VITE_APP_TITLE=ElementAdmin + +# 是否包分析 +VITE_USE_BUNDLE_ANALYZER=false + +# 是否全量引入element-plus样式 +VITE_USE_ALL_ELEMENT_PLUS_STYLE=false + +# 是否开启mock +VITE_USE_MOCK=true + +# 是否切割css +VITE_USE_CSS_SPLIT=true + +# 是否使用在线图标 +VITE_USE_ONLINE_ICON=true + +# 是否隐藏全局设置按钮 +VITE_HIDE_GLOBAL_SETTING=false diff --git a/promo-ui2/.env.pro b/promo-ui2/.env.pro new file mode 100644 index 0000000..f23a5ee --- /dev/null +++ b/promo-ui2/.env.pro @@ -0,0 +1,45 @@ +# 环境 +VITE_NODE_ENV=production + +# 接口前缀 +VITE_API_BASE_PATH=http://103.108.67.60:28081 + +# 打包路径 +VITE_BASE_PATH=/ + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 输出路径 +VITE_OUT_DIR=dist-pro + +# 标题 +VITE_APP_TITLE=推广后台 + +# 是否包分析 +VITE_USE_BUNDLE_ANALYZER=true + +# 是否全量引入element-plus样式 +VITE_USE_ALL_ELEMENT_PLUS_STYLE=false + +# 是否开启mock +VITE_USE_MOCK=false + +# 是否切割css +VITE_USE_CSS_SPLIT=true + +# 是否使用在线图标 +VITE_USE_ONLINE_ICON=true + +# 是否隐藏全局设置按钮 +VITE_HIDE_GLOBAL_SETTING=true + +# ==================== AES 加密开关 ==================== +# 注意:必须与后端 application-pro.yml 的 aes.enabled 保持一致! +VITE_USE_AES_ENCRYPT=false diff --git a/promo-ui2/.env.test b/promo-ui2/.env.test new file mode 100644 index 0000000..2b9bcc0 --- /dev/null +++ b/promo-ui2/.env.test @@ -0,0 +1,45 @@ +# 环境 +VITE_NODE_ENV=production + +# 接口前缀 +VITE_API_BASE_PATH=http://8.160.181.6:8081 + +# 打包路径 +VITE_BASE_PATH=/ + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 输出路径 +VITE_OUT_DIR=dist-pro + +# 标题 +VITE_APP_TITLE=推广后台 + +# 是否包分析 +VITE_USE_BUNDLE_ANALYZER=true + +# 是否全量引入element-plus样式 +VITE_USE_ALL_ELEMENT_PLUS_STYLE=false + +# 是否开启mock +VITE_USE_MOCK=false + +# 是否切割css +VITE_USE_CSS_SPLIT=true + +# 是否使用在线图标 +VITE_USE_ONLINE_ICON=false + +# 是否隐藏全局设置按钮 +VITE_HIDE_GLOBAL_SETTING=true + +# ==================== AES 加密开关 ==================== +# 测试环境建议关闭,方便排查问题 +VITE_USE_AES_ENCRYPT=false diff --git a/promo-ui2/.github/workflows/auto-merge.yml b/promo-ui2/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..f9cd3d7 --- /dev/null +++ b/promo-ui2/.github/workflows/auto-merge.yml @@ -0,0 +1,131 @@ +name: Automerge + +on: + pull_request: + types: + - labeled + - unlabeled + - synchronize + - opened + - edited + - ready_for_review + - reopened + - unlocked + pull_request_review: + types: + - submitted + status: {} + +jobs: + # 合并发布版本的 pr 到 master + auto-merge: + runs-on: ubuntu-latest + steps: + - name: Automerge + uses: 'pascalgn/automerge-action@v0.14.3' + env: + BASE_BRANCHES: 'release' + GITHUB_TOKEN: '${{ secrets.TOKEN }}' + MERGE_LABELS: '' + MERGE_FILTER_AUTHOR: 'kailong321200875' + + push-to-gh-pages: + needs: [auto-merge] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Pnpm + uses: pnpm/action-setup@v2 + with: + version: latest + + - name: use Node.js 18 + uses: actions/setup-node@v2.1.2 + with: + node-version: '18.x' + + - name: Set SSH Environment + env: + DOCS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }} + run: | + mkdir -p ~/.ssh/ + echo "$ACTIONS_DEPLOY_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan github.com > ~/.ssh/known_hosts + chmod 700 ~/.ssh && chmod 600 ~/.ssh/* + git config --local user.email "321200875@qq.com" + git config --local user.name "kailong321200875" + + # 发布到 github + - name: Build Github + run: | + pnpm install --no-frozen-lockfile + pnpm run build:pro + + - name: Deploy Github + uses: peaceiris/actions-gh-pages@v3 + with: + deploy_key: ${{secrets.ACTIONS_DEPLOY_KEY}} + publish_branch: gh-pages + publish_dir: ./dist-pro + force_orphan: true + cname: element-plus-admin.cn + + push-to-gh-pages-gitee: + needs: [auto-merge] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Pnpm + uses: pnpm/action-setup@v2 + with: + version: latest + + - name: use Node.js 18 + uses: actions/setup-node@v2.1.2 + with: + node-version: '18.x' + + - name: Set SSH Environment + env: + DOCS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }} + run: | + mkdir -p ~/.ssh/ + echo "$ACTIONS_DEPLOY_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan github.com > ~/.ssh/known_hosts + chmod 700 ~/.ssh && chmod 600 ~/.ssh/* + git config --local user.email "321200875@qq.com" + git config --local user.name "kailong321200875" + + - name: Build Gitee + run: | + pnpm install --no-frozen-lockfile + pnpm run build:gitee + + # 发布到 gitee + - name: Deploy Gitee + uses: peaceiris/actions-gh-pages@v3 + with: + deploy_key: ${{secrets.ACTIONS_DEPLOY_KEY}} + publish_branch: gh-pages-gitee + publish_dir: ./dist-pro + force_orphan: true + + - name: Sync Github Repos To Gitee # 名字随便起 + uses: Yikun/hub-mirror-action@v1.1 # 使用Yikun/hub-mirror-action + with: + src: github/kailong321200875 # 源端账户名(github) + dst: gitee/kailong110120130 # 目的端账户名(gitee) + dst_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} # SSH密钥对中的私钥 + dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌 + account_type: user # 账户类型 + clone_style: 'https' # 使用https方式进行clone,也可以使用ssh + debug: true # 启用后会显示所有执行命令 + force_update: true # 启用后,强制同步,即强制覆盖目的端仓库 + static_list: 'vue-element-plus-admin' # 静态同步列表,在此填写需要同步的仓库名称,可填写多个 + timeout: '600s' # git超时设置,超时后会自动重试git操作 diff --git a/promo-ui2/.github/workflows/release.yml b/promo-ui2/.github/workflows/release.yml new file mode 100644 index 0000000..2c11444 --- /dev/null +++ b/promo-ui2/.github/workflows/release.yml @@ -0,0 +1,19 @@ +on: + push: + branches: + - release + +name: Release + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/release-please-action@v3 + id: release + with: + token: ${{ secrets.TOKEN }} + default-branch: release + release-type: node + package-name: standard-version + changelog-types: '[{"type": "types", "section":"Types", "hidden": false},{"type": "revert", "section":"Reverts", "hidden": false},{"type": "feat", "section": "Features", "hidden": false},{"type": "fix", "section": "Bug Fixes", "hidden": false},{"type": "improvement", "section": "Feature Improvements", "hidden": false},{"type": "docs", "section":"Docs", "hidden": false},{"type": "style", "section":"Styling", "hidden": false},{"type": "refactor", "section":"Code Refactoring", "hidden": false},{"type": "perf", "section":"Performance Improvements", "hidden": false},{"type": "test", "section":"Tests", "hidden": false},{"type": "build", "section":"Build System", "hidden": false},{"type": "ci", "section":"CI", "hidden":false}]' diff --git a/promo-ui2/.gitignore b/promo-ui2/.gitignore new file mode 100644 index 0000000..2c274c8 --- /dev/null +++ b/promo-ui2/.gitignore @@ -0,0 +1,9 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +/dist* +pnpm-debug +stats.html +.idea diff --git a/promo-ui2/.husky/commit-msg b/promo-ui2/.husky/commit-msg new file mode 100644 index 0000000..34eed8b --- /dev/null +++ b/promo-ui2/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit $1 \ No newline at end of file diff --git a/promo-ui2/.husky/lintstagedrc.cjs b/promo-ui2/.husky/lintstagedrc.cjs new file mode 100644 index 0000000..fd96fcb --- /dev/null +++ b/promo-ui2/.husky/lintstagedrc.cjs @@ -0,0 +1,9 @@ +module.exports = { + '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'], + '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --parser json --write'], + 'package.json': ['prettier --write'], + '*.vue': ['prettier --write', 'stylelint --fix'], + '*.{scss,less,styl,css,html}': ['stylelint --fix', 'prettier --write'], + '*.md': ['prettier --write'], + '*.hbs': ['prettier --write'] +} diff --git a/promo-ui2/.husky/pre-commit b/promo-ui2/.husky/pre-commit new file mode 100644 index 0000000..d96ed69 --- /dev/null +++ b/promo-ui2/.husky/pre-commit @@ -0,0 +1,2 @@ +npm run ts:check +npm run lint:lint-staged \ No newline at end of file diff --git a/promo-ui2/.prettierignore b/promo-ui2/.prettierignore new file mode 100644 index 0000000..4ff8c9d --- /dev/null +++ b/promo-ui2/.prettierignore @@ -0,0 +1,9 @@ +/node_modules/** +/dist/ +/dist* +/public/* +/docs/* +/src/types/env.d.ts +/docs/**/* +/plop/**/* +CHANGELOG diff --git a/promo-ui2/.stylelintignore b/promo-ui2/.stylelintignore new file mode 100644 index 0000000..aa605b4 --- /dev/null +++ b/promo-ui2/.stylelintignore @@ -0,0 +1,6 @@ +/dist/* +/public/* +public/* +/dist* +/src/types/env.d.ts +/docs/**/* diff --git a/promo-ui2/CHANGELOG.md b/promo-ui2/CHANGELOG.md new file mode 100644 index 0000000..9d8dadd --- /dev/null +++ b/promo-ui2/CHANGELOG.md @@ -0,0 +1,1421 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## [2.9.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.8.1...v2.9.0) (2024-09-07) + + +### Features + +* The dialog supports custom-defined window size. [#527](https://github.com/kailong321200875/vue-element-plus-admin/issues/527) ([e6affad](https://github.com/kailong321200875/vue-element-plus-admin/commit/e6affad67fed5e815563396e7ce6ab816ee737ce)) +* 全局默认box-sizing: border-box;降低元素布局难度。 ([a27b14e](https://github.com/kailong321200875/vue-element-plus-admin/commit/a27b14e7ffeeeae6a212f262d20303db02b017f1)) + + +### Bug Fixes + +* VideoPlayer 的实例未赋值 [#524](https://github.com/kailong321200875/vue-element-plus-admin/issues/524) ([0e99f83](https://github.com/kailong321200875/vue-element-plus-admin/commit/0e99f8374a6c4aac19da9105c9b0c1f4950f87c7)) +* 修复husky问题 ([7b8e58e](https://github.com/kailong321200875/vue-element-plus-admin/commit/7b8e58e0ec81e99517cbca41e04930f013773380)) +* 修复isUrl判断错误bug [#526](https://github.com/kailong321200875/vue-element-plus-admin/issues/526) ([3cd89bd](https://github.com/kailong321200875/vue-element-plus-admin/commit/3cd89bdd09ee45d97d1ea41a5ee7686ca56c4ff2)) +* 修复在表格中给按钮添加link属性后,字体颜色变成白色 [#490](https://github.com/kailong321200875/vue-element-plus-admin/issues/490) ([5d9ca8a](https://github.com/kailong321200875/vue-element-plus-admin/commit/5d9ca8ac629adeb3d9c5fbf908502e597da7a168)) +* 修复新增权限时 id 缺失前端判断错误问题 ([88a0440](https://github.com/kailong321200875/vue-element-plus-admin/commit/88a04404d429e0a7e258fc8974e328dcbc04ae7a)) +* 修复示例图标错误 ([a07f4e9](https://github.com/kailong321200875/vue-element-plus-admin/commit/a07f4e9925ba15b3a94ddbea1908e42c2184b4cb)) +* 修复非一级子菜单显示位置错误;修改滚动条样式和系统滚动条样式一致 ([e0596ef](https://github.com/kailong321200875/vue-element-plus-admin/commit/e0596ef9f1f3ed022396961bf2a82665bebaecf7)) +* 左侧菜单收起后,组件菜单的子菜单显示不全 ([574055c](https://github.com/kailong321200875/vue-element-plus-admin/commit/574055c2749b9a183af44a0aaacab45f898cfff2)) + + +### Docs + +* 修改readme ([367b350](https://github.com/kailong321200875/vue-element-plus-admin/commit/367b3508e8efaf4b2b8383f44c3fea0e1dd09e8e)) + + +### Performance Improvements + +* 移除scrollbar__view高度限定。 ([18b08e2](https://github.com/kailong321200875/vue-element-plus-admin/commit/18b08e2983c116228de5282cd6e0c84884b24238)) + +## [2.8.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.8.0...v2.8.1) (2024-06-20) + + +### Bug Fixes + +* [#458](https://github.com/kailong321200875/vue-element-plus-admin/issues/458) ([49451ae](https://github.com/kailong321200875/vue-element-plus-admin/commit/49451ae606009d1f5ab0b98f84535892d3fd7646)) +* [#481](https://github.com/kailong321200875/vue-element-plus-admin/issues/481) ([c77586c](https://github.com/kailong321200875/vue-element-plus-admin/commit/c77586c5670cdc63978b032bbda694a14e875838)) +* 修复 search组件的收起展开 和重置 Bug ([9a5c7bc](https://github.com/kailong321200875/vue-element-plus-admin/commit/9a5c7bcb5b51e76eac6bc1d3aebc287593b13ca1)) +* 修复css前缀无法应用问题([#482](https://github.com/kailong321200875/vue-element-plus-admin/issues/482)) ([4b43c87](https://github.com/kailong321200875/vue-element-plus-admin/commit/4b43c87949fe4a68b4be004a06dfff4c7f87fbd4)) +* 修复表格default-expand-all属性无效BUG ([6657bbc](https://github.com/kailong321200875/vue-element-plus-admin/commit/6657bbc9f11f22cbfb04a57f5629bc810575496e)) +* 修复表格合计报错问题 ([9c44006](https://github.com/kailong321200875/vue-element-plus-admin/commit/9c44006ec26bee446dc5c90b6a4546cdd84ba4dc)) + + +### Styling + +* 修改登录页样式 ([9f98b7b](https://github.com/kailong321200875/vue-element-plus-admin/commit/9f98b7be266825612f93135c460d7db2d6a8beb0)) + + +### Performance Improvements + +* 优化使用离线图标后运行慢问题 ([7e9c4a6](https://github.com/kailong321200875/vue-element-plus-admin/commit/7e9c4a6109b417a577d9ac9ecf02db52eb1964af)) + +## [2.8.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.7.0...v2.8.0) (2024-06-01) + + +### Types + +* 修复Table类型错误 ([79b917a](https://github.com/kailong321200875/vue-element-plus-admin/commit/79b917af4957aa4b47db46e034385477828f5fca)) + + +### Features + +* Add a new component CodeEditor ([#466](https://github.com/kailong321200875/vue-element-plus-admin/issues/466)) ([00989b7](https://github.com/kailong321200875/vue-element-plus-admin/commit/00989b7ac9b92685be495c15c1f11dd2546eb6be)) + + +### Bug Fixes + +* [#427](https://github.com/kailong321200875/vue-element-plus-admin/issues/427) ([a00d76e](https://github.com/kailong321200875/vue-element-plus-admin/commit/a00d76e4149b430e19c985a78b9d89ce992dba3f)) +* [#428](https://github.com/kailong321200875/vue-element-plus-admin/issues/428) ([97a1cd4](https://github.com/kailong321200875/vue-element-plus-admin/commit/97a1cd41de82dad8855c95cec6bb106541fd53a7)) +* [#432](https://github.com/kailong321200875/vue-element-plus-admin/issues/432) ([df5b716](https://github.com/kailong321200875/vue-element-plus-admin/commit/df5b7166b48b7e0e77a1fb10ab6dd353d186547e)) +* [#438](https://github.com/kailong321200875/vue-element-plus-admin/issues/438) ([f977fdb](https://github.com/kailong321200875/vue-element-plus-admin/commit/f977fdb05d018ee07baeb6db454b9a77acb89f07)) +* [#451](https://github.com/kailong321200875/vue-element-plus-admin/issues/451) ([08665a3](https://github.com/kailong321200875/vue-element-plus-admin/commit/08665a35ac606549322039d073daf8072053eef4)) +* [#465](https://github.com/kailong321200875/vue-element-plus-admin/issues/465) ([8996e01](https://github.com/kailong321200875/vue-element-plus-admin/commit/8996e01ca35bfad8c13bef321f86bdd711202e12)) +* less 变量命名与 css 关键字冲突 [#475](https://github.com/kailong321200875/vue-element-plus-admin/issues/475) ([1c56e13](https://github.com/kailong321200875/vue-element-plus-admin/commit/1c56e13c5523a86d77464eccee26b5408db028c7)) +* 修复 lint-staged 中 prettier 以 json 格式美化代码的无效命令问题 ([7b2eae1](https://github.com/kailong321200875/vue-element-plus-admin/commit/7b2eae1d6aa813e162c3ad4a0553d2df480c765f)) +* 修复 Transfer 组件 optionApi 不生效 ([198718b](https://github.com/kailong321200875/vue-element-plus-admin/commit/198718b8749a036263d756f928b5dd38cfb47701)) +* 修复富文本编辑器初始化时, 报错 Error: Cannot find a descendant at path [0,1] in node ([a65d5fd](https://github.com/kailong321200875/vue-element-plus-admin/commit/a65d5fd20334307a56cb469361e8f9bd838510c9)) +* 修复组件-查询界面:收起和展开功能bug [#473](https://github.com/kailong321200875/vue-element-plus-admin/issues/473) ([8e58eae](https://github.com/kailong321200875/vue-element-plus-admin/commit/8e58eaeed6ea9beb749afaed75edc5a4f6d9867a)) + + +### Docs + +* 更新群二维码 ([2c89dbc](https://github.com/kailong321200875/vue-element-plus-admin/commit/2c89dbc884c38511d40c92480f65aef46511cefb)) + + +### Performance Improvements + +* 已经是 FormData 对象的不用再次转换 ([d582ad4](https://github.com/kailong321200875/vue-element-plus-admin/commit/d582ad428f4b378014d063635c4afbbad944a71a)) + +## [2.7.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.6.0...v2.7.0) (2024-02-29) + + +### Features + +* IAgree ([abb6906](https://github.com/kailong321200875/vue-element-plus-admin/commit/abb69064dfdb979e2843e3a1b62a2510f6ed3637)) +* 头像列表 ([3bf28a5](https://github.com/kailong321200875/vue-element-plus-admin/commit/3bf28a5d4555bf2a10754474db81d70b04ee432a)) +* 新增个人中心页 ([4146716](https://github.com/kailong321200875/vue-element-plus-admin/commit/4146716655bfbe4ae5b780e5b52a6377efd914ec)) + + +### Bug Fixes + +* 修复启动慢问题 ([61d7ef6](https://github.com/kailong321200875/vue-element-plus-admin/commit/61d7ef642a027e9e1f942bc84322233be3ca9a82)) +* 修复第四种布局样式层级问题([#424](https://github.com/kailong321200875/vue-element-plus-admin/issues/424)) ([78aeb89](https://github.com/kailong321200875/vue-element-plus-admin/commit/78aeb897fc93cfb998f94578d1fbe4480426843f)) + + +### Docs + +* 更新群二维码 ([c8c1a1b](https://github.com/kailong321200875/vue-element-plus-admin/commit/c8c1a1b6357105da73e23adff968c3f2fad7d837)) + +## [2.6.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.5.6...v2.6.0) (2024-02-07) + + +### Features + +* add vite-plugin-url-copy ([f5ab977](https://github.com/kailong321200875/vue-element-plus-admin/commit/f5ab9776a90e0136b243601571f4619c20da3ccd)) + + +### Bug Fixes + +* Table组件中size属性的validator设置错误 ([f30e37e](https://github.com/kailong321200875/vue-element-plus-admin/commit/f30e37ee777d4f30d4ae58c4a016a1392d41c25f)) +* Table组件注册为全局组件报错问题,存在对pinia的提前引用 ([1e209a7](https://github.com/kailong321200875/vue-element-plus-admin/commit/1e209a702a5114943a615063eefd0c00f1a6a003)) +* Table组件设置 align="center" 导致横向滚动条位置错误问题 ([22f071d](https://github.com/kailong321200875/vue-element-plus-admin/commit/22f071d9268806f7abd23ab2d08e9392e377a426)) +* 修复 element-plus 2.5版本以上,el-form-item inline模式下,select宽度问题 ([f44e48d](https://github.com/kailong321200875/vue-element-plus-admin/commit/f44e48d08d3f8dd347b829166107dd62e5e18c72)) +* 修复 prettier 报错 ([f5f08f8](https://github.com/kailong321200875/vue-element-plus-admin/commit/f5f08f8f87b063d489f55ea8f19c7c802acf15f7)) +* 修复 useCrudSchemas 详情组件数据结构文案不匹配问题 ([d94fc0a](https://github.com/kailong321200875/vue-element-plus-admin/commit/d94fc0a701bcbc9343ab3e7b630e3db8f6d61623)) +* 修复cutMenu布局刷新样式问题 ([03580b0](https://github.com/kailong321200875/vue-element-plus-admin/commit/03580b0ca0c8d088589ae1d8426b1535f654361b)) +* 修复Menu组件缩略菜单弹窗内样式不统一问题 ([d5dc4e3](https://github.com/kailong321200875/vue-element-plus-admin/commit/d5dc4e32d5978fcd271e841832c9cbf1e0c87db7)) +* 修复TagsView右键菜单逻辑错误 ([901c891](https://github.com/kailong321200875/vue-element-plus-admin/commit/901c891872ef6164e3517eb8e798d6039b7b7f4e)) +* 修复test打包VITE_USE_ONLINE_ICON无效问题 ([a3436a3](https://github.com/kailong321200875/vue-element-plus-admin/commit/a3436a32c6fd746e9e2af67c3cc5a8872aabf919)) +* 修复本地化图标空白问题 ([14ff83a](https://github.com/kailong321200875/vue-element-plus-admin/commit/14ff83affcd267fbdb405d2f46e9f929a1fbfaeb)) +* 修复请求示例中,mock开启时无法取消单个请求的问题 ([d6d70a4](https://github.com/kailong321200875/vue-element-plus-admin/commit/d6d70a443cccb2fe12161b57a1f227d1ed63384a)) +* 修改兼容方式,兼容Form 组件中contentMap中类输入框或下拉选择的所有组件,特殊兼容 InputNumber 组件 ([ab98ceb](https://github.com/kailong321200875/vue-element-plus-admin/commit/ab98ceb85f52c5f7b87c2114997c63f1b80f216f)) +* 更换判断条件 ([b5cb626](https://github.com/kailong321200875/vue-element-plus-admin/commit/b5cb626bfac4df8b1a0741b5000d5b22f6cd4555)) + + +### Docs + +* 修改群二维码 ([395ff68](https://github.com/kailong321200875/vue-element-plus-admin/commit/395ff68412ff71a9b8ce670c2399da285cfed67d)) +* 更新群二维码 ([c8ccaa8](https://github.com/kailong321200875/vue-element-plus-admin/commit/c8ccaa8d49b5bf7a2784a29af6e126657ce54cda)) + + +### Styling + +* 添加TabMenu边框 ([feb3d9a](https://github.com/kailong321200875/vue-element-plus-admin/commit/feb3d9a8d07f6444c39ca89f6eb63245c06783a0)) + +## [2.5.6](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.5.5...v2.5.6) (2024-01-18) + + +### Bug Fixes + +* [#396](https://github.com/kailong321200875/vue-element-plus-admin/issues/396) ([9b2b4d4](https://github.com/kailong321200875/vue-element-plus-admin/commit/9b2b4d42a6d5fffd5012506b7cac3892774c8595)) +* [#399](https://github.com/kailong321200875/vue-element-plus-admin/issues/399) ([59d4ed4](https://github.com/kailong321200875/vue-element-plus-admin/commit/59d4ed4dd9d6b6f0d5881b4d466e7a621770ad75)) +* 修复Form组件设置了hidden还是会出现占位空白 ([0f531fd](https://github.com/kailong321200875/vue-element-plus-admin/commit/0f531fd1d0469ddd56327b0a9a7956a0d6076c91)) +* 修复无法登录问题 ([8ce00ab](https://github.com/kailong321200875/vue-element-plus-admin/commit/8ce00ab247de4061cb56f9c2f6d3079abd39aefd)) +* 修复菜单管理回显问题 ([d9ca9ba](https://github.com/kailong321200875/vue-element-plus-admin/commit/d9ca9ba5e8111b7cc3758a8bba14f7fac45c9446)) +* 升级依赖,修复vue警告 ([eafb507](https://github.com/kailong321200875/vue-element-plus-admin/commit/eafb5075d587feac0501a1adae90a176a72c240f)) + + +### Docs + +* 更新README ([28bd10f](https://github.com/kailong321200875/vue-element-plus-admin/commit/28bd10f26373ad6e139b412e08d1e2afacc4ab92)) + + +### Styling + +* 调整样式 ([09b96c7](https://github.com/kailong321200875/vue-element-plus-admin/commit/09b96c75425cd2d931e7df4ef3f330b78bf74f9e)) + + +### Performance Improvements + +* request请求根据ContentType自动转换数据 ([ef9aa62](https://github.com/kailong321200875/vue-element-plus-admin/commit/ef9aa625724b754afc565b8b1f2589376f4d5c50)) +* 使用flex布局,优化section区域min-height的繁琐计算 ([fbb6f9a](https://github.com/kailong321200875/vue-element-plus-admin/commit/fbb6f9ad4b6d5fac9bb95d0a9250b5a318680d99)) + +## [2.5.5](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.5.4...v2.5.5) (2024-01-06) + + +### Bug Fixes + +* [#276](https://github.com/kailong321200875/vue-element-plus-admin/issues/276) ([6fbc2b0](https://github.com/kailong321200875/vue-element-plus-admin/commit/6fbc2b0243e4aec0463a734c37591dc3de40f7db)) +* el-button组件和其他部分使用到相关变量的组件无法适配主题色变化问题 ([00cac6a](https://github.com/kailong321200875/vue-element-plus-admin/commit/00cac6a831c2a0bb2f8a9df8b9264f1cad13ddde)) + + +### Styling + +* 菜单支持超出省略号 ([a926c56](https://github.com/kailong321200875/vue-element-plus-admin/commit/a926c5607a162145f77d35762b3d6730d67b23f6)) +* 菜单背景支持跟随暗黑模式 ([b34aeba](https://github.com/kailong321200875/vue-element-plus-admin/commit/b34aeba10a464a0f92752fc966386286443df53a)) + + +### Performance Improvements + +* 优化ImageCropping ([069777c](https://github.com/kailong321200875/vue-element-plus-admin/commit/069777c8801c51ab28c070b2ba3f10000e9c91b4)) +* 图标选择器逻辑优化 ([c2dde25](https://github.com/kailong321200875/vue-element-plus-admin/commit/c2dde252297c94036221d5d9971781182bc2998e)) +* 表格组件预览字段拆分 ([8c5858e](https://github.com/kailong321200875/vue-element-plus-admin/commit/8c5858e2c5d42db1de37d5290ea2ca784f4d4612)) + +## [2.5.4](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.5.3...v2.5.4) (2023-12-26) + + +### Types + +* 修复全局组件属性类型无法推导 ([94160c0](https://github.com/kailong321200875/vue-element-plus-admin/commit/94160c0418816e560f440e259e1f0fd4742e0143)) + + +### Bug Fixes + +* Menu菜单组件显示bug,renderMenuItem返回的数组存在undefined数据,导致省略菜单显示问题 ([1c63757](https://github.com/kailong321200875/vue-element-plus-admin/commit/1c63757d55076d15ffdf21d647de393ca3c6b0be)) +* useClipboard在ip地址下不能使用问题 ([f3593c4](https://github.com/kailong321200875/vue-element-plus-admin/commit/f3593c453a8b8d5eb8cbd0ed5402132b027461b9)) +* 修复request请求自定义headers类型错误 ([bf2cd72](https://github.com/kailong321200875/vue-element-plus-admin/commit/bf2cd720d0e5801603848a0b1520a928443ac549)) +* 修复切换主题色缓存失败 ([1074520](https://github.com/kailong321200875/vue-element-plus-admin/commit/10745207e64d2d444636cb9d877cec9a0bebf1eb)) +* 修复非正式环境打包报错 ([3a5db42](https://github.com/kailong321200875/vue-element-plus-admin/commit/3a5db42c97f382f3fc701b8f113385b38a214583)) + + +### Styling + +* 本地化图标 ([608bf50](https://github.com/kailong321200875/vue-element-plus-admin/commit/608bf50e1cae49b7f97587395f794ae351f833f0)) +* 添加常见问题链接 ([16b9375](https://github.com/kailong321200875/vue-element-plus-admin/commit/16b93757d32c8ce2f611a62d6015072b0ecfc09a)) + + +### Code Refactoring + +* 新增列设置 ([7314065](https://github.com/kailong321200875/vue-element-plus-admin/commit/7314065c907f8ef4d184c1f3c724b67c30410ab9)) +* 重写useEventBus ([8035151](https://github.com/kailong321200875/vue-element-plus-admin/commit/80351516ced0ec2d67c30405d4a644aca8ca4bc2)) + + +### Performance Improvements + +* 优化启动速度 ([379b340](https://github.com/kailong321200875/vue-element-plus-admin/commit/379b340750eb0d4f7816f5d7c25cbd2983fd33b9)) +* 还原mock.js ([83de387](https://github.com/kailong321200875/vue-element-plus-admin/commit/83de387e2a0124804a9c99080ac841a9d6676fca)) + +## [2.5.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.5.2...v2.5.3) (2023-12-17) + + +### Bug Fixes + +* [#374](https://github.com/kailong321200875/vue-element-plus-admin/issues/374) ([30fb2de](https://github.com/kailong321200875/vue-element-plus-admin/commit/30fb2de6f37fe0bb00b0f364da31b07a292d59a1)) +* 修复cutMenu布局和top布局内容高度计算错误问题 ([8badd48](https://github.com/kailong321200875/vue-element-plus-admin/commit/8badd48a699aabd8fe510052d098fa6848ff5cbd)) +* 修复动态路由多开标签页404问题 ([1c5b16f](https://github.com/kailong321200875/vue-element-plus-admin/commit/1c5b16f529d2e60a1eefcadf3f416585d1adb93b)) +* 修复类型推导错误 ([649fb17](https://github.com/kailong321200875/vue-element-plus-admin/commit/649fb17d000c0d500ffcfe1f9ab6ddd73ab7ecfa)) +* 修复项目配置清楚缓存无效 ([a09ee60](https://github.com/kailong321200875/vue-element-plus-admin/commit/a09ee60bb123f5bc4bbe6d80539145d5c4b94cb8)) + + +### Code Refactoring + +* 重构描述组件样式 ([c7658d8](https://github.com/kailong321200875/vue-element-plus-admin/commit/c7658d8c70618045a7527156444ba1d564963325)) + + +### Performance Improvements + +* 优化登录记住我流程 ([2009594](https://github.com/kailong321200875/vue-element-plus-admin/commit/2009594f089722151b739598dbad5ee7fb062b6e)) + +## [2.5.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.5.1...v2.5.2) (2023-12-10) + + +### Bug Fixes + +* 修复mock无法使用问题 ([319aaef](https://github.com/kailong321200875/vue-element-plus-admin/commit/319aaef7eec6287a0e80f25a479918d43c051810)) + +## [2.5.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.5.0...v2.5.1) (2023-12-10) + + +### Bug Fixes + +* 修复表单回车刷新页面 ([2f64836](https://github.com/kailong321200875/vue-element-plus-admin/commit/2f6483652b5d130057b4422b0f3350542b4b4b1d)) +* 表单布局方式为top时,查询组件按钮位置错位 ([ca98359](https://github.com/kailong321200875/vue-element-plus-admin/commit/ca983590da72cb13392cb8897f4045fbacbc6c8d)) + + +### Docs + +* 更新Readme ([81d2dc6](https://github.com/kailong321200875/vue-element-plus-admin/commit/81d2dc6a43df8fd5799461cdafc1b7e6054cf1e1)) + + +### Styling + +* 修改视频表格样式 ([93767b6](https://github.com/kailong321200875/vue-element-plus-admin/commit/93767b65aa7c41e28a8a79a82edd2a91d20bd176)) +* 抽离BaseButton,支持按钮修改主题色 ([69539ee](https://github.com/kailong321200875/vue-element-plus-admin/commit/69539ee2d34ddfcb83cbfb25e218b94891196e76)) + + +### Performance Improvements + +* [#344](https://github.com/kailong321200875/vue-element-plus-admin/issues/344) ([7fa533b](https://github.com/kailong321200875/vue-element-plus-admin/commit/7fa533b8ba0d886c0009b350a3b5fe4b027a9126)) + +## [2.5.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.4.1...v2.5.0) (2023-12-03) + + +### Types + +* 删除无用类型 ([30e4214](https://github.com/kailong321200875/vue-element-plus-admin/commit/30e421438793b8283a0113ba50eb9aef90cfed4e)) + + +### Features + +* VideoPlayer ([7b5bbed](https://github.com/kailong321200875/vue-element-plus-admin/commit/7b5bbedbccf56049ff611005ba17a0f07b07034d)) +* 持久化缓存 ([893459d](https://github.com/kailong321200875/vue-element-plus-admin/commit/893459da7cf819b6b94477cd76fdfeeecacc287f)) +* 新增ImageCropping ([b0a43a7](https://github.com/kailong321200875/vue-element-plus-admin/commit/b0a43a70e6c93690ba4b0779527316f40297a45d)) +* 新增userStore ([77c962e](https://github.com/kailong321200875/vue-element-plus-admin/commit/77c962ea91de68299a01680a7941cf7a73c7e4a7)) +* 新增表格视频预览 ([cfc2d54](https://github.com/kailong321200875/vue-element-plus-admin/commit/cfc2d54586e73353295e7b73e2bf39e4e4d03c96)) +* 替换mock-server插件 ([b8f9a99](https://github.com/kailong321200875/vue-element-plus-admin/commit/b8f9a9940d5eb3f532421b1b85aeb1f3d9afb4b1)) +* 替换mockjs ([7c76d94](https://github.com/kailong321200875/vue-element-plus-admin/commit/7c76d945be8c46b427fe65c728ae0e70ab7a5e91)) +* 重新整理目录结构,mock请求 ([179ab26](https://github.com/kailong321200875/vue-element-plus-admin/commit/179ab2672fe7fff55c8a9c55fae22a4b6c362623)) + + +### Bug Fixes + +* [#367](https://github.com/kailong321200875/vue-element-plus-admin/issues/367) ([c8400ab](https://github.com/kailong321200875/vue-element-plus-admin/commit/c8400abd9f37405127890be1c9a559edf9f251f8)) + + +### Styling + +* 格式化代码 ([31ea31d](https://github.com/kailong321200875/vue-element-plus-admin/commit/31ea31dde8a149f4fc805c08e4fca4e755c36752)) +* 表单项宽度默认100% ([416de2b](https://github.com/kailong321200875/vue-element-plus-admin/commit/416de2b4d644f68d7db379c7cb1139c8a17f64d7)) + + +### Performance Improvements + +* 新增token过期示例 ([bdc8d35](https://github.com/kailong321200875/vue-element-plus-admin/commit/bdc8d358a1ca8f5fc6b43990899834791364e4f2)) + +## [2.4.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.4.0...v2.4.1) (2023-11-12) + + +### Bug Fixes + +* [#361](https://github.com/kailong321200875/vue-element-plus-admin/issues/361) ([2e7797b](https://github.com/kailong321200875/vue-element-plus-admin/commit/2e7797be68b2469d979231e6588b43d0b5bdb88b)) +* Default currentSize ([af583c7](https://github.com/kailong321200875/vue-element-plus-admin/commit/af583c71b0d1760ba4ed4cfa12458820c3f4db52)) +* 修复瀑布流示例图片无法展示 ([3477173](https://github.com/kailong321200875/vue-element-plus-admin/commit/3477173b7649eb43a1e64c91135b0e657a3c7888)) +* 修复自动格式化无效 ([bd82108](https://github.com/kailong321200875/vue-element-plus-admin/commit/bd8210858126f945bad31b3f1e0416aa178afef1)) + + +### Styling + +* 修改样式 ([92d436b](https://github.com/kailong321200875/vue-element-plus-admin/commit/92d436b8bb95c94831fcfe30678d384c3debc052)) + + +### Performance Improvements + +* 优化权限管理 ([efc1c25](https://github.com/kailong321200875/vue-element-plus-admin/commit/efc1c25db86d28438a2c324a3dc302501e1fdf8f)) +* 优化瀑布流组件 ([82eb7f1](https://github.com/kailong321200875/vue-element-plus-admin/commit/82eb7f16ad3f663be602a747b55a78f6b986da30)) + +## [2.4.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.3.0...v2.4.0) (2023-10-14) + + +### Types + +* 修改类型错误 ([4760733](https://github.com/kailong321200875/vue-element-plus-admin/commit/4760733bbe39b547285894555754bae6539190f9)) + + +### Features + +* Waterfall ([d543e56](https://github.com/kailong321200875/vue-element-plus-admin/commit/d543e56efb3b3e5800ab3ec24eda25565311eda2)) + + +### Bug Fixes + +* [#342](https://github.com/kailong321200875/vue-element-plus-admin/issues/342) ([1c51221](https://github.com/kailong321200875/vue-element-plus-admin/commit/1c512216453b17c64a09f97263fd481816badf7c)) +* [#346](https://github.com/kailong321200875/vue-element-plus-admin/issues/346) ([d392868](https://github.com/kailong321200875/vue-element-plus-admin/commit/d392868c2799c2066ba606b0cdad95c011399559)) +* [#355](https://github.com/kailong321200875/vue-element-plus-admin/issues/355) ([03d5e13](https://github.com/kailong321200875/vue-element-plus-admin/commit/03d5e130146a662a8a312e6c49f995f85ea0f9d3)) +* **Descriptions:** Add a default value ([83b09f0](https://github.com/kailong321200875/vue-element-plus-admin/commit/83b09f09ffafb2a6273a1c5274e22f842c202c32)) +* table column 中定义 selectable 无效 ([b8e043c](https://github.com/kailong321200875/vue-element-plus-admin/commit/b8e043c09c74fe00521ac0d7390331b9f223c797)) +* Table的addColumn不能添加首列 ([240178f](https://github.com/kailong321200875/vue-element-plus-admin/commit/240178fd380402571fc056ddb9c8ae44ccb1e265)) +* 修复Waterfall列数错误BUG ([1c2befa](https://github.com/kailong321200875/vue-element-plus-admin/commit/1c2befa4ddc76c625774100e3f5dd5a68a6faa45)) +* 去除控制台警告 ([4d14246](https://github.com/kailong321200875/vue-element-plus-admin/commit/4d14246de50d2ba9d652ec5ef038f4fd3597006a)) + + +### Styling + +* Descriptions样式调整 ([be73f4d](https://github.com/kailong321200875/vue-element-plus-admin/commit/be73f4da3e4bbbacf3f748f7ebfd70f825e0d15e)) +* formDemo集成图标选择器 ([99ffe6a](https://github.com/kailong321200875/vue-element-plus-admin/commit/99ffe6a86ac9961ad5b9be0171b01acdfa0cf994)) +* 修改 Search 组件图标错误 ([7c93b74](https://github.com/kailong321200875/vue-element-plus-admin/commit/7c93b74e8f3e69d6c88ef2891eb6accc99a6a1e8)) + + +### Performance Improvements + +* IconPicker新增搜索功能 ([a4d1391](https://github.com/kailong321200875/vue-element-plus-admin/commit/a4d1391390bb33d498f2ec2cc64965f1a0b0aaab)) +* useClipboard ([1db2248](https://github.com/kailong321200875/vue-element-plus-admin/commit/1db22482b43f6fb7ca8321b838fb41a5b0aff62e)) +* useNetwork ([88be3ee](https://github.com/kailong321200875/vue-element-plus-admin/commit/88be3eea10196054596945af0eb9910e998dfd42)) +* 优化请求例子 ([6b3d2e1](https://github.com/kailong321200875/vue-element-plus-admin/commit/6b3d2e14985c1a7a3c68001e17820d0e7a833a56)) +* 完善demo ([2c4ff7d](https://github.com/kailong321200875/vue-element-plus-admin/commit/2c4ff7d190c816a92d92f9c2dbe048436b2bf964)) +* 新增请求示例 ([2762aaf](https://github.com/kailong321200875/vue-element-plus-admin/commit/2762aaf09b3616944476797a6e112c350c12a0ec)) + +## [2.3.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.2.0...v2.3.0) (2023-09-24) + + +### Features + +* IconPicker ([4490d5e](https://github.com/kailong321200875/vue-element-plus-admin/commit/4490d5eeeb4389f94f90c9c45a30343324db2250)) +* 表格工具栏新增列设置功能 ([9d10ba8](https://github.com/kailong321200875/vue-element-plus-admin/commit/9d10ba821feca414b9b020322859ca4a47291005)) + + +### Bug Fixes + +* [#326](https://github.com/kailong321200875/vue-element-plus-admin/issues/326) ([c95a4e0](https://github.com/kailong321200875/vue-element-plus-admin/commit/c95a4e0763838e843cf5ce174110a01f2baa8000)) +* default interceptor response return ([c3d8540](https://github.com/kailong321200875/vue-element-plus-admin/commit/c3d8540ab284312f24d9355072f6fb4506ed6d1d)) +* 修复IconPicker BUG ([1e3aa78](https://github.com/kailong321200875/vue-element-plus-admin/commit/1e3aa789260773b1caecdaa32e1cafede22733e3)) +* 修复useCrudSchemas无法自定义label ([aa5deb1](https://github.com/kailong321200875/vue-element-plus-admin/commit/aa5deb13904e45e7cb6ec7285e936b9ebae57273)) + + +### Docs + +* 更新README ([4947c82](https://github.com/kailong321200875/vue-element-plus-admin/commit/4947c82d6770f0dce2845682f0c41d853268cf82)) +* 更新README ([c3624ce](https://github.com/kailong321200875/vue-element-plus-admin/commit/c3624cee588457e7fedaab360746500337c1b2a7)) +* 更新群二维码 ([ead1ab8](https://github.com/kailong321200875/vue-element-plus-admin/commit/ead1ab8c88c05593d539b56a811809382675faf5)) + + +### Styling + +* 修复样式层级问题 ([f92d2b6](https://github.com/kailong321200875/vue-element-plus-admin/commit/f92d2b60a956e1963b63e23b446a9d42096704e0)) +* 修改登录样式 ([bdd31f0](https://github.com/kailong321200875/vue-element-plus-admin/commit/bdd31f0621712af89d89b87ac439c3e0b398605a)) + +## [2.2.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.1.0...v2.2.0) (2023-08-27) + + +### Features + +* JsonEditor ([c0f4517](https://github.com/kailong321200875/vue-element-plus-admin/commit/c0f4517b87de5a0172a057fb9da141f758cca1fa)) +* 新增 useCrudSchemas demo ([ae0628e](https://github.com/kailong321200875/vue-element-plus-admin/commit/ae0628e3af3466c7c9d7b593b825f776843de5ec)) +* 新增useTagsView ([a869a45](https://github.com/kailong321200875/vue-element-plus-admin/commit/a869a457e6a8052531ce3040ae0d332d7afbb478)) + + +### Bug Fixes + +* [#316](https://github.com/kailong321200875/vue-element-plus-admin/issues/316) ([7582e4d](https://github.com/kailong321200875/vue-element-plus-admin/commit/7582e4d12f18ae86f5ef4ff36211c364afca5763)) +* [#317](https://github.com/kailong321200875/vue-element-plus-admin/issues/317) ([2095caa](https://github.com/kailong321200875/vue-element-plus-admin/commit/2095caaa854b686b57f47ee183419f42563a5a95)) +* [#318](https://github.com/kailong321200875/vue-element-plus-admin/issues/318) ([4169e52](https://github.com/kailong321200875/vue-element-plus-admin/commit/4169e52baaaa43765848c29c5ce222d019e81c35)) +* [#319](https://github.com/kailong321200875/vue-element-plus-admin/issues/319) ([b6ee4e5](https://github.com/kailong321200875/vue-element-plus-admin/commit/b6ee4e5d48deb3a07f289366ed3700baa3674cd6)) +* 修复useValidator报错 ([4912f6c](https://github.com/kailong321200875/vue-element-plus-admin/commit/4912f6c0586249b3de7ac7d365c8ea98af7923c7)) +* 修复动态路由无效 ([1452a1a](https://github.com/kailong321200875/vue-element-plus-admin/commit/1452a1afc77eb3f64cd3de91a05ddc15e40f4a06)) + + +### Docs + +* 更新README ([5b4defa](https://github.com/kailong321200875/vue-element-plus-admin/commit/5b4defa8c4be2de894b2cb50ae9ea739a10cf7d9)) +* 更新群二维码 ([13aa71c](https://github.com/kailong321200875/vue-element-plus-admin/commit/13aa71c5bd5b5076599501961a24a171a9133c57)) +* 更新群二维码 ([ae29e97](https://github.com/kailong321200875/vue-element-plus-admin/commit/ae29e974bfed2214d1beda703b976cdfa63070ab)) + + +### Styling + +* 修改Descriptions样式 ([cd0e05a](https://github.com/kailong321200875/vue-element-plus-admin/commit/cd0e05a6b9146af7ae64be62613724cd58e6c2a3)) + + +### Code Refactoring + +* 重构useValidator ([b8849da](https://github.com/kailong321200875/vue-element-plus-admin/commit/b8849dabe2b306831f69e84db167a367570d992a)) + + +### Performance Improvements + +* 优化动态路由 ([8793588](https://github.com/kailong321200875/vue-element-plus-admin/commit/879358821d02d5e4575dfee0d189b9fee7f2e217)) +* 完善useTagsView ([e0c55f4](https://github.com/kailong321200875/vue-element-plus-admin/commit/e0c55f40d4c1c47e29de6c4c7e9433efa978bf7f)) +* 完善useTagsView ([175abd0](https://github.com/kailong321200875/vue-element-plus-admin/commit/175abd0aa3388e8473f6ecbf63e28133fce55bd3)) +* 更新demo ([2c99cd2](https://github.com/kailong321200875/vue-element-plus-admin/commit/2c99cd20f0c25a740ac7a3a8319f7a112e69c0d3)) + +## [2.1.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v2.0.0...v2.1.0) (2023-08-12) + + +### Features + +* 新增多开标签页Demo ([5c253ce](https://github.com/kailong321200875/vue-element-plus-admin/commit/5c253ce803a9ef7ce03534ddd5f0865db4602378)) + + +### Bug Fixes + +* [#307](https://github.com/kailong321200875/vue-element-plus-admin/issues/307) ([4ce07e1](https://github.com/kailong321200875/vue-element-plus-admin/commit/4ce07e150c0bd3903cc5f43fcd88c2cb292d7690)) +* [#311](https://github.com/kailong321200875/vue-element-plus-admin/issues/311) ([bdde4cc](https://github.com/kailong321200875/vue-element-plus-admin/commit/bdde4ccd39d5d698d68b299c6e80546d4a8be89f)) +* 修复eslint错误 ([b5e47e0](https://github.com/kailong321200875/vue-element-plus-admin/commit/b5e47e04d8f5f889e0c46a2dced108d058ded94e)) +* 修复Table插槽传参错误 ([97344e6](https://github.com/kailong321200875/vue-element-plus-admin/commit/97344e68f5abb144d9e5d4ad273108858dbcfba2)) +* 修复Table组件插槽传参错误 ([c83a026](https://github.com/kailong321200875/vue-element-plus-admin/commit/c83a026d559e2854fead17d2e28fbebcf25490de)) + + +### Docs + +* 修改Readme ([ee059b7](https://github.com/kailong321200875/vue-element-plus-admin/commit/ee059b7619ad01ded9d3be20287086ddbcce3253)) +* 修改Readme ([e05f5a7](https://github.com/kailong321200875/vue-element-plus-admin/commit/e05f5a77edc175daa267e4fc6abbcfc8fec2e291)) +* 修改Readme ([b0e561d](https://github.com/kailong321200875/vue-element-plus-admin/commit/b0e561d8acd36e8780087e317cc34257956981fd)) +* 修改Readme ([fced2e0](https://github.com/kailong321200875/vue-element-plus-admin/commit/fced2e0087694445a89cf360e5e3e3013d8ca604)) +* 修改README ([dce76f0](https://github.com/kailong321200875/vue-element-plus-admin/commit/dce76f042d5243039540828a3fd982af25f37531)) +* 更新群二维码 ([607ef58](https://github.com/kailong321200875/vue-element-plus-admin/commit/607ef585d010c9ade6f54d96c2a12b36099ece74)) + + +### Styling + +* 修改TabMenu样式 ([e8cd6f9](https://github.com/kailong321200875/vue-element-plus-admin/commit/e8cd6f9e1c4387c582e461cde4d59796bf17c1bd)) + +## [2.0.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.10.0...v2.0.0) (2023-08-06) + + +### ⚠ BREAKING CHANGES + +* 重构完成 + +### Features + +* 重构完成 ([76e971e](https://github.com/kailong321200875/vue-element-plus-admin/commit/76e971ef96ad4f5cc7df58abd0559898ce70207d)) + + +### Code Refactoring + +* 重构完成 ([85f8cda](https://github.com/kailong321200875/vue-element-plus-admin/commit/85f8cda19d8cafb951f211b845aad970a661dd1e)) +* 重构完成 ([5d55597](https://github.com/kailong321200875/vue-element-plus-admin/commit/5d55597cca6c9d2bc6cb6211a01c161fa5f086ba)) + +## [1.10.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.9...v1.10.0) (2023-08-06) + + +### Types + +* Form类型调整 ([a0f4aeb](https://github.com/kailong321200875/vue-element-plus-admin/commit/a0f4aebc5a685366cd56b1a7bb39fa614976e3bb)) +* Form类型调整 ([674d760](https://github.com/kailong321200875/vue-element-plus-admin/commit/674d760029b451c0c6fc23a2aeac5c83992a0b27)) +* 修改类型 ([c3ac191](https://github.com/kailong321200875/vue-element-plus-admin/commit/c3ac1915045d4d59bca09ec6d19151bc5da342f1)) +* 修改类型 ([7d0476f](https://github.com/kailong321200875/vue-element-plus-admin/commit/7d0476f47c5858019db871cff2bdd19f0210f0d4)) +* 类型优化 ([283bc58](https://github.com/kailong321200875/vue-element-plus-admin/commit/283bc58d46151a8954bb81ee6bf8f499177b15fc)) +* 调整类型 ([24c8af9](https://github.com/kailong321200875/vue-element-plus-admin/commit/24c8af91835fb2c8c00e7c2673fff01f098c9944)) +* 迁移types ([ccbec86](https://github.com/kailong321200875/vue-element-plus-admin/commit/ccbec865568b1c9b3c3321d7071c164fdc350a0f)) +* 迁移types ([46b35e4](https://github.com/kailong321200875/vue-element-plus-admin/commit/46b35e48b3e7876c74159625b5149ef663396f5c)) + + +### Features + +* axios 改造 ([3238140](https://github.com/kailong321200875/vue-element-plus-admin/commit/32381408bbe418eeaca2a975305bac80ddaa03f5)) +* axios 改造 ([5807db1](https://github.com/kailong321200875/vue-element-plus-admin/commit/5807db1dc12a7ff2dbf66801a742a78974ad8f9c)) +* Descriptions组件重构 ([49e415d](https://github.com/kailong321200875/vue-element-plus-admin/commit/49e415d27788cb468c96f2a828f1df7ae65b7a3c)) +* Dialog组件重构 ([3701a04](https://github.com/kailong321200875/vue-element-plus-admin/commit/3701a04231af02ec7f7ef73533f3a22e707380fb)) +* Form useForm 完成 ([3e4e27c](https://github.com/kailong321200875/vue-element-plus-admin/commit/3e4e27c21fd59c944229856bee929f005d2ee140)) +* Form改造 ([9c724dc](https://github.com/kailong321200875/vue-element-plus-admin/commit/9c724dc9aad18397d5ecd00e53c3c24e142a34b5)) +* Icon改版 ([882f162](https://github.com/kailong321200875/vue-element-plus-admin/commit/882f162ff21c74239b638f284f52161e5791722d)) +* Radio改造 ([deeee73](https://github.com/kailong321200875/vue-element-plus-admin/commit/deeee73bcb3ad912844fddee62b1155d95d4b42b)) +* Radio改造 ([83513d5](https://github.com/kailong321200875/vue-element-plus-admin/commit/83513d519d4b6b8fbfd48db266b9bd7b3a998d63)) +* Search组件重构 ([a7f3702](https://github.com/kailong321200875/vue-element-plus-admin/commit/a7f370214481577ab82bf2871191dda717c7978a)) +* SelectV2改造完成 ([4d04734](https://github.com/kailong321200875/vue-element-plus-admin/commit/4d04734e13f6926c16aeee421feecb0d339534f0)) +* Table重构 ([94800b0](https://github.com/kailong321200875/vue-element-plus-admin/commit/94800b0120ee05ca7d534dda3e59653f38d7fda0)) +* 完善search组件demo ([cdf44a4](https://github.com/kailong321200875/vue-element-plus-admin/commit/cdf44a43a05010dbcba3a3ec0cb7c8251f16fce3)) +* 拖拽表格 ([b69b8ed](https://github.com/kailong321200875/vue-element-plus-admin/commit/b69b8ed1bde36100fc86e51fcc63805d4ea21210)) +* 新增TreeSelect表单项 ([de0cb43](https://github.com/kailong321200875/vue-element-plus-admin/commit/de0cb43566b9065250abbc71548ffeca4c8e8bf1)) +* 新增Uload ([c181887](https://github.com/kailong321200875/vue-element-plus-admin/commit/c181887f7f0c5eecc9584edfe99e9065440bdc56)) +* 新增useStorage ([dfea91c](https://github.com/kailong321200875/vue-element-plus-admin/commit/dfea91c7e1d18fa299067c62557cac61723ea861)) +* 新增权限测试页 ([3fe40ba](https://github.com/kailong321200875/vue-element-plus-admin/commit/3fe40ba62df29c2ffea9adfd65fc559489481e24)) +* 新增锁屏功能 ([e2fd349](https://github.com/kailong321200875/vue-element-plus-admin/commit/e2fd349070147c57f9400fa9a413260b7707bda2)) +* 用户列表重构 ([755cea0](https://github.com/kailong321200875/vue-element-plus-admin/commit/755cea0990d9e3b64c936f29c02e4053393a1a19)) +* 登录页改造 ([5312951](https://github.com/kailong321200875/vue-element-plus-admin/commit/5312951359b5d919b6c1a03783aa6bbaf8ec0044)) +* 综合示例重构 ([9a0259d](https://github.com/kailong321200875/vue-element-plus-admin/commit/9a0259de5c47970502db95f4dda24998ad5d9efe)) +* 菜单管理 ([c72b3a3](https://github.com/kailong321200875/vue-element-plus-admin/commit/c72b3a33aab7d3605770a64d23b8a84ef4ad68d2)) +* 角色管理 ([47016a5](https://github.com/kailong321200875/vue-element-plus-admin/commit/47016a535f2b7a22ab498bee197bc30a983f507d)) +* 部门管理 ([28d0785](https://github.com/kailong321200875/vue-element-plus-admin/commit/28d0785be842022cae7808c23e1f19eaab5fb996)) +* 重构Dialog组件示例 ([9a78ac9](https://github.com/kailong321200875/vue-element-plus-admin/commit/9a78ac977eb0cfb3bd6c2a9b96e69d9f010017f4)) + + +### Bug Fixes + +* mock数据 ([8bdac71](https://github.com/kailong321200875/vue-element-plus-admin/commit/8bdac7152f463cd98c50c9893a46bb6c111fd428)) +* 修复Form已知问题 ([097b32e](https://github.com/kailong321200875/vue-element-plus-admin/commit/097b32e1a9d92a609a66179d68b3dabe12f96b66)) +* 修复Table组件已知问题 ([b1a83f6](https://github.com/kailong321200875/vue-element-plus-admin/commit/b1a83f601838cb82fb29c036654a4cdc729997cd)) +* 修复类型错误 ([26dc886](https://github.com/kailong321200875/vue-element-plus-admin/commit/26dc886f8ccb5cf1ffc10e1d9601c827a1f960c2)) +* 样式问题修复 ([cdc7c76](https://github.com/kailong321200875/vue-element-plus-admin/commit/cdc7c76eb5ac3ccb79f5f55ff5b7ce6b8c4955e1)) +* 解决类型检测报错 ([9d93496](https://github.com/kailong321200875/vue-element-plus-admin/commit/9d9349600b3d2008e4216d49c9fa1c1b9995fa79)) +* 解决类型检测报错 ([513108c](https://github.com/kailong321200875/vue-element-plus-admin/commit/513108c00e622812e2e70dfe833435f6b5462d6e)) +* 解决类型检测报错 ([28bf8be](https://github.com/kailong321200875/vue-element-plus-admin/commit/28bf8bee45e3cc8575a356623abdbe56e30991f8)) + + +### Styling + +* Table样式修改 ([5fc57bd](https://github.com/kailong321200875/vue-element-plus-admin/commit/5fc57bdb08488f6898eafd6f28289b0567d6d9e2)) +* Table样式修改 ([411c0f7](https://github.com/kailong321200875/vue-element-plus-admin/commit/411c0f792ae8359c49e81974d8193f049120985b)) +* Table样式修改 ([d487c6a](https://github.com/kailong321200875/vue-element-plus-admin/commit/d487c6a93ec0281d76a3938e6e23ea2a4a7940c1)) +* Table样式修改 ([c7d21e3](https://github.com/kailong321200875/vue-element-plus-admin/commit/c7d21e36d012377ba863ac848d77abb5db4f475a)) +* Table样式修改 ([7f5078a](https://github.com/kailong321200875/vue-element-plus-admin/commit/7f5078a436c4d5abcaf7a420df35d2be9b3680c5)) +* 修改Dialog样式 ([e451bfc](https://github.com/kailong321200875/vue-element-plus-admin/commit/e451bfcde6e5a47d4b3022e240ffcc0576ebb9a8)) +* 修改样式 ([207c5b3](https://github.com/kailong321200875/vue-element-plus-admin/commit/207c5b3fc4e52bb06baa36cd4b659e14893785ba)) +* 完善角色管理 ([c4576bd](https://github.com/kailong321200875/vue-element-plus-admin/commit/c4576bd57bcf504733f20188202ea7d33ab1c184)) +* 布局样式优化 ([962689a](https://github.com/kailong321200875/vue-element-plus-admin/commit/962689a8bd0ed5eb17d946b8a21dec4a197f13a7)) +* 样式布局调整完成 ([7193176](https://github.com/kailong321200875/vue-element-plus-admin/commit/719317694f71e22692256bb557070343f034ffe5)) +* 用户管理样式修改 ([57a5fa7](https://github.com/kailong321200875/vue-element-plus-admin/commit/57a5fa7b82ae9f3d7a1f8ec5391f14b1d1cd32e8)) +* 移除不必要样式 ([7ef1d1e](https://github.com/kailong321200875/vue-element-plus-admin/commit/7ef1d1e3013cc5bf7fc574e67c2004f50792e66d)) +* 移除不必要样式 ([366db45](https://github.com/kailong321200875/vue-element-plus-admin/commit/366db4528254d18659e6a922817702b5b92a57b0)) +* 调整Icon悬停样式 ([64c7e48](https://github.com/kailong321200875/vue-element-plus-admin/commit/64c7e48bd18ba83e605daccbc4c2f4cc6b58695d)) +* 调整工作台样式错乱 ([cc18f29](https://github.com/kailong321200875/vue-element-plus-admin/commit/cc18f297ef50655d5773d01fcfddabc365dc53e7)) + + +### Performance Improvements + +* Dialog默认高度修改 ([0e04fce](https://github.com/kailong321200875/vue-element-plus-admin/commit/0e04fce4367d6829e8de97a249318b0309e06fd5)) +* Form Table Search Descriptions 支持嵌套赋值 ([46ddf62](https://github.com/kailong321200875/vue-element-plus-admin/commit/46ddf62d2d4ce1a653f47695cb0bb3475aa16bd8)) +* ImageViewer组件优化 ([3b9c3d8](https://github.com/kailong321200875/vue-element-plus-admin/commit/3b9c3d8b757646eaf74625403112a969bfd15e55)) +* 优化Form事件传递 ([69cafb3](https://github.com/kailong321200875/vue-element-plus-admin/commit/69cafb3b7b2ce7ecbd9f2e8ef09e250817e9a55c)) +* 优化Search组件 ([e548668](https://github.com/kailong321200875/vue-element-plus-admin/commit/e548668ccef8c41d9ac7d9fe39ffe66471d160d2)) +* 优化表单组件 ([77a3866](https://github.com/kailong321200875/vue-element-plus-admin/commit/77a38662488ab9ff4cbe5ff3cf9b65eea34abca1)) +* 优化锁屏组件 ([4f8330a](https://github.com/kailong321200875/vue-element-plus-admin/commit/4f8330a4faf6cc98a9bac17bd3e1719ae1b30c81)) + +## [1.9.9](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.8...v1.9.9) (2023-04-13) + + +### Bug Fixes + +* 使用动态路由时,多级路由只有一个子路且父路由未使用alwaysShow时,子路由未添加至路由中 ([9b330a1](https://github.com/kailong321200875/vue-element-plus-admin/commit/9b330a1f513d3af9233b9a9dde6bdfeeefbc3393)) + +## [1.9.8](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.7...v1.9.8) (2023-04-12) + + +### Bug Fixes + +* 修复已知问题 ([0a6f306](https://github.com/kailong321200875/vue-element-plus-admin/commit/0a6f306686ea024e30bcdccac34e485b8526e38f)) + +## [1.9.7](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.6...v1.9.7) (2023-03-28) + + +### Bug Fixes + +* 修复表格与搜索框字段不能不一致的问题 ([5c1cd29](https://github.com/kailong321200875/vue-element-plus-admin/commit/5c1cd298defefb36b858adc766b776a0a7b9bd66)) + +## [1.9.6](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.5...v1.9.6) (2023-03-22) + + +### Bug Fixes + +* 修改 Editor 的 z-index 使其不会遮挡『综合示例 - 新增』界面的下拉菜单 ([c046e45](https://github.com/kailong321200875/vue-element-plus-admin/commit/c046e4554ba8fd99614484d8fb636650072b833e)) + +## [1.9.5](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.4...v1.9.5) (2023-03-13) + + +### Bug Fixes + +* 面包屑:1.修复使用动态路由的时候,无法显示的bug ([8790c8c](https://github.com/kailong321200875/vue-element-plus-admin/commit/8790c8cbd8d63ea0f8f276fd5af006f39b06e7d3)) + +## [1.9.4](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.3...v1.9.4) (2023-03-03) + + +### Bug Fixes + +* 修复已知BUG ([782b8e2](https://github.com/kailong321200875/vue-element-plus-admin/commit/782b8e2f94c867c3ee282287c37c888fff93fc55)) + +## [1.9.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.2...v1.9.3) (2023-03-01) + + +### Types + +* 修复类型错误 ([297b2c6](https://github.com/kailong321200875/vue-element-plus-admin/commit/297b2c69a239b487126c3b9316645a1b5f06bb7c)) + + +### Styling + +* 抽屉弹出样式问题 ([16a3eef](https://github.com/kailong321200875/vue-element-plus-admin/commit/16a3eef85a1ffb296bd44f67d89a911ecaf1c25e)) +* 调整主题切换样式 ([6e6beef](https://github.com/kailong321200875/vue-element-plus-admin/commit/6e6beefc3c380f7297985adcabcf966fbd2c5197)) + +## [1.9.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.1...v1.9.2) (2023-01-16) + + +### Bug Fixes + +* 修复TS类型错误 ([1c06a27](https://github.com/kailong321200875/vue-element-plus-admin/commit/1c06a27b900a891cd0b47098062cebc984ff6505)) + +## [1.9.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.0...v1.9.1) (2023-01-11) + + +### Bug Fixes + +* dark mode toggle ([bbc7646](https://github.com/kailong321200875/vue-element-plus-admin/commit/bbc764601ec864c2fdbe3ad78c083c5ae80615e0)) + +## [1.9.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.7...v1.9.0) (2022-12-28) + + +### Features + +* 添加打包进度条 ([354e87f](https://github.com/kailong321200875/vue-element-plus-admin/commit/354e87f7c533ad8e93ef484b47d0fe16f17048c9)) + + +### Bug Fixes + +* husky ([6fe5b2e](https://github.com/kailong321200875/vue-element-plus-admin/commit/6fe5b2e6c781b251bff5f0ac936c04dcfe5ef95f)) + +## [1.8.7](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.6...v1.8.7) (2022-12-05) + + +### Bug Fixes + +* 解决iframe无法正常工作 ([4fcc46f](https://github.com/kailong321200875/vue-element-plus-admin/commit/4fcc46fccf747b47909e2079c4f2abc5dbfb1a0c)) + +## [1.8.6](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.5...v1.8.6) (2022-11-21) + + +### Bug Fixes + +* 修复Search组件无法默认值 ([3368fda](https://github.com/kailong321200875/vue-element-plus-admin/commit/3368fda251bd3ff5a8e0059b3b33f9c0339d236b)) + +## [1.8.5](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.4...v1.8.5) (2022-11-17) + + +### Bug Fixes + +* 修复Form赋值问题 ([f37cc1b](https://github.com/kailong321200875/vue-element-plus-admin/commit/f37cc1b5801add3ada168dbbcf4cd2c340f0e30d)) + +## [1.8.4](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.3...v1.8.4) (2022-11-07) + + +### Bug Fixes + +* 修复option禁用属性无效 ([0b671e9](https://github.com/kailong321200875/vue-element-plus-admin/commit/0b671e914e396c7666ad5e34768a6e29f7dfbd33)) + + +### Styling + +* input默认宽度与select对齐 ([0b671e9](https://github.com/kailong321200875/vue-element-plus-admin/commit/0b671e914e396c7666ad5e34768a6e29f7dfbd33)) + +## [1.8.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.2...v1.8.3) (2022-10-28) + + +### Performance Improvements + +* 优化描述组件 ([73ecc98](https://github.com/kailong321200875/vue-element-plus-admin/commit/73ecc98671d430013920246d98ce9ab1752e56eb)) + +## [1.8.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.1...v1.8.2) (2022-10-18) + + +### Bug Fixes + +* Correct spelling of words(aciton →action) ([eb405b2](https://github.com/kailong321200875/vue-element-plus-admin/commit/eb405b2a9041ca0ad4455db79bf617ec910dc485)) +* Correct spelling of words(tigger →trigger) ([c2ca2d7](https://github.com/kailong321200875/vue-element-plus-admin/commit/c2ca2d736c92e02380923a6741450844acb41a38)) + +## [1.8.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.0...v1.8.1) (2022-10-11) + + +### Bug Fixes + +* 修复cutMenu收起时 ([993af6b](https://github.com/kailong321200875/vue-element-plus-admin/commit/993af6bb6576249e66e0c0ea592ebf851f65ab8c)) + + +### Styling + +* cutMenu层级样式 ([32d2408](https://github.com/kailong321200875/vue-element-plus-admin/commit/32d2408588c487cff2cf73e3cc132e5105ff4459)) + +## [1.8.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.7.1...v1.8.0) (2022-10-10) + + +### Features + +* types优化 ([3351155](https://github.com/kailong321200875/vue-element-plus-admin/commit/33511553cd9055b036b2d7491f9c2eda123f8b22)) + + +### Styling + +* 优化第四种布局 ([122fa62](https://github.com/kailong321200875/vue-element-plus-admin/commit/122fa62d859413d16175e0d97c7bf13f232dbb3a)) + +## [1.7.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.7.0...v1.7.1) (2022-10-10) + + +### Bug Fixes + +* 修正types提示错误 ([ef3e006](https://github.com/kailong321200875/vue-element-plus-admin/commit/ef3e006859dcd8b93ffb7cffcaeae24cbb330f2a)) + +## [1.7.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.6...v1.7.0) (2022-10-09) + + +### Features + +* type抽离 ([8b4fa1a](https://github.com/kailong321200875/vue-element-plus-admin/commit/8b4fa1aa21aa2c1379288315ccd64a6f3375be51)) + +## [1.6.6](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.5...v1.6.6) (2022-10-09) + + +### Bug Fixes + +* table search params ([a62929a](https://github.com/kailong321200875/vue-element-plus-admin/commit/a62929a8dac21028d3dd1cddf98189492c33b093)) + +## [1.6.5](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.4...v1.6.5) (2022-10-08) + + +### Bug Fixes + +* The attribute of option does not work ([d946920](https://github.com/kailong321200875/vue-element-plus-admin/commit/d946920e61ed81beacf9f1f8be7ee1f50505f64d)) + + +### Performance Improvements + +* perf store ([d416178](https://github.com/kailong321200875/vue-element-plus-admin/commit/d416178d69ca6100be4b635922b1a22d27629f08)) +* token test ([b320e65](https://github.com/kailong321200875/vue-element-plus-admin/commit/b320e658d1a559a6eaebdf374d63649c223c2ecd)) + +## [1.6.4](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.3...v1.6.4) (2022-09-21) + + +### Bug Fixes + +* fix bug ([da39f3b](https://github.com/kailong321200875/vue-element-plus-admin/commit/da39f3bc904ca2d80f432a31709725f9a57deb19)) + +## [1.6.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.2...v1.6.3) (2022-08-20) + + +### Bug Fixes + +* 修复重定向错误 ([89d03fd](https://github.com/kailong321200875/vue-element-plus-admin/commit/89d03fd067e7aca565ceb84ea9276f340bbfcb60)) + + +### Styling + +* 调整样式 ([d29e151](https://github.com/kailong321200875/vue-element-plus-admin/commit/d29e151f8a660031a685f6ef9f789532b1b7b58b)) + +## [1.6.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.1...v1.6.2) (2022-08-13) + + +### Styling + +* Misspelling ([c43e833](https://github.com/kailong321200875/vue-element-plus-admin/commit/c43e833582e4f14ac78b0683f1eb3bdeb9fb4821)) +* perfect tableDemo ([c589edd](https://github.com/kailong321200875/vue-element-plus-admin/commit/c589edd960b23ad0c8b56d2c7880b61014114d45)) + + +### Code Refactoring + +* refactor axios ([0980640](https://github.com/kailong321200875/vue-element-plus-admin/commit/0980640f65fc80e3e58ba49e98db10b7b1b80077)) + +## [1.6.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.0...v1.6.1) (2022-07-30) + + +### Bug Fixes + +* fix menu active bug ([ff59fc7](https://github.com/kailong321200875/vue-element-plus-admin/commit/ff59fc7e133202945360a7e210f9cdf3a4a89dd7)) +* Pie chart data not updated ([55d4ce7](https://github.com/kailong321200875/vue-element-plus-admin/commit/55d4ce7e35ff9a0c5590bc3589160cfd304d3ae5)) + + +### Performance Improvements + +* add static router ([55522b0](https://github.com/kailong321200875/vue-element-plus-admin/commit/55522b0661a8df3ad3c8afafcc9f8fcb162c5a00)) + + +### Styling + +* tagviews styles update ([bff7d93](https://github.com/kailong321200875/vue-element-plus-admin/commit/bff7d9370db7a9c171828721bb99643dac2f235d)) + +## [1.6.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.5.4...v1.6.0) (2022-07-18) + + +### Features + +* 添加按钮权限 ([7bef662](https://github.com/kailong321200875/vue-element-plus-admin/commit/7bef662db1e91aa8164e9f7a92de3fe4480a3c3b)) + + +### Performance Improvements + +* 移除md5依赖 ([a123097](https://github.com/kailong321200875/vue-element-plus-admin/commit/a123097f1f38eac45e945c7d3fdccafc16ea9b69)) + + +### Styling + +* 压缩图片尺寸 ([ae3c565](https://github.com/kailong321200875/vue-element-plus-admin/commit/ae3c5657b604ffbdae5ce3ce3603626ad4acc5e5)) + + +### Docs + +* vite2 to vite3 ([b3918b9](https://github.com/kailong321200875/vue-element-plus-admin/commit/b3918b9c3c5de4b48811ec95967851cfb3c231e1)) +* vite2 to vite3 ([aaf07de](https://github.com/kailong321200875/vue-element-plus-admin/commit/aaf07de77aa600332880a894faa35757f787c012)) + +## [1.6.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.5.4...v1.6.0) (2022-07-18) + + +### Features + +* 添加按钮权限 ([7bef662](https://github.com/kailong321200875/vue-element-plus-admin/commit/7bef662db1e91aa8164e9f7a92de3fe4480a3c3b)) + + +### Performance Improvements + +* 移除md5依赖 ([a123097](https://github.com/kailong321200875/vue-element-plus-admin/commit/a123097f1f38eac45e945c7d3fdccafc16ea9b69)) + + +### Styling + +* 压缩图片尺寸 ([ae3c565](https://github.com/kailong321200875/vue-element-plus-admin/commit/ae3c5657b604ffbdae5ce3ce3603626ad4acc5e5)) + + +### Docs + +* vite2 to vite3 ([b3918b9](https://github.com/kailong321200875/vue-element-plus-admin/commit/b3918b9c3c5de4b48811ec95967851cfb3c231e1)) +* vite2 to vite3 ([aaf07de](https://github.com/kailong321200875/vue-element-plus-admin/commit/aaf07de77aa600332880a894faa35757f787c012)) + +## [1.5.4](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.5.3...v1.5.4) (2022-07-16) + + +### Bug Fixes + +* fix build:test error ([14530cf](https://github.com/kailong321200875/vue-element-plus-admin/commit/14530cf790bfbbe37c72fa831f0376bbb4209e9d)) + +## [1.5.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.5.2...v1.5.3) (2022-07-01) + + +### Bug Fixes + +* fix useCrudSchemas not work ([0a855b9](https://github.com/kailong321200875/vue-element-plus-admin/commit/0a855b93e282dfe7863b3fce31dde5d7e0d3e2b6)) + +## [1.5.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.5.1...v1.5.2) (2022-07-01) + + +### Performance Improvements + +* add useCrudSchemas demo ([ca3ce54](https://github.com/kailong321200875/vue-element-plus-admin/commit/ca3ce54630b723d87415b14c642440d6734876ff)) + +## [1.5.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.5.0...v1.5.1) (2022-07-01) + + +### Bug Fixes + +* change showMainRoute to canTo ([5e292f8](https://github.com/kailong321200875/vue-element-plus-admin/commit/5e292f8a2b2ded7297a2a76893e113ac81517d23)) + + +### Performance Improvements + +* dialog combine with form ([34aefb6](https://github.com/kailong321200875/vue-element-plus-admin/commit/34aefb64ab9237521a1225925264818eebff9ad3)) +* dynamic options demo ([1acb4d7](https://github.com/kailong321200875/vue-element-plus-admin/commit/1acb4d7e8f449ba342699f1b4387ac2404a4c1fb)) +* dynamic options demo ([9a3b617](https://github.com/kailong321200875/vue-element-plus-admin/commit/9a3b6177aa0fbc99c86c5073a1c6c696e1eaf890)) +* useCrudSchemas cutom label ([7864d83](https://github.com/kailong321200875/vue-element-plus-admin/commit/7864d830e2134d814609e722b7bad1754ea9460e)) + +## [1.5.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.4.5...v1.5.0) (2022-06-25) + + +### Features + +* refactoring API ([37b7583](https://github.com/kailong321200875/vue-element-plus-admin/commit/37b75839a591648b145065432efb1dc8c7a3b917)) + + +### Bug Fixes + +* 修复axios已知问题 ([537af57](https://github.com/kailong321200875/vue-element-plus-admin/commit/537af57a0aaa24c88ebe75acf52dc0403a58b04b)) + + +### Performance Improvements + +* perf axios config ([39edd84](https://github.com/kailong321200875/vue-element-plus-admin/commit/39edd84023109a84683c21ea33e41bd024756520)) + +## [1.4.5](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.4.4...v1.4.5) (2022-06-09) + + +### Bug Fixes + +* fix tagsview not work ([d88e051](https://github.com/kailong321200875/vue-element-plus-admin/commit/d88e0514349e877f1c5280a48f9b1bd2bfd622bf)) +* fix tagsview not work ([1bf2d4c](https://github.com/kailong321200875/vue-element-plus-admin/commit/1bf2d4c77287fdca7ed1cb8c9998a53f1375dc6f)) + + +### Types + +* fix types error ([586486a](https://github.com/kailong321200875/vue-element-plus-admin/commit/586486a68d4bf2a024e50a79945b4007324f642d)) + +## [1.4.4](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.4.3...v1.4.4) (2022-06-06) + + +### Types + +* fix type error ([d66f12e](https://github.com/kailong321200875/vue-element-plus-admin/commit/d66f12e0e77f6acf485bae06509d9ea4abcd1eaa)) + +### [1.4.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.4.2...v1.4.3) (2022-06-01) + + +### Bug Fixes + +* multiple requests when pageSize change and currentPage isn't 1 ([f71a250](https://github.com/kailong321200875/vue-element-plus-admin/commit/f71a2503bc521c01e7102feecf4ec39a5224a6bb)) + +### [1.4.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.4.1...v1.4.2) (2022-05-15) + + +### Styling + +* fix dark mode bug ([2f9fd5d](https://github.com/kailong321200875/vue-element-plus-admin/commit/2f9fd5d21550d771ec12ae3540b975e2eebcd25b)) + +### [1.4.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.4.0...v1.4.1) (2022-05-12) + + +### Bug Fixes + +* the warning of VSCode extensions ([a368c21](https://github.com/kailong321200875/vue-element-plus-admin/commit/a368c21fb9c41f98f31f51586a2023076a8a9132)) + +## [1.4.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.3.2...v1.4.0) (2022-05-10) + + +### Features + +* add dark mode ([0758a6a](https://github.com/kailong321200875/vue-element-plus-admin/commit/0758a6a9d83170e53d45d39c3313e52ff5885746)) + +### [1.3.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.3.1...v1.3.2) (2022-05-07) + + +### Bug Fixes + +* fix the problem that the page is stuck in top mode ([8d01f48](https://github.com/kailong321200875/vue-element-plus-admin/commit/8d01f48d5098195b10c03b3cb3a0f485ebc9e018)) + +### [1.3.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.3.0...v1.3.1) (2022-05-06) + + +### Bug Fixes + +* spelling 'useRenderChcekbox' ([ee92f03](https://github.com/kailong321200875/vue-element-plus-admin/commit/ee92f039bea4307ccfb819728d3e2ed04fa00e03)) + +## [1.3.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.13...v1.3.0) (2022-04-26) + + +### Features + +* add useCrudSchemas hook ([00d947e](https://github.com/kailong321200875/vue-element-plus-admin/commit/00d947e2f81105194b0622d33768f999e37ad28a)) + + +### Bug Fixes + +* fix Table slot warning ([0eac05d](https://github.com/kailong321200875/vue-element-plus-admin/commit/0eac05d4f973ff7b15e00973f6e96595a3cd6d43)) + + +### Code Refactoring + +* refactor useAxios ([185f1e6](https://github.com/kailong321200875/vue-element-plus-admin/commit/185f1e6e210ecaac28ebfdee4198b7ca2eaa0933)) + + +### Build System + +* add url ([ab0f59a](https://github.com/kailong321200875/vue-element-plus-admin/commit/ab0f59ac91a077cf060923fa76e6d57e05d0b21b)) +* update plugins ([c475a61](https://github.com/kailong321200875/vue-element-plus-admin/commit/c475a610c19094034306f2dc665e240c7c117f87)) +* update plugins ([dfedbc7](https://github.com/kailong321200875/vue-element-plus-admin/commit/dfedbc74fdb2c819a96b6263849bdaab59b9e337)) + +### [1.2.13](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.12...v1.2.13) (2022-04-18) + + +### Performance Improvements + +* Editor component support v-model ([d77f8e3](https://github.com/kailong321200875/vue-element-plus-admin/commit/d77f8e334d77ee43c9ee0f411733f7397b278bc0)) + +### [1.2.12](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.11...v1.2.12) (2022-04-17) + + +### Bug Fixes + +* fixed spelling 'ElememtPlusSzie' ([5dbbc60](https://github.com/kailong321200875/vue-element-plus-admin/commit/5dbbc608640d93fe68fec6f58fdb30a43e02aada)) + +### [1.2.11](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.10...v1.2.11) (2022-04-14) + + +### Bug Fixes + +* fix the error reported by the Editor component ([7dc6d8a](https://github.com/kailong321200875/vue-element-plus-admin/commit/7dc6d8a9d7289bfaf27f972e9ca1773c0a1ddd7d)) +* fix the error reported by the Editor component ([90ef985](https://github.com/kailong321200875/vue-element-plus-admin/commit/90ef9856a0885fa812339cb7047ecc98b86c7b73)) + + +### Performance Improvements + +* add tagsViewIcon setting ([d395f03](https://github.com/kailong321200875/vue-element-plus-admin/commit/d395f03a57a9265f1d39b3220fc7c9b983efee30)) +* add uniqueopened setting ([b060319](https://github.com/kailong321200875/vue-element-plus-admin/commit/b0603199a5ae0ee923483dad449f49220d36f444)) + + +### Build System + +* update plugins ([2ee4954](https://github.com/kailong321200875/vue-element-plus-admin/commit/2ee49549e7b601af26ef5204f7648d271f3348f2)) + + +### Styling + +* add layout background color ([9b614fe](https://github.com/kailong321200875/vue-element-plus-admin/commit/9b614fe89288538197c50f164586aeed7836b7a8)) + +### [1.2.10](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.9...v1.2.10) (2022-04-12) + + +### Bug Fixes + +* fix bug ([327522f](https://github.com/kailong321200875/vue-element-plus-admin/commit/327522f2b73ae0e11f8ebbc39394b06029ce0b65)) + + +### Styling + +* modify the commitlint package manager ([ba7e722](https://github.com/kailong321200875/vue-element-plus-admin/commit/ba7e7224ab58612548519415f5429c32827a61de)) + +### [1.2.9](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.8...v1.2.9) (2022-04-12) + + +### Docs + +* update changlog ([e37273d](https://github.com/kailong321200875/vue-element-plus-admin/commit/e37273d95d29a3bb752604658d550264aacdc979)) + +### [1.2.8](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.7...v1.2.8) (2022-04-11) + +### Build System + +- update plugins ([00a573a](https://github.com/kailong321200875/vue-element-plus-admin/commit/00a573af3f455395b4ee2ab99a03f3103d466e9c)) + +### Docs + +- update changlog ([62fc183](https://github.com/kailong321200875/vue-element-plus-admin/commit/62fc1839fdff3a4d06a7db4cf3f8ce2cb9aee681)) + +### [1.2.7](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.6...v1.2.7) (2022-04-10) + +### Build System + +- update plugins ([f13a91d](https://github.com/kailong321200875/vue-element-plus-admin/commit/f13a91dd460b1dcdbd17aef723ab3ca2b01c34f0)) + +### Styling + +- .bhs code formatting ([57b2707](https://github.com/kailong321200875/vue-element-plus-admin/commit/57b27071e9a33423c46542a5d0e5d5c2e9a3b718)) + +### [1.2.6](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.5...v1.2.6) (2022-04-08) + +### Build System + +- update plugins ([d645892](https://github.com/kailong321200875/vue-element-plus-admin/commit/d645892cde2f7f43215a2ba1776ee94a322437bf)) + +### [1.2.5](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.4...v1.2.5) (2022-04-08) + +### Performance Improvements + +- add plop ([fa54a17](https://github.com/kailong321200875/vue-element-plus-admin/commit/fa54a1704ffd93f7b42dbeb1229bc4868d2d3a6a)) + +### Build System + +- update plugins ([18c6bd8](https://github.com/kailong321200875/vue-element-plus-admin/commit/18c6bd868622d954b51ea34e37516361ad4eb540)) + +### Styling + +- fix padding and background color ([f8c9d54](https://github.com/kailong321200875/vue-element-plus-admin/commit/f8c9d54687edafd92f5b61bf5288bb1188c73f01)) + +### [1.2.4](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.3...v1.2.4) (2022-04-06) + +### Bug Fixes + +- add Sticky props comment ([46133b3](https://github.com/kailong321200875/vue-element-plus-admin/commit/46133b3ff39d48d11cbcaa1f20a271118f48eb29)) +- fix bug ([179ca06](https://github.com/kailong321200875/vue-element-plus-admin/commit/179ca064ba8adbb3b063d9798ec1930ccc68e459)) +- fix remove unnecessary variables ([ca01cbf](https://github.com/kailong321200875/vue-element-plus-admin/commit/ca01cbfd98b63a0d76190fe8d43097fdc9df74e6)) +- fix style ([17c8fea](https://github.com/kailong321200875/vue-element-plus-admin/commit/17c8fea93811d9d9b708808484f5c907d761fcf1)) +- remove ContentDetailWrap style ([4ceaa9d](https://github.com/kailong321200875/vue-element-plus-admin/commit/4ceaa9d7816369d0dcaf3e18e4cdbbd6165cef88)) + +### [1.2.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.2...v1.2.3) (2022-03-31) + +### Bug Fixes + +- fix refresh with query ([e94020f](https://github.com/kailong321200875/vue-element-plus-admin/commit/e94020ff541a061599486c0003258f1dbf13aba8)) + +### [1.2.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.1...v1.2.2) (2022-03-30) + +### Bug Fixes + +- fix avatar height bug ([cd4ab76](https://github.com/kailong321200875/vue-element-plus-admin/commit/cd4ab767018941777174d7837045f5259d1cc403)) +- fix parmas to params ([2c7211c](https://github.com/kailong321200875/vue-element-plus-admin/commit/2c7211c89d7299ffc0a36bef8999b3c201dbaf4a)) + +### [1.2.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.2.0...v1.2.1) (2022-03-29) + +### Bug Fixes + +- fix invalid paging ([ad184ee](https://github.com/kailong321200875/vue-element-plus-admin/commit/ad184ee9c0619da36f1ca3f26e67f18f88488523)) + +### Build System + +- update plugins ([0c7276f](https://github.com/kailong321200875/vue-element-plus-admin/commit/0c7276feadaedef83e6a4ad9d457e26d408698a8)) + +## [1.2.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.14...v1.2.0) (2022-03-27) + +### Features + +- add hooks demo ([c43f39e](https://github.com/kailong321200875/vue-element-plus-admin/commit/c43f39efef296266c64cc24690717d07fa0bcb85)) +- add inputPassword demo ([8f8b126](https://github.com/kailong321200875/vue-element-plus-admin/commit/8f8b1260e75df6998ebea617f62d4ab6be81d721)) + +### Docs + +- update LICENSE ([69d3dcc](https://github.com/kailong321200875/vue-element-plus-admin/commit/69d3dcc7edf69e9b4e3042ddb11faa85ec7d39e2)) + +### Styling + +- modify the function name to make it more semantic ([046ae51](https://github.com/kailong321200875/vue-element-plus-admin/commit/046ae512f02df2d3f08134949b9376a061c1eef3)) +- update Footer component presentation ([d4a9ba3](https://github.com/kailong321200875/vue-element-plus-admin/commit/d4a9ba3aa6758b8aac18b30e1a6b9501baff826c)) +- update Icon demo ([8597122](https://github.com/kailong321200875/vue-element-plus-admin/commit/85971227cd3055ea280cf493c7c42b250c1515da)) + +### Tests + +- test push first commit ([a67bb48](https://github.com/kailong321200875/vue-element-plus-admin/commit/a67bb48f269651a2dcd01b9e33d10f20c42d76ee)) + +### Build System + +- update plugins ([9c13d92](https://github.com/kailong321200875/vue-element-plus-admin/commit/9c13d92b36a2a7c95b9edb7821367fc8f0ac6658)) +- update server port ([d2be8c1](https://github.com/kailong321200875/vue-element-plus-admin/commit/d2be8c1a307a3c5daf363bd7f1d21e574598de5c)) + +### [1.1.14](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.13...v1.1.14) (2022-03-22) + +### Bug Fixes + +- fix the bug that the form search function is invalid of the example-dialog page ([9ec30e7](https://github.com/kailong321200875/vue-element-plus-admin/commit/9ec30e719f89865497dbb1321be1df906f59f14e)) + +### [1.1.13](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.12...v1.1.13) (2022-03-17) + +### Build System + +- update plugins ([a2d0313](https://github.com/kailong321200875/vue-element-plus-admin/commit/a2d03137899f9b16fc1d4a09a23576cd74e7950e)) + +### [1.1.12](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.11...v1.1.12) (2022-03-15) + +### Build System + +- update plugins ([fee2252](https://github.com/kailong321200875/vue-element-plus-admin/commit/fee2252930b05b709d0c012e809568c4ed32bd89)) + +### [1.1.11](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.10...v1.1.11) (2022-03-15) + +### Bug Fixes + +- fix the problem of blank bar when toggle the TagsView component ([b1d9771](https://github.com/kailong321200875/vue-element-plus-admin/commit/b1d9771c750709fe45061d13299a85dbbd6ead25)) +- fix the problem that no reaction when copy setting config in http page ([61e0e33](https://github.com/kailong321200875/vue-element-plus-admin/commit/61e0e33c64d6a889fe6ed80d27a10cf8b201d21a)) + +### [1.1.10](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.9...v1.1.10) (2022-03-13) + +### Build System + +- update plugins ([0b525c8](https://github.com/kailong321200875/vue-element-plus-admin/commit/0b525c875075a28288e92243b205b337f85ab550)) + +### [1.1.9](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.8...v1.1.9) (2022-03-07) + +### Build System + +- update plugins ([1456fd4](https://github.com/kailong321200875/vue-element-plus-admin/commit/1456fd49ec9abbfe1f25aeadfe5fed54fec07394)) + +### [1.1.8](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.7...v1.1.8) (2022-03-07) + +### Docs + +- update changelog ([bf09441](https://github.com/kailong321200875/vue-element-plus-admin/commit/bf09441852e59b0d07d4949a33de75958696817f)) + +### [1.1.7](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.6...v1.1.7) (2022-03-06) + +### Styling + +- add labelMessage attribute to Form component ([8c42790](https://github.com/kailong321200875/vue-element-plus-admin/commit/8c427907843ccb2dfd882d27c1e8a894c5616487)) + +### [1.1.6](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.5...v1.1.6) (2022-03-04) + +### Bug Fixes + +- fix the problem that the tree data of Table component cannot be displayed ([bf83d3e](https://github.com/kailong321200875/vue-element-plus-admin/commit/bf83d3efbad9097f245c32cc07d1178580cec4e3)) + +### [1.1.5](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.4...v1.1.5) (2022-03-02) + +### Bug Fixes + +- fix the problem of tagsview error when loginout ([835d76a](https://github.com/kailong321200875/vue-element-plus-admin/commit/835d76ae87b950106f957976ebc8f6f2e8842ddf)) + +### Build System + +- update plugins ([de34bb1](https://github.com/kailong321200875/vue-element-plus-admin/commit/de34bb193d6c844dbc1cec38db5f61b3f95e19f2)) + +### Styling + +- fix tabMenu z-index bug ([8b3be02](https://github.com/kailong321200875/vue-element-plus-admin/commit/8b3be02368a1bddb7dc78f18adbea7f4ebfe75d6)) +- fix tags-view style bug ([ebff817](https://github.com/kailong321200875/vue-element-plus-admin/commit/ebff81777b9c0b839256b83e321ecbdbff25fc73)) + +### [1.1.4](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.3...v1.1.4) (2022-03-01) + +### CI + +- update workflow ([0490d18](https://github.com/kailong321200875/vue-element-plus-admin/commit/0490d18145cb0d9c4b066ab01a2c10cb527e38ba)) +- update workflow ([51f7bca](https://github.com/kailong321200875/vue-element-plus-admin/commit/51f7bca6034902b251d081ee383b0d796782d434)) + +### [1.1.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.2...v1.1.3) (2022-03-01) + +### CI + +- update workflow ([91cc5c5](https://github.com/kailong321200875/vue-element-plus-admin/commit/91cc5c595cadc5695d8f54bdc4922d8f04439f24)) + +### [1.1.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.1...v1.1.2) (2022-03-01) + +### Workflows + +- update workflow ([d9708aa](https://github.com/kailong321200875/vue-element-plus-admin/commit/d9708aae5bc0cb795bb0fbf8d17df753cc88ba1d)) + +### [1.1.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.1.0...v1.1.1) (2022-03-01) + +### Workflows + +- update workflow ([085328a](https://github.com/kailong321200875/vue-element-plus-admin/commit/085328aba8c4f356bf7915a6bbdc1ec4f46ceeda)) + +## [1.1.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.0.3...v1.1.0) (2022-03-01) + +### Features + +- 🎸 layout 三种布局重构完成 ([429e428](https://github.com/kailong321200875/vue-element-plus-admin/commit/429e42809cef33a33662e41ad50297217d128b8c)) +- 🎸 layout 布局重构 �[bd24b92](https://github.com/kailong321200875/vue-element-plus-admin/commit/bd24b92acb279343dbaf83b74f1ed2a3f57f1003)) +- 🎸 Table 组件重构完成并给出相应示 �[35879f8](https://github.com/kailong321200875/vue-element-plus-admin/commit/35879f8ecc0ffa76122a336e2eaa93ecfb408c1d)) +- 🎸 v0.0.4 发布 ([a58dc1b](https://github.com/kailong321200875/vue-element-plus-admin/commit/a58dc1b1c2774974782ef6d116b8805975b82b1c)) +- 🎸 初始化项 �[26d4c7c](https://github.com/kailong321200875/vue-element-plus-admin/commit/26d4c7c56894cf2031b3a7cce08d53c37f4a49e3)) +- 🎸 初版完成 ([5bfe4d2](https://github.com/kailong321200875/vue-element-plus-admin/commit/5bfe4d236fd9c2841da100f34c980b4572b67b20)) +- 🎸 新增 Detail 详情组件并给出相应示 �[e77a931](https://github.com/kailong321200875/vue-element-plus-admin/commit/e77a931ef2d2967a9717e27b187d68512c01284f)) +- 🎸 新增二维码组 �[85555ee](https://github.com/kailong321200875/vue-element-plus-admin/commit/85555eef7dc7d72cb701bdd81044ba8fb8e72acc)) +- 🎸 新增全局配置 ([f8405a6](https://github.com/kailong321200875/vue-element-plus-admin/commit/f8405a63c9b1288fbe95bae235b65a08e8fae8d2)) +- 🎸 新增固定 � 级菜单配 �[4c4903e](https://github.com/kailong321200875/vue-element-plus-admin/commit/4c4903e806c8818e320108cc3e5279d728061c29)) +- 🎸 新增权限管理及相关示例文 �[32b6583](https://github.com/kailong321200875/vue-element-plus-admin/commit/32b6583099646b2ee622ac7b35388468769b91b8)) +- 🎸 显示更多组建 � 发中 ([fa9f24d](https://github.com/kailong321200875/vue-element-plus-admin/commit/fa9f24d5da8d2e40d7c3661eabacb8f0474a7bf2)) +- 🎸 权限管理 � 发中 ([38f5211](https://github.com/kailong321200875/vue-element-plus-admin/commit/38f521174ba9eba750fee4516141d7a267f1c4ce)) +- 🎸 权限管理 � 发中 ([6d7ea66](https://github.com/kailong321200875/vue-element-plus-admin/commit/6d7ea6694d8299332018a6689bcd82502a9a552c)) +- 🎸 综合实例重构 �[5142e6e](https://github.com/kailong321200875/vue-element-plus-admin/commit/5142e6e323cb20c89a97398bf41d32c93ce42cad)) +- 🎸 重构 layout ([7ede021](https://github.com/kailong321200875/vue-element-plus-admin/commit/7ede02141e258ab4c88e9b4daad966513d4dbe68)) +- 🎸 重构 layout-classic 布局 ([29d9c98](https://github.com/kailong321200875/vue-element-plus-admin/commit/29d9c988605b822195900268da6bc3f3b0b9c770)) +- 🎸 重构 sider 组件 �[51313d7](https://github.com/kailong321200875/vue-element-plus-admin/commit/51313d7116c7ab2ded7e3a65514ea9ac413edecd)) +- Add analysis api ([83327ea](https://github.com/kailong321200875/vue-element-plus-admin/commit/83327ea763ebb233bb540513276ffa288fbcb4a1)) +- Add analysis demo ([cd06934](https://github.com/kailong321200875/vue-element-plus-admin/commit/cd069340fc5157535fdc82e792c6b6dce7d7a97e)) +- Add count-to demo ([d3fbd3a](https://github.com/kailong321200875/vue-element-plus-admin/commit/d3fbd3a06c3b802fc863b4dc8013122c14bd16f2)) +- Add Descriptions component and add Descriptions demo ([7ad46f8](https://github.com/kailong321200875/vue-element-plus-admin/commit/7ad46f828d626a87699cd4d3a959a5634577d580)) +- Add Dialog component and add dailog demo ([a18ad8f](https://github.com/kailong321200875/vue-element-plus-admin/commit/a18ad8f4a89b78c73e57d8d2543494243f656d05)) +- add doucment link ([53201ae](https://github.com/kailong321200875/vue-element-plus-admin/commit/53201ae97a425714871d99e8847a3672ba0d389f)) +- Add dynamic route ([9d926b2](https://github.com/kailong321200875/vue-element-plus-admin/commit/9d926b2760b75e1d8e71a68dc7ff6c5026223a43)) +- Add Editor component and add editor demo ([3fb3e8d](https://github.com/kailong321200875/vue-element-plus-admin/commit/3fb3e8da39d816bcf4aedb65d40c7052bdb6d8bf)) +- Add Error component ([7411dbc](https://github.com/kailong321200875/vue-element-plus-admin/commit/7411dbc9fd8f122187c86a11523b49c88cc71a8c)) +- Add example-dialog demo ([262f421](https://github.com/kailong321200875/vue-element-plus-admin/commit/262f4211cf53aef30a32f4b88e88fb1b9246ffcb)) +- Add example-page demo ([1492f91](https://github.com/kailong321200875/vue-element-plus-admin/commit/1492f9119aa2960cc05956218e6d151c8b316875)) +- Add form demo ([472f574](https://github.com/kailong321200875/vue-element-plus-admin/commit/472f574f42f8f31c4e6047043ac755ba5fb35b7b)) +- Add form demo ([e6f9580](https://github.com/kailong321200875/vue-element-plus-admin/commit/e6f95803316bb5df2d1060285c1d591a79340721)) +- Add form demo ([543156f](https://github.com/kailong321200875/vue-element-plus-admin/commit/543156f328350bd12e71a41c872e547e41cda7fe)) +- Add form demo ([7795d2a](https://github.com/kailong321200875/vue-element-plus-admin/commit/7795d2a4fe3dbc9849ddc7c1d3e2d9215dc66f56)) +- Add guide demo ([0832194](https://github.com/kailong321200875/vue-element-plus-admin/commit/0832194e6131051416edff7c2eac6b0a016ffd80)) +- Add highlight demo ([eb206b0](https://github.com/kailong321200875/vue-element-plus-admin/commit/eb206b0cc31ac7da3dfd8b3d4b874061c5c91d53)) +- Add Icon demo ([e4b7a76](https://github.com/kailong321200875/vue-element-plus-admin/commit/e4b7a769126d6f0fca424007c294ff229eefcb35)) +- Add ImageViewer component and add ImageViewer demo ([af9fc0a](https://github.com/kailong321200875/vue-element-plus-admin/commit/af9fc0a4aded3ec08746ddeaeabac4c3cfa9463d)) +- Add Infotip component ([e4b7a76](https://github.com/kailong321200875/vue-element-plus-admin/commit/e4b7a769126d6f0fca424007c294ff229eefcb35)) +- Add infotip demo ([dbf3b0f](https://github.com/kailong321200875/vue-element-plus-admin/commit/dbf3b0f5a333ccef524bbac825035b0c6dc78ec9)) +- Add Qrcode component and add qrcode demo ([535a31b](https://github.com/kailong321200875/vue-element-plus-admin/commit/535a31b35eb6a76589f602fd96dcf91f46f349b0)) +- Add Search component and add search demo ([33eca8a](https://github.com/kailong321200875/vue-element-plus-admin/commit/33eca8a97d59f5cc453e1a60ee81b1519527d0f1)) +- Add Table component and add useTable hook ([17e8e7c](https://github.com/kailong321200875/vue-element-plus-admin/commit/17e8e7cda9a009818f11cfa0429ce0f9adc00be5)) +- Add useScrollTo hook ([7d7fd9e](https://github.com/kailong321200875/vue-element-plus-admin/commit/7d7fd9ed646d2b68cec0547ad8e65b0404bb95bb)) +- Add useWatermark hook and add useWatermark demo ([d3fbd3a](https://github.com/kailong321200875/vue-element-plus-admin/commit/d3fbd3a06c3b802fc863b4dc8013122c14bd16f2)) +- Add workplace api ([cb558f8](https://github.com/kailong321200875/vue-element-plus-admin/commit/cb558f8af9dfef2ba2879f021db395ee79e8c8d4)) +- **Animate:** Add animate.css ([1436543](https://github.com/kailong321200875/vue-element-plus-admin/commit/1436543a5c599f651ed7805165ea83b9ebcddef5)) +- **Breadcrumbe:** Add Breadcrumb component ([4612e55](https://github.com/kailong321200875/vue-element-plus-admin/commit/4612e5544bcd626d686972e5cb874d0aa4af08b3)) +- **component:** Add CountTo component and Echart component ([e20fa76](https://github.com/kailong321200875/vue-element-plus-admin/commit/e20fa76cad0894a69fd04c81c2108faabf392684)) +- **component:** Add Footer component ([dad7330](https://github.com/kailong321200875/vue-element-plus-admin/commit/dad733063413c79eca61c6cb5ff671b35933a85f)) +- **component:** Add Footer component ([f81e996](https://github.com/kailong321200875/vue-element-plus-admin/commit/f81e996a426538aeaa2aa37a540395dcf360a09c)) +- **Component:** Add Highlight component ([c53fa56](https://github.com/kailong321200875/vue-element-plus-admin/commit/c53fa562e540447df082e35c7f26e56f2426e430)) +- **component:** Add namespace of class ([d847ccb](https://github.com/kailong321200875/vue-element-plus-admin/commit/d847ccb098edc72fe55c1f8459bf149453a3b73d)) +- **Component:** Setting component add copy button ([e496096](https://github.com/kailong321200875/vue-element-plus-admin/commit/e496096539e6a56b0761a625c9d59210facc5432)) +- **ContextMenu:** Add ContextMenu component ([349ac9d](https://github.com/kailong321200875/vue-element-plus-admin/commit/349ac9d3989d77e5246cecf0006dd8d83c489990)) +- Detail 组件重构完成 ([7f5ef99](https://github.com/kailong321200875/vue-element-plus-admin/commit/7f5ef99ccc32b03f7be21f70c333bb8e679c7d93)) +- Highlight 组件重构 ([34221f3](https://github.com/kailong321200875/vue-element-plus-admin/commit/34221f387f5e15a08cdc21edd76ce8d8c5c20fbc)) +- **hooks:** Add useIntro hook ([0832194](https://github.com/kailong321200875/vue-element-plus-admin/commit/0832194e6131051416edff7c2eac6b0a016ffd80)) +- **hooks:** Add useTimeAgo hook ([c53fa56](https://github.com/kailong321200875/vue-element-plus-admin/commit/c53fa562e540447df082e35c7f26e56f2426e430)) +- **I18n:** Add Ii8n ([3810b8c](https://github.com/kailong321200875/vue-element-plus-admin/commit/3810b8c3b26f86c27aa7db479dfb7b0d283d970f)) +- **Layout:** Add classic layout ([839b601](https://github.com/kailong321200875/vue-element-plus-admin/commit/839b6015b8e31bf70e6f0bf0608fa729b028729b)) +- **Layout:** Add cutMenu layout ([ff4dd3a](https://github.com/kailong321200875/vue-element-plus-admin/commit/ff4dd3afbf5c0c7a439c71b0c494b81e0f2c70d4)) +- **Layout:** Add topLeft layout ([71b1c5e](https://github.com/kailong321200875/vue-element-plus-admin/commit/71b1c5e10cade8d1c018d0c5f63c98ba9357bab8)) +- **LocaleDropdown:** Add LocaleDropdown Component ([3810b8c](https://github.com/kailong321200875/vue-element-plus-admin/commit/3810b8c3b26f86c27aa7db479dfb7b0d283d970f)) +- **Logo:** Add Logo component ([958edef](https://github.com/kailong321200875/vue-element-plus-admin/commit/958edefe7bc2bf3ae77520a5d885a9d47e8a37b9)) +- **mock:** Add mock ([3fc7d4d](https://github.com/kailong321200875/vue-element-plus-admin/commit/3fc7d4d39a72056fcf419fe19a9d41d90f945bad)) +- **router:** Add dynamic routing ([b218ccc](https://github.com/kailong321200875/vue-element-plus-admin/commit/b218ccc9cce2ce1363c4a21d22b4d69c43c7b2dc)) +- Search component add expand attribute and expandField attribute ([9b4b317](https://github.com/kailong321200875/vue-element-plus-admin/commit/9b4b31781765d31dec50acc40e2eed91401502d4)) +- **store:** Add localeStore ([3810b8c](https://github.com/kailong321200875/vue-element-plus-admin/commit/3810b8c3b26f86c27aa7db479dfb7b0d283d970f)) +- **store:** Add tagsView store ([349ac9d](https://github.com/kailong321200875/vue-element-plus-admin/commit/349ac9d3989d77e5246cecf0006dd8d83c489990)) +- Table 组件重构 ([07adefb](https://github.com/kailong321200875/vue-element-plus-admin/commit/07adefb89b7555280e6217e09cf81ba7aa5b93c2)) +- **TagsView:** Add TagsView component ([349ac9d](https://github.com/kailong321200875/vue-element-plus-admin/commit/349ac9d3989d77e5246cecf0006dd8d83c489990)) +- **useForm:** Add useForm ([357fc44](https://github.com/kailong321200875/vue-element-plus-admin/commit/357fc44e519c5829567c17f611fcaadee3f9f933)) +- **useNProgress:** Add useNProgress ([c5ab359](https://github.com/kailong321200875/vue-element-plus-admin/commit/c5ab3599c8ea001ff7831b72fefc9e274163fbbb)) +- **useTitle:** Add useTitle ([c5ab359](https://github.com/kailong321200875/vue-element-plus-admin/commit/c5ab3599c8ea001ff7831b72fefc9e274163fbbb)) +- **utils:** Add color utils ([71dfba2](https://github.com/kailong321200875/vue-element-plus-admin/commit/71dfba21c5bc0276689b5aecf0d75e53efdda09f)) +- **VForm:** Add VForm component ([448ac52](https://github.com/kailong321200875/vue-element-plus-admin/commit/448ac5293e48a03840df2bb0b399a8f02aae666e)) +- **VInputPassword:** Add VInputPassword Component ([a1bf7e9](https://github.com/kailong321200875/vue-element-plus-admin/commit/a1bf7e9b552f75d3b87c64904ac9e7c99fc936a5)) +- **Workplace:** Add wrokplace demo ([c53fa56](https://github.com/kailong321200875/vue-element-plus-admin/commit/c53fa562e540447df082e35c7f26e56f2426e430)) +- 综合实例、权限管理重 �[a4bd206](https://github.com/kailong321200875/vue-element-plus-admin/commit/a4bd2068a5d40d146b5b45cb3727ced990147b68)) +- 部分组件重构完成 ([3d96229](https://github.com/kailong321200875/vue-element-plus-admin/commit/3d9622978dc234ef12dbce63e18caf3440563aa0)) + +### Bug Fixes + +- 🐛 删除 Editor 双向绑定,改 �props 传参 ([c395e27](https://github.com/kailong321200875/vue-element-plus-admin/commit/c395e27f67af9f60b151a5484ab5a3c90c4c1d1e)) +- fix Form component setProps not work bug ([48ffc52](https://github.com/kailong321200875/vue-element-plus-admin/commit/48ffc52ca8fa26d8e6a5fa4b8b3001701a1f0732)) +- fix useScrollTo not work bug ([53201ae](https://github.com/kailong321200875/vue-element-plus-admin/commit/53201ae97a425714871d99e8847a3672ba0d389f)) +- 修复 tagsView 无动画效 �[0e3eb4b](https://github.com/kailong321200875/vue-element-plus-admin/commit/0e3eb4ba8b1503e1d221dfda59a3a0001dbdcb56)) + +### Performance Improvements + +- update useForm hook ([8a958cd](https://github.com/kailong321200875/vue-element-plus-admin/commit/8a958cd71d9afbd32b243aac0814bfa3281477cd)) + +### Code Refactoring + +- 💡 综合实例查看详情重构 ([9c26edd](https://github.com/kailong321200875/vue-element-plus-admin/commit/9c26edd5d599b5fb5a832fb547e3d95b6bfa9a98)) + +### Build System + +- Add conventional-changelog-cli plugin ([384485f](https://github.com/kailong321200875/vue-element-plus-admin/commit/384485f6994c6ac33abee506508ab9d35fe658a9)) +- Add conventional-github-releaser plugin ([3cd5c71](https://github.com/kailong321200875/vue-element-plus-admin/commit/3cd5c71899dde3ac3910aef0180d8b39fad51f1b)) +- Add standard-version plugin ([110ce25](https://github.com/kailong321200875/vue-element-plus-admin/commit/110ce257841648e29b247a0338624a188694b6e9)) +- Add vite-plugin-html plugin ([d5b6e2a](https://github.com/kailong321200875/vue-element-plus-admin/commit/d5b6e2a7770eb59aa32839f69da5be37397e3538)) +- delete useless plugin ([c756761](https://github.com/kailong321200875/vue-element-plus-admin/commit/c756761dfc3200156acb228474d3539197ef413b)) +- **pinia:** Add pinia ([2040500](https://github.com/kailong321200875/vue-element-plus-admin/commit/2040500af14d277a79f01eba5eca2a440203cecf)) +- **types:** Add vue-types ([2c41826](https://github.com/kailong321200875/vue-element-plus-admin/commit/2c41826c572268b74a663a6966c548628ac7e280)) +- **unplugin-auto-import:** Delete unplugin-auto-import ([2040500](https://github.com/kailong321200875/vue-element-plus-admin/commit/2040500af14d277a79f01eba5eca2a440203cecf)) +- **unplugin-vue-components:** Delete unplugin-vue-components ([2040500](https://github.com/kailong321200875/vue-element-plus-admin/commit/2040500af14d277a79f01eba5eca2a440203cecf)) +- update plugin ([8d08bc6](https://github.com/kailong321200875/vue-element-plus-admin/commit/8d08bc6fc92258674abdd12834eaa4530ec276dd)) +- update plugins ([3c58042](https://github.com/kailong321200875/vue-element-plus-admin/commit/3c580420a20121845f02c0dd3caca5a74f06a89d)) +- **vite-plugin-style-import:** Add vite-plugin-style-import ([2040500](https://github.com/kailong321200875/vue-element-plus-admin/commit/2040500af14d277a79f01eba5eca2a440203cecf)) +- **vite-plugin-vue-setup-extend:** Delete vite-plugin-vue-setup-extend ([2040500](https://github.com/kailong321200875/vue-element-plus-admin/commit/2040500af14d277a79f01eba5eca2a440203cecf)) +- 修改 vite 配置 ([9991fb4](https://github.com/kailong321200875/vue-element-plus-admin/commit/9991fb4e5c46b9e4016beaade7232e28dc272797)) +- 设置多语 �([45e879e](https://github.com/kailong321200875/vue-element-plus-admin/commit/45e879edeef677b6aa1d2cfe4dd8dc5b76c83c59)) +- 配置代码格式 �[ffdb556](https://github.com/kailong321200875/vue-element-plus-admin/commit/ffdb556a096db247306eae8eecc1b85718314cdd)) +- 集成基础配置 ([ced99de](https://github.com/kailong321200875/vue-element-plus-admin/commit/ced99de9b113a01d9d0b190f6d2c6adc983a3102)) +- 集成基础配置 ([5dbcf23](https://github.com/kailong321200875/vue-element-plus-admin/commit/5dbcf2397ccdec80c695c113f49e8aa9bb6d012c)) + +### Styling + +- 💄 优化 layout 样式 ([37ec378](https://github.com/kailong321200875/vue-element-plus-admin/commit/37ec378f0b2bf83d73ddf0e472aada6aab248f09)) +- 💄 微调样式 ([612b486](https://github.com/kailong321200875/vue-element-plus-admin/commit/612b48673c3389779ccfdd161e2ca80b21d265b2)) +- Add elNamespace ([d847ccb](https://github.com/kailong321200875/vue-element-plus-admin/commit/d847ccb098edc72fe55c1f8459bf149453a3b73d)) +- **appStore:** code style ([641ed68](https://github.com/kailong321200875/vue-element-plus-admin/commit/641ed684fefeb52e2f91e8baab7b610fc74c8d88)) +- **Breadcrumb:** fix Breadcrumb style bug ([8755c86](https://github.com/kailong321200875/vue-element-plus-admin/commit/8755c862b837d90a25b27c01fabe64abf81fc4a2)) +- **breadcrumb:** update disabled text color ([1522e92](https://github.com/kailong321200875/vue-element-plus-admin/commit/1522e925bae37cb9df4de2252d81f717788f4537)) +- change function to arrow function ([4612e55](https://github.com/kailong321200875/vue-element-plus-admin/commit/4612e5544bcd626d686972e5cb874d0aa4af08b3)) +- delete console.log ([49a6bfe](https://github.com/kailong321200875/vue-element-plus-admin/commit/49a6bfe9d81a40e2f5f15b68d7289e1787e89b54)) +- **Icon:** delete default color ([95a2bd8](https://github.com/kailong321200875/vue-element-plus-admin/commit/95a2bd884dd9846a56cda7c4c3ee4a41ce631b7c)) +- level demo style beautification ([dbf3b0f](https://github.com/kailong321200875/vue-element-plus-admin/commit/dbf3b0f5a333ccef524bbac825035b0c6dc78ec9)) +- lint code style ([b292419](https://github.com/kailong321200875/vue-element-plus-admin/commit/b2924190b8996e8208f951e3fadbcb09baddb8df)) +- **Login:** update login styles ([eb68f1d](https://github.com/kailong321200875/vue-element-plus-admin/commit/eb68f1d919e13c07b7d200e9aec53804b2a6dc7b)) +- modify menu z-index attribute ([0d7a778](https://github.com/kailong321200875/vue-element-plus-admin/commit/0d7a7781ce0b5e39f01355d3acdb3f364cabf76d)) +- **TagView:** Vertical center tag ([41281c4](https://github.com/kailong321200875/vue-element-plus-admin/commit/41281c4d541a2744e5df5dff2764cc85465b6a4c)) + +### Types + +- add ImportMetaEnv ([38e0257](https://github.com/kailong321200875/vue-element-plus-admin/commit/38e0257487e4138a74ad1bb4ee4ba004abcfaa12)) +- Adding BfFrom Component types ([8e036f5](https://github.com/kailong321200875/vue-element-plus-admin/commit/8e036f54b56ce8521eb8ec4b7ca21aa9c24f43f2)) +- **BfForm:** Adding BfForm types ([bc9195b](https://github.com/kailong321200875/vue-element-plus-admin/commit/bc9195b21eeb79629a82a04d90e2ac5aa6592928)) +- **BfForm:** Adding BfForm types ([184b468](https://github.com/kailong321200875/vue-element-plus-admin/commit/184b468cd41dcd1cdae11477b9ee2d6e17de1481)) +- **BfForm:** Adding BfForm types ([58cb24d](https://github.com/kailong321200875/vue-element-plus-admin/commit/58cb24d9f8a50be80b5ea793387d582a77a59137)) +- delete useless types ([3fc79c0](https://github.com/kailong321200875/vue-element-plus-admin/commit/3fc79c0ae7acd0929f47e33f96c8d45a90d8f762)) +- **VForm:** Adding VForm types ([7528fe6](https://github.com/kailong321200875/vue-element-plus-admin/commit/7528fe6da60368213d28d9f1b6310d02d3d53282)) + +### Docs + +- ✏️ 修改 readme ([8edb2a3](https://github.com/kailong321200875/vue-element-plus-admin/commit/8edb2a3493dca975036859b5d2c52afaa91f5dbb)) +- ✏️ 更新 readme ([62eeb55](https://github.com/kailong321200875/vue-element-plus-admin/commit/62eeb55330dd4af2a46801c7a19f38a3ef312bbf)) +- Add README.md ([21dcf88](https://github.com/kailong321200875/vue-element-plus-admin/commit/21dcf88ba31957bbdb50c6207d010650daab70fc)) +- Error modifying readme name ([25d5c84](https://github.com/kailong321200875/vue-element-plus-admin/commit/25d5c84e92e68aa72362a14f55aacd946fa5b1b2)) +- update description ([be6ff98](https://github.com/kailong321200875/vue-element-plus-admin/commit/be6ff9899b25cc00519210950d27ee56ac5112e6)) +- update description ([c15aa87](https://github.com/kailong321200875/vue-element-plus-admin/commit/c15aa8755c9c937512c7380a6d03c4d877ef4d87)) +- update README ([27979dc](https://github.com/kailong321200875/vue-element-plus-admin/commit/27979dc6def7d9d8cea62a08d49a6c828be2258b)) +- update README.md ([53201ae](https://github.com/kailong321200875/vue-element-plus-admin/commit/53201ae97a425714871d99e8847a3672ba0d389f)) +- update README.md ([c11823a](https://github.com/kailong321200875/vue-element-plus-admin/commit/c11823abd8a033e14b4c20d17ac941195d39bcfe)) +- 修改 README ([b79a567](https://github.com/kailong321200875/vue-element-plus-admin/commit/b79a56753df55976e749c4494266df052d315416)) diff --git a/promo-ui2/Dockerfile.dev b/promo-ui2/Dockerfile.dev new file mode 100644 index 0000000..106259c --- /dev/null +++ b/promo-ui2/Dockerfile.dev @@ -0,0 +1,13 @@ +FROM node:18.0.0 + +WORKDIR /app + +RUN npm install -g pnpm@8.1.0 + +COPY package.json . + +RUN pnpm install + +COPY . . + +CMD [ "pnpm", "run", "dev" ] \ No newline at end of file diff --git a/promo-ui2/LICENSE b/promo-ui2/LICENSE new file mode 100644 index 0000000..9861118 --- /dev/null +++ b/promo-ui2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present Archer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/promo-ui2/README.md b/promo-ui2/README.md new file mode 100644 index 0000000..ab94ab3 --- /dev/null +++ b/promo-ui2/README.md @@ -0,0 +1,148 @@ +


+ +[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE) [![repo-size](https://img.shields.io/github/repo-size/kailong321200875/vue-element-plus-admin.svg)](repo-size) [![last-commit](https://img.shields.io/github/last-commit/kailong321200875/vue-element-plus-admin.svg)](last-commit) [![stars](https://img.shields.io/github/stars/kailong321200875/vue-element-plus-admin.svg)](stars) [![forks](https://img.shields.io/github/forks/kailong321200875/vue-element-plus-admin.svg)](forks) [![release](https://img.shields.io/github/release/kailong321200875/vue-element-plus-admin.svg)](release) [![watchers](https://img.shields.io/github/watchers/kailong321200875/vue-element-plus-admin.svg)](watchers) + +

vue-element-plus-admin

+
+ +**English** | [中文](./README.zh-CN.md) + +## Introduction + +vue-element-plus-admin is a free and open source middle and background template based on `element-plus`. Developed using the latest mainstream technologies such as `vue3`, `vite` and `typescript`, the out of the box middle and background front-end solution can be used as the starting template of the project and learning reference. And always pay attention to the latest technological trends and update them as soon as possible. + +vue-element-plus-admin is positioned as a background integration scheme, which is not suitable for secondary development as a basic template. Because it integrates many functions that you may not use, it will cause a lot of code redundancy. If your project doesn't pay attention to this problem, you can also directly carry out secondary development based on it. + +If you need a basic template, please switch to the `mini` branch. `mini` simply integrates some common layout functions such as layout and dynamic menu, which is more suitable for developers to carry out secondary development. + +## Feature + +- **State of The Art Development**:Use front-end front-end technology development such as Vue3/vite4 +- **TypeScript**: Application-level JavaScript language +- **Theming**: Configurable themes +- **International**:Built-in complete internationalization program +- **Mock Server** Built-in mock data scheme +- **Authority** Built-in complete dynamic routing permission generation scheme. +- **Component** Multiple commonly used components are encapsulated twice +- **Examples** Built-in rich examples + +## Preview + +- [vue-element-plus-admin](https://element-plus-admin.cn/) - Full version of the github site +- [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - Full version of the gitee site + +account: **admin/admin** + +Online examples do not apply to menu filtering by default, but directly use Static routing + +## Documentation + +[Document Github](https://element-plus-admin-doc.cn/) + +[Document Gitee](https://kailong110120130.gitee.io/vue-element-plus-admin-doc) + +## Preparation + +- [node](http://nodejs.org/) and [git](https://git-scm.com/) - Project development environment +- [Vite](https://vitejs.dev/) - Familiar with vite features +- [Vue3](https://v3.vuejs.org/) - Familiar with Vue basic syntax +- [TypeScript](https://www.typescriptlang.org/) - Familiar with the basic syntax of `TypeScript` +- [Es6+](http://es6.ruanyifeng.com/) - Familiar with es6 basic syntax +- [Vue-Router-Next](https://next.router.vuejs.org/) - Familiar with the basic use of vue-router +- [Element-Plus](https://element-plus.org/) - Familiar with the basic use of element-plus +- [Mock.js](https://github.com/nuysoft/Mock) - mockjs basic syntax + +## Install and use + +- Get the project code + +```bash +git clone https://github.com/kailong321200875/vue-element-plus-admin.git +``` + +- Installation dependencies + +```bash +cd vue-element-plus-admin + +pnpm install + +``` + +- run + +```bash +pnpm run dev +``` + +- build + +```bash +pnpm run build:pro +``` + +## Change Log + +[CHANGELOG](./CHANGELOG.md) + +## How to contribute + + + + + +You can [Raise an issue](https://github.com/kailong321200875/vue-element-plus-admin/issues/new) Or submit a Pull Request. + +**Pull Request:** + +1. Fork code +2. Create your own branch: `git checkout -b feat/xxxx` +3. Submit your changes: `git commit -am 'feat(function): add xxxxx'` +4. Push your branch: `git push origin feat/xxxx` +5. submit `pull request` + +## Git Contribution submission specification + +- `feat` New features +- `fix` Fix bugs +- `docs` document +- `style` Format and style (changes that do not affect code operation) +- `refactor` Refactor +- `perf` Optimize related, such as improving performance and experience +- `test` Add test +- `build` Compilation related modifications, changes to project construction or dependencies +- `ci` Continuous integration modification +- `chore` Changes in the construction process or auxiliary tools +- `revert` Rollback to previous version +- `workflow` Workflow improvement +- `mod` Uncertain modification classification +- `wip` Under development +- `types` type + +## Browser support + +The `Chrome 80+` browser is recommended for local development + +Support modern browsers, not IE + +| [ Edge](http://godban.github.io/browsers-support-badges/)
IE | [ Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| :-: | :-: | :-: | :-: | :-: | +| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | + +## Donate + +If you find this project helpful, welcome sponsorship to show your support~ + +[Paypal Me](https://www.paypal.com/paypalme/ckl94) + + + +### My QR code + +If you have any project cooperation or outsourcing, please scan the code to add me as a friend and leave a note of your purpose. + + + +## License + +[MIT](./LICENSE) diff --git a/promo-ui2/README.zh-CN.md b/promo-ui2/README.zh-CN.md new file mode 100644 index 0000000..2b153a6 --- /dev/null +++ b/promo-ui2/README.zh-CN.md @@ -0,0 +1,148 @@ +


+ +[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE) [![repo-size](https://img.shields.io/github/repo-size/kailong321200875/vue-element-plus-admin.svg)](repo-size) [![last-commit](https://img.shields.io/github/last-commit/kailong321200875/vue-element-plus-admin.svg)](last-commit) [![stars](https://img.shields.io/github/stars/kailong321200875/vue-element-plus-admin.svg)](stars) [![forks](https://img.shields.io/github/forks/kailong321200875/vue-element-plus-admin.svg)](forks) [![release](https://img.shields.io/github/release/kailong321200875/vue-element-plus-admin.svg)](release) [![watchers](https://img.shields.io/github/watchers/kailong321200875/vue-element-plus-admin.svg)](watchers) + +

vue-element-plus-admin

+
+ +[English](./README.md) | **中文** + +## 介绍 + +vue-element-plus-admin 是一个基于 `element-plus` 免费开源的中后台模版。使用了最新的`vue3`,`vite`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,可以用来作为项目的启动模版,也可用于学习参考。并且时刻关注着最新技术动向,尽可能的第一时间更新。 + +vue-element-plus-admin 的定位是后台集成方案,不太适合当基础模板来进行二次开发。因为集成了很多你可能用不到的功能,会造成不少的代码冗余。如果你的项目不关注这方面的问题,也可以直接基于它进行二次开发。 + +如需要基础模版,请切换到 `mini` 分支,`mini` 只简单集成了一些如:布局、动态菜单等常用布局功能,更适合开发者进行二次开发。 + +## 特性 + +- **最新技术栈**:使用 Vue3/vite4 等前端前沿技术开发 +- **TypeScript**: 应用程序级 JavaScript 的语言 +- **主题**: 可配置的主题 +- **国际化**:内置完善的国际化方案 +- **自定义数据** 内置 Mock 数据方案 +- **权限** 内置完善的动态路由权限生成方案 +- **组件** 二次封装了多个常用的组件 +- **示例** 内置丰富的示例 + +## 预览 + +- [vue-element-plus-admin](https://element-plus-admin.cn/) - 完整版 github 站点 +- [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - 完整版 gitee 站点 + +帐号:**admin/admin** + +在线例子默认不适用菜单过滤,而是直接使用静态路由表 + +## 文档 + +[文档地址 Github](https://element-plus-admin-doc.cn/) + +[文档地址 Gitee](https://kailong110120130.gitee.io/vue-element-plus-admin-doc) + +## 前序准备 + +- [node](http://nodejs.org/) 和 [git](https://git-scm.com/) - 项目开发环境 +- [Vite](https://vitejs.dev/) - 熟悉 vite 特性 +- [Vue3](https://v3.vuejs.org/) - 熟悉 Vue 基础语法 +- [TypeScript](https://www.typescriptlang.org/) - 熟悉 `TypeScript` 基本语法 +- [Es6+](http://es6.ruanyifeng.com/) - 熟悉 es6 基本语法 +- [Vue-Router-Next](https://next.router.vuejs.org/) - 熟悉 vue-router 基本使用 +- [Element-Plus](https://element-plus.org/) - element-plus 基本使用 +- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法 + +## 安装和使用 + +- 获取代码 + +```bash +git clone https://github.com/kailong321200875/vue-element-plus-admin.git +``` + +- 安装依赖 + +```bash +cd vue-element-plus-admin + +pnpm install + +``` + +- 运行 + +```bash +pnpm run dev +``` + +- 打包 + +```bash +pnpm run build:pro +``` + +## 更新日志 + +[更新日志](./CHANGELOG.md) + +## 如何贡献 + + + + + +你可以[提一个 issue](https://github.com/kailong321200875/vue-element-plus-admin/issues/new) 或者提交一个 Pull Request。 + +**Pull Request:** + +1. Fork 代码 +2. 创建自己的分支: `git checkout -b feat/xxxx` +3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'` +4. 推送您的分支: `git push origin feat/xxxx` +5. 提交 `pull request` + +## Git 贡献提交规范 + +- `feat` 新功能 +- `fix` 修补 bug +- `docs` 文档 +- `style` 格式、样式(不影响代码运行的变动) +- `refactor` 重构(即不是新增功能,也不是修改 BUG 的代码) +- `perf` 优化相关,比如提升性能、体验 +- `test` 添加测试 +- `build` 编译相关的修改,对项目构建或者依赖的改动 +- `ci` 持续集成修改 +- `chore` 构建过程或辅助工具的变动 +- `revert` 回滚到上一个版本 +- `workflow` 工作流改进 +- `mod` 不确定分类的修改 +- `wip` 开发中 +- `types` 类型 + +## 浏览器支持 + +本地开发推荐使用 `Chrome 80+` 浏览器 + +支持现代浏览器, 不支持 IE + +| [ Edge](http://godban.github.io/browsers-support-badges/)
IE | [ Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| :-: | :-: | :-: | :-: | :-: | +| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | + +## Donate + +如果你觉得这个项目有帮助,欢迎赞助以示支持~ + +[Paypal Me](https://www.paypal.com/paypalme/ckl94) + + + +### 我的二维码 + +如有项目合作或项目外包,扫码加我好友,请备注来意。 + + + +## 许可证 + +[MIT](./LICENSE) diff --git a/promo-ui2/apidoc/redpack.http b/promo-ui2/apidoc/redpack.http new file mode 100644 index 0000000..c0e5d64 --- /dev/null +++ b/promo-ui2/apidoc/redpack.http @@ -0,0 +1,17 @@ +POST http://localhost:8081/promo/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + +{"username":"15375298377","password":"123456"} + + +### 获取卡密列表 +GET http://localhost:8081/promo/card/list +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImQ4MWI5OTM5LTUwZWItNGFiNC05NGIxLTEwOWFiYzY5NzI5MCJ9.RHXzv6Ao6hUl0fwY-Zhszz1-Jda6ceZybjmFddKFLZQbjufYkZd167ZCmlU9SbcrZ3Ec8TIt4XqSS7zb7fVHtQ + + +### 生成卡密 + +POST http://localhost:8081/promo/generateCard/6/1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImQ4MWI5OTM5LTUwZWItNGFiNC05NGIxLTEwOWFiYzY5NzI5MCJ9.RHXzv6Ao6hUl0fwY-Zhszz1-Jda6ceZybjmFddKFLZQbjufYkZd167ZCmlU9SbcrZ3Ec8TIt4XqSS7zb7fVHtQ +Content-Type: application/json; charset=UTF-8 + diff --git a/promo-ui2/apidoc/user.http b/promo-ui2/apidoc/user.http new file mode 100644 index 0000000..f6a15b2 --- /dev/null +++ b/promo-ui2/apidoc/user.http @@ -0,0 +1,4 @@ +POST http://localhost:8081/promo/register HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + +{"username":"17688554522","password":"123456", "contactInformation": "hello"} \ No newline at end of file diff --git a/promo-ui2/apidoc/分享.http b/promo-ui2/apidoc/分享.http new file mode 100644 index 0000000..7ddada7 --- /dev/null +++ b/promo-ui2/apidoc/分享.http @@ -0,0 +1,11 @@ +POST http://localhost:8081/promo/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + +{"username":"15375298377","password":"123456"} + + +### 分享域名 + +GET http://localhost:8081/promo/getsharedomain HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImEwMmU0Y2MyLWM5MDctNGU4OS1iNTkxLTk1Mjk1MGI2NjRmNCJ9._imhNdbqREBrYv-Kx73VN0S5U7Pr1zMvvMLEMXHf77uKOLO1Y0NnWRuTvSQlYp1T1KMVcNxEChSLj7I-Xex2wA + diff --git a/promo-ui2/apidoc/普通用户.http b/promo-ui2/apidoc/普通用户.http new file mode 100644 index 0000000..0f12f2f --- /dev/null +++ b/promo-ui2/apidoc/普通用户.http @@ -0,0 +1,13 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"13545419205","password":"123456"} + + +### 兑换红包卡密 + +GET http://localhost:8081/api/bonus/receiveRedPacket/A628920554234AFDB539398823835378 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjY1MjA4OTYwLTA3NDQtNDQxZS05YzA1LWJjNTc2ZTQyN2RiMyJ9.Ogqmm8iTgiPhc8b7iDxz_a2atBtAgbmwqzi1M5ogLEpAVFAv91N0Ygo-1_yUVioPVn0WTWVJvJnIW42-4CgIJg + + diff --git a/promo-ui2/commitlint.config.cjs b/promo-ui2/commitlint.config.cjs new file mode 100644 index 0000000..78d145d --- /dev/null +++ b/promo-ui2/commitlint.config.cjs @@ -0,0 +1,28 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + [ + 'feat', // 新功能(feature) + 'fix', // 修补bug + 'docs', // 文档(documentation) + 'style', // 格式、样式(不影响代码运行的变动) + 'refactor', // 重构(即不是新增功能,也不是修改BUG的代码) + 'perf', // 优化相关,比如提升性能、体验 + 'test', // 添加测试 + 'ci', // 持续集成修改 + 'chore', // 构建过程或辅助工具的变动 + 'revert', // 回滚到上一个版本 + 'workflow', // 工作流改进 + 'mod', // 不确定分类的修改 + 'wip', // 开发中 + 'types', // 类型修改 + 'release' // 版本发布 + ] + ], + 'subject-full-stop': [0, 'never'], + 'subject-case': [0, 'never'] + } +} diff --git a/promo-ui2/docker-compose.dev.yaml b/promo-ui2/docker-compose.dev.yaml new file mode 100644 index 0000000..b6fd3e1 --- /dev/null +++ b/promo-ui2/docker-compose.dev.yaml @@ -0,0 +1,10 @@ +services: + vue-element-plus-admin: + build: + context: . + dockerfile: Dockerfile.dev + ports: + - "4000:4000" + volumes: + - /app/node_modules + - .:/app \ No newline at end of file diff --git a/promo-ui2/eslint.config.mjs b/promo-ui2/eslint.config.mjs new file mode 100644 index 0000000..67bc096 --- /dev/null +++ b/promo-ui2/eslint.config.mjs @@ -0,0 +1,82 @@ +// 引入vue模版的eslint +import pluginVue from 'eslint-plugin-vue' +import eslint from '@eslint/js' +// ts-eslint解析器,使 eslint 可以解析 ts 语法 +import tseslint from 'typescript-eslint' +// vue文件解析器 +import vueParser from 'vue-eslint-parser' +import prettier from 'eslint-plugin-prettier' + +export default tseslint.config({ + // ignores: ['node_modules', 'prettier.config.cjs', 'dist*'], + files: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'], + // tseslint.config添加了extends扁平函数,直接用。否则是eslint9.0版本是没有extends的 + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...pluginVue.configs['flat/essential'] + ], + plugins: { + prettier + }, + languageOptions: { + parser: vueParser, // 使用vue解析器,这个可以识别vue文件 + parserOptions: { + parser: tseslint.parser, // 在vue文件上使用ts解析器 + sourceType: 'module', + ecmaVersion: 2020, + ecmaFeatures: { + jsx: true + } + } + }, + rules: { + 'prettier/prettier': 'error', + 'no-useless-escape': 0, + 'no-undef': 0, + '@typescript-eslint/no-unused-expressions': 0, + '@typescript-eslint/no-unsafe-function-type': 0, + 'vue/no-setup-props-destructure': 0, + 'vue/script-setup-uses-vars': 1, + 'vue/no-reserved-component-names': 0, + '@typescript-eslint/ban-ts-ignore': 0, + '@typescript-eslint/explicit-function-return-type': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/no-empty-function': 0, + 'vue/custom-event-name-casing': 0, + 'no-use-before-define': 0, + '@typescript-eslint/no-use-before-define': 0, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/ban-types': 0, + '@typescript-eslint/no-non-null-assertion': 0, + '@typescript-eslint/explicit-module-boundary-types': 0, + '@typescript-eslint/no-unused-vars': 0, + 'no-unused-vars': 0, + 'space-before-function-paren': 0, + 'vue/attributes-order': 0, + 'vue/one-component-per-file': 0, + 'vue/html-closing-bracket-newline': 0, + 'vue/max-attributes-per-line': 0, + 'vue/multiline-html-element-content-newline': 0, + 'vue/singleline-html-element-content-newline': 0, + 'vue/attribute-hyphenation': 0, + 'vue/require-default-prop': 0, + 'vue/require-explicit-emits': 0, + 'vue/html-self-closing': [ + 1, + { + html: { + void: 'always', + normal: 'never', + component: 'always' + }, + svg: 'always', + math: 'always' + } + ], + 'vue/multi-word-component-names': 0, + 'vue/no-v-html': 0, + 'vue/require-toggle-inside-transition': 0 + } +}) diff --git a/promo-ui2/index.html b/promo-ui2/index.html new file mode 100644 index 0000000..aa76548 --- /dev/null +++ b/promo-ui2/index.html @@ -0,0 +1,23 @@ + + + + + + + <%= title %> + + + + +
+ +
+ + + diff --git a/promo-ui2/mock/_createProductionServer.ts b/promo-ui2/mock/_createProductionServer.ts new file mode 100644 index 0000000..598ac64 --- /dev/null +++ b/promo-ui2/mock/_createProductionServer.ts @@ -0,0 +1,18 @@ +import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' + +const modules = import.meta.glob('./**/*.mock.ts', { + import: 'default', + eager: true +}) + +const mockModules: any[] = [] +Object.keys(modules).forEach(async (key) => { + if (key.includes('_')) { + return + } + mockModules.push(...(modules[key] as any)) +}) + +export function setupProdMockServer() { + createProdMockServer(mockModules) +} diff --git a/promo-ui2/mock/analysis/index.mock.ts b/promo-ui2/mock/analysis/index.mock.ts new file mode 100644 index 0000000..195f367 --- /dev/null +++ b/promo-ui2/mock/analysis/index.mock.ts @@ -0,0 +1,87 @@ +import { SUCCESS_CODE } from '@/constants' +import { MockMethod } from 'vite-plugin-mock' + +const timeout = 1000 + +export default [ + // 分析页统计接口 + { + url: '/mock/analysis/total', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: { + users: 102400, + messages: 81212, + moneys: 9280, + shoppings: 13600 + } + } + } + }, + // 用户来源 + { + url: '/mock/analysis/userAccessSource', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: [ + { value: 1000, name: 'analysis.directAccess' }, + { value: 310, name: 'analysis.mailMarketing' }, + { value: 234, name: 'analysis.allianceAdvertising' }, + { value: 135, name: 'analysis.videoAdvertising' }, + { value: 1548, name: 'analysis.searchEngines' } + ] + } + } + }, + // 每周用户活跃量 + { + url: '/mock/analysis/weeklyUserActivity', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: [ + { value: 13253, name: 'analysis.monday' }, + { value: 34235, name: 'analysis.tuesday' }, + { value: 26321, name: 'analysis.wednesday' }, + { value: 12340, name: 'analysis.thursday' }, + { value: 24643, name: 'analysis.friday' }, + { value: 1322, name: 'analysis.saturday' }, + { value: 1324, name: 'analysis.sunday' } + ] + } + } + }, + // 每月销售额 + { + url: '/mock/analysis/monthlySales', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: [ + { estimate: 100, actual: 120, name: 'analysis.january' }, + { estimate: 120, actual: 82, name: 'analysis.february' }, + { estimate: 161, actual: 91, name: 'analysis.march' }, + { estimate: 134, actual: 154, name: 'analysis.april' }, + { estimate: 105, actual: 162, name: 'analysis.may' }, + { estimate: 160, actual: 140, name: 'analysis.june' }, + { estimate: 165, actual: 145, name: 'analysis.july' }, + { estimate: 114, actual: 250, name: 'analysis.august' }, + { estimate: 163, actual: 134, name: 'analysis.september' }, + { estimate: 185, actual: 56, name: 'analysis.october' }, + { estimate: 118, actual: 99, name: 'analysis.november' }, + { estimate: 123, actual: 123, name: 'analysis.december' } + ] + } + } + } +] as MockMethod[] diff --git a/promo-ui2/mock/department/index.mock.ts b/promo-ui2/mock/department/index.mock.ts new file mode 100644 index 0000000..443907e --- /dev/null +++ b/promo-ui2/mock/department/index.mock.ts @@ -0,0 +1,206 @@ +import { toAnyString } from '@/utils' +import Mock from 'mockjs' +import { SUCCESS_CODE } from '@/constants' + +const departmentList: any = [] + +const citys = ['厦门总公司', '北京分公司', '上海分公司', '福州分公司', '深圳分公司', '杭州分公司'] + +for (let i = 0; i < 5; i++) { + departmentList.push({ + // 部门名称 + departmentName: citys[i], + id: toAnyString(), + createTime: '@datetime', + // 状态 + status: Mock.Random.integer(0, 1), + // 备注 + remark: '@cword(10, 15)', + children: [ + { + // 部门名称 + departmentName: '研发部', + id: toAnyString(), + createTime: '@datetime', + // 状态 + status: Mock.Random.integer(0, 1), + // 备注 + remark: '@cword(10, 15)' + }, + { + // 部门名称 + departmentName: '产品部', + id: toAnyString(), + createTime: '@datetime', + // 状态 + status: Mock.Random.integer(0, 1), + // 备注 + remark: '@cword(10, 15)' + }, + { + // 部门名称 + departmentName: '运营部', + id: toAnyString(), + createTime: '@datetime', + // 状态 + status: Mock.Random.integer(0, 1), + // 备注 + remark: '@cword(10, 15)' + }, + { + // 部门名称 + departmentName: '市场部', + id: toAnyString(), + createTime: '@datetime', + // 状态 + status: Mock.Random.integer(0, 1), + // 备注 + remark: '@cword(10, 15)' + }, + { + // 部门名称 + departmentName: '销售部', + id: toAnyString(), + createTime: '@datetime', + // 状态 + status: Mock.Random.integer(0, 1), + // 备注 + remark: '@cword(10, 15)' + }, + { + // 部门名称 + departmentName: '客服部', + id: toAnyString(), + createTime: '@datetime', + // 状态 + status: Mock.Random.integer(0, 1), + // 备注 + remark: '@cword(10, 15)' + } + ] + }) +} + +export default [ + // 列表接口 + { + url: '/mock/department/list', + method: 'get', + response: () => { + return { + code: SUCCESS_CODE, + data: { + list: departmentList + } + } + } + }, + { + url: '/mock/department/table/list', + method: 'get', + response: () => { + return { + code: SUCCESS_CODE, + data: { + list: departmentList, + total: 5 + } + } + } + }, + { + url: '/mock/department/users', + method: 'get', + timeout: 1000, + response: ({ query }) => { + const { pageSize } = query + // 根据pageSize来创建数据 + const mockList: any = [] + for (let i = 0; i < pageSize; i++) { + mockList.push( + Mock.mock({ + // 用户名 + username: '@cname', + // 账号 + account: '@first', + // 邮箱 + email: '@EMAIL', + // 创建时间 + createTime: '@datetime', + // 用户id + id: toAnyString() + }) + ) + } + return { + code: SUCCESS_CODE, + data: { + total: 100, + list: mockList + } + } + } + }, + // 保存接口 + { + url: '/mock/department/user/save', + method: 'post', + timeout: 1000, + response: () => { + return { + code: SUCCESS_CODE, + data: 'success' + } + } + }, + // 删除接口 + { + url: '/mock/department/user/delete', + method: 'post', + response: ({ body }) => { + const ids = body.ids + if (!ids) { + return { + code: 500, + message: '请选择需要删除的数据' + } + } else { + return { + code: SUCCESS_CODE, + data: 'success' + } + } + } + }, + // 保存接口 + { + url: '/mock/department/save', + method: 'post', + timeout: 1000, + response: () => { + return { + code: SUCCESS_CODE, + data: 'success' + } + } + }, + // 删除接口 + { + url: '/mock/department/delete', + method: 'post', + response: ({ body }) => { + const ids = body.ids + if (!ids) { + return { + code: 500, + message: '请选择需要删除的数据' + } + } else { + return { + code: SUCCESS_CODE, + data: 'success' + } + } + } + } +] diff --git a/promo-ui2/mock/dict/index.mock.ts b/promo-ui2/mock/dict/index.mock.ts new file mode 100644 index 0000000..29042c6 --- /dev/null +++ b/promo-ui2/mock/dict/index.mock.ts @@ -0,0 +1,60 @@ +import { SUCCESS_CODE } from '@/constants' + +const timeout = 1000 + +const dictObj: Recordable = { + importance: [ + { + value: 0, + label: 'tableDemo.commonly' + }, + { + value: 1, + label: 'tableDemo.good' + }, + { + value: 2, + label: 'tableDemo.important' + } + ] +} + +export default [ + // 字典接口 + { + url: '/mock/dict/list', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: dictObj + } + } + }, + // 获取某个字典 + { + url: '/mock/dict/one', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: [ + { + label: 'test1', + value: 0 + }, + { + label: 'test2', + value: 1 + }, + { + label: 'test3', + value: 2 + } + ] + } + } + } +] diff --git a/promo-ui2/mock/menu/index.mock.ts b/promo-ui2/mock/menu/index.mock.ts new file mode 100644 index 0000000..2823692 --- /dev/null +++ b/promo-ui2/mock/menu/index.mock.ts @@ -0,0 +1,357 @@ +import Mock from 'mockjs' +import { SUCCESS_CODE } from '@/constants' + +const timeout = 1000 + +export default [ + // 列表接口 + { + url: '/mock/menu/list', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: { + list: [ + { + path: '/dashboard', + component: '#', + redirect: '/dashboard/analysis', + name: 'Dashboard', + status: Mock.Random.integer(0, 1), + id: 1, + type: 0, + parentId: undefined, + title: '首页', + meta: { + title: '首页', + icon: 'vi-ant-design:dashboard-filled', + alwaysShow: true + }, + children: [ + { + path: 'analysis', + component: 'views/Dashboard/Analysis', + name: 'Analysis', + status: Mock.Random.integer(0, 1), + id: 2, + type: 1, + parentId: 1, + title: '分析页', + permissionList: [ + { + id: 1, + label: '新增', + value: 'add' + }, + { + id: 2, + label: '编辑', + value: 'edit' + } + ], + meta: { + title: '分析页', + noCache: true, + permission: ['add', 'edit'] + } + }, + { + path: 'workplace', + component: 'views/Dashboard/Workplace', + name: 'Workplace', + status: Mock.Random.integer(0, 1), + id: 3, + type: 1, + parentId: 1, + title: '工作台', + permissionList: [ + { + id: 1, + label: '新增', + value: 'add' + }, + { + id: 2, + label: '编辑', + value: 'edit' + }, + { + id: 3, + label: '删除', + value: 'delete' + } + ], + meta: { + title: '工作台', + noCache: true + } + } + ] + }, + { + path: '/external-link', + component: '#', + meta: { + title: '文档', + icon: 'vi-clarity:document-solid' + }, + name: 'ExternalLink', + status: Mock.Random.integer(0, 1), + id: 4, + type: 0, + parentId: undefined, + title: '文档', + children: [ + { + path: 'https://element-plus-admin-doc.cn/', + name: 'DocumentLink', + status: Mock.Random.integer(0, 1), + id: 5, + type: 1, + parentId: 4, + title: '文档', + meta: { + title: '文档' + } + } + ] + }, + { + path: '/level', + component: '#', + redirect: '/level/menu1/menu1-1/menu1-1-1', + name: 'Level', + status: Mock.Random.integer(0, 1), + id: 6, + type: 0, + parentId: undefined, + title: '菜单', + meta: { + title: '菜单', + icon: 'vi-carbon:skill-level-advanced' + }, + children: [ + { + path: 'menu1', + name: 'Menu1', + component: '##', + status: Mock.Random.integer(0, 1), + id: 7, + type: 0, + parentId: 6, + title: '菜单1', + redirect: '/level/menu1/menu1-1/menu1-1-1', + meta: { + title: '菜单1' + }, + children: [ + { + path: 'menu1-1', + name: 'Menu11', + component: '##', + status: Mock.Random.integer(0, 1), + id: 8, + type: 0, + parentId: 7, + title: '菜单1-1', + redirect: '/level/menu1/menu1-1/menu1-1-1', + meta: { + title: '菜单1-1', + alwaysShow: true + }, + children: [ + { + path: 'menu1-1-1', + name: 'Menu111', + component: 'views/Level/Menu111', + status: Mock.Random.integer(0, 1), + id: 9, + type: 1, + parentId: 8, + title: '菜单1-1-1', + meta: { + title: '菜单1-1-1' + } + } + ] + }, + { + path: 'menu1-2', + name: 'Menu12', + component: 'views/Level/Menu12', + status: Mock.Random.integer(0, 1), + id: 10, + type: 1, + parentId: 7, + title: '菜单1-2', + meta: { + title: '菜单1-2' + } + } + ] + }, + { + path: 'menu2', + name: 'Menu2Demo', + component: 'views/Level/Menu2', + status: Mock.Random.integer(0, 1), + id: 11, + type: 1, + parentId: 6, + title: '菜单2', + meta: { + title: '菜单2' + } + } + ] + }, + { + path: '/example', + component: '#', + redirect: '/example/example-dialog', + name: 'Example', + status: Mock.Random.integer(0, 1), + id: 12, + type: 0, + parentId: undefined, + title: '综合示例', + meta: { + title: '综合示例', + icon: 'vi-ep:management', + alwaysShow: true + }, + children: [ + { + path: 'example-dialog', + component: 'views/Example/Dialog/ExampleDialog', + name: 'ExampleDialog', + status: Mock.Random.integer(0, 1), + id: 13, + type: 1, + parentId: 12, + title: '综合示例-弹窗', + permissionList: [ + { + id: 1, + label: '新增', + value: 'add' + }, + { + id: 2, + label: '编辑', + value: 'edit' + }, + { + id: 3, + label: '删除', + value: 'delete' + }, + { + id: 4, + label: '查看', + value: 'view' + } + ], + meta: { + title: '综合示例-弹窗' + } + }, + { + path: 'example-page', + component: 'views/Example/Page/ExamplePage', + name: 'ExamplePage', + status: Mock.Random.integer(0, 1), + id: 14, + type: 1, + parentId: 12, + title: '综合示例-页面', + permissionList: [ + { + id: 1, + label: '新增', + value: 'add' + }, + { + id: 2, + label: '编辑', + value: 'edit' + }, + { + id: 3, + label: '删除', + value: 'delete' + }, + { + id: 4, + label: '查看', + value: 'view' + } + ], + meta: { + title: '综合示例-页面' + } + }, + { + path: 'example-add', + component: 'views/Example/Page/ExampleAdd', + name: 'ExampleAdd', + status: Mock.Random.integer(0, 1), + id: 15, + type: 1, + parentId: 12, + title: '综合示例-新增', + meta: { + title: '综合示例-新增', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page' + } + }, + { + path: 'example-edit', + component: 'views/Example/Page/ExampleEdit', + name: 'ExampleEdit', + status: Mock.Random.integer(0, 1), + id: 16, + type: 1, + parentId: 12, + title: '综合示例-编辑', + meta: { + title: '综合示例-编辑', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page' + } + }, + { + path: 'example-detail', + component: 'views/Example/Page/ExampleDetail', + name: 'ExampleDetail', + status: Mock.Random.integer(0, 1), + id: 17, + type: 1, + parentId: 12, + title: '综合示例-详情', + meta: { + title: '综合示例-详情', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page' + } + } + ] + } + ] + } + } + } + } +] diff --git a/promo-ui2/mock/request/index.mock.ts b/promo-ui2/mock/request/index.mock.ts new file mode 100644 index 0000000..bfaa266 --- /dev/null +++ b/promo-ui2/mock/request/index.mock.ts @@ -0,0 +1,72 @@ +import { SUCCESS_CODE } from '@/constants' + +const timeout = 600000 + +export default [ + { + url: '/mock/request/1', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: 'request-1' + } + } + }, + { + url: '/mock/request/2', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: 'request-2' + } + } + }, + { + url: '/mock/request/3', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: 'request-3' + } + } + }, + { + url: '/mock/request/4', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: 'request-4' + } + } + }, + { + url: '/mock/request/5', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: 'request-5' + } + } + }, + { + url: '/mock/request/expired', + method: 'get', + timeout: 0, + response: () => { + return { + code: 401, + message: 'token expired' + } + } + } +] diff --git a/promo-ui2/mock/role/index.mock.ts b/promo-ui2/mock/role/index.mock.ts new file mode 100644 index 0000000..910b2cc --- /dev/null +++ b/promo-ui2/mock/role/index.mock.ts @@ -0,0 +1,1235 @@ +import Mock from 'mockjs' +import { SUCCESS_CODE } from '@/constants' +import { toAnyString } from '@/utils' + +const timeout = 1000 + +const adminList = [ + { + path: '/dashboard', + component: '#', + redirect: '/dashboard/analysis', + name: 'Dashboard', + meta: { + title: 'router.dashboard', + icon: 'vi-ant-design:dashboard-filled', + alwaysShow: true + }, + children: [ + { + path: 'analysis', + component: 'views/Dashboard/Analysis', + name: 'Analysis', + meta: { + title: 'router.analysis', + noCache: true, + affix: true + } + }, + { + path: 'workplace', + component: 'views/Dashboard/Workplace', + name: 'Workplace', + meta: { + title: 'router.workplace', + noCache: true, + affix: true + } + } + ] + }, + { + path: '/external-link', + component: '#', + meta: {}, + name: 'ExternalLink', + children: [ + { + path: 'https://element-plus-admin-doc.cn/', + name: 'DocumentLink', + meta: { + title: 'router.document', + icon: 'vi-clarity:document-solid' + } + } + ] + }, + { + path: '/guide', + component: '#', + name: 'Guide', + meta: {}, + children: [ + { + path: 'index', + component: 'views/Guide/Guide', + name: 'GuideDemo', + meta: { + title: 'router.guide', + icon: 'vi-cib:telegram-plane' + } + } + ] + }, + { + path: '/components', + component: '#', + redirect: '/components/form/default-form', + name: 'ComponentsDemo', + meta: { + title: 'router.component', + icon: 'vi-bx:bxs-component', + alwaysShow: true + }, + children: [ + { + path: 'form', + component: '##', + name: 'Form', + meta: { + title: 'router.form', + alwaysShow: true + }, + children: [ + { + path: 'default-form', + component: 'views/Components/Form/DefaultForm', + name: 'DefaultForm', + meta: { + title: 'router.defaultForm' + } + }, + { + path: 'use-form', + component: 'views/Components/Form/UseFormDemo', + name: 'UseForm', + meta: { + title: 'UseForm' + } + } + ] + }, + { + path: 'table', + component: '##', + redirect: '/components/table/default-table', + name: 'TableDemo', + meta: { + title: 'router.table', + alwaysShow: true + }, + children: [ + { + path: 'default-table', + component: 'views/Components/Table/DefaultTable', + name: 'DefaultTable', + meta: { + title: 'router.defaultTable' + } + }, + { + path: 'use-table', + component: 'views/Components/Table/UseTableDemo', + name: 'UseTable', + meta: { + title: 'UseTable' + } + }, + { + path: 'tree-table', + component: 'views/Components/Table/TreeTable', + name: 'TreeTable', + meta: { + title: 'TreeTable' + } + }, + { + path: 'table-image-preview', + component: 'views/Components/Table/TableImagePreview', + name: 'TableImagePreview', + meta: { + title: 'router.PicturePreview' + } + }, + { + path: 'table-video-preview', + component: 'views/Components/Table/TableVideoPreview', + name: 'TableVideoPreview', + meta: { + title: 'router.tableVideoPreview' + } + }, + { + path: 'card-table', + component: 'views/Components/Table/CardTable', + name: 'CardTable', + meta: { + title: 'router.cardTable' + } + } + // { + // path: 'ref-table', + // component: 'views/Components/Table/RefTable', + // name: 'RefTable', + // meta: { + // title: 'RefTable' + // } + // } + ] + }, + { + path: 'editor-demo', + component: '##', + redirect: '/components/editor-demo/editor', + name: 'EditorDemo', + meta: { + title: 'router.editor', + alwaysShow: true + }, + children: [ + { + path: 'editor', + component: 'views/Components/Editor/Editor', + name: 'Editor', + meta: { + title: 'router.richText' + } + }, + { + path: 'json-editor', + component: 'views/Components/Editor/JsonEditor', + name: 'JsonEditor', + meta: { + title: 'router.jsonEditor' + } + }, + { + path: 'code-editor', + component: 'views/Components/Editor/CodeEditor', + name: 'CodeEditor', + meta: { + title: 'router.codeEditor' + } + } + ] + }, + { + path: 'search', + component: 'views/Components/Search', + name: 'Search', + meta: { + title: 'router.search' + } + }, + { + path: 'descriptions', + component: 'views/Components/Descriptions', + name: 'Descriptions', + meta: { + title: 'router.descriptions' + } + }, + { + path: 'image-viewer', + component: 'views/Components/ImageViewer', + name: 'ImageViewer', + meta: { + title: 'router.imageViewer' + } + }, + { + path: 'dialog', + component: 'views/Components/Dialog', + name: 'Dialog', + meta: { + title: 'router.dialog' + } + }, + { + path: 'icon', + component: 'views/Components/Icon', + name: 'Icon', + meta: { + title: 'router.icon' + } + }, + { + path: 'icon-picker', + component: 'views/Components/IconPicker', + name: 'IconPicker', + meta: { + title: 'router.iconPicker' + } + }, + { + path: 'echart', + component: 'views/Components/Echart', + name: 'Echart', + meta: { + title: 'router.echart' + } + }, + { + path: 'count-to', + component: 'views/Components/CountTo', + name: 'CountTo', + meta: { + title: 'router.countTo' + } + }, + { + path: 'qrcode', + component: 'views/Components/Qrcode', + name: 'Qrcode', + meta: { + title: 'router.qrcode' + } + }, + { + path: 'highlight', + component: 'views/Components/Highlight', + name: 'Highlight', + meta: { + title: 'router.highlight' + } + }, + { + path: 'infotip', + component: 'views/Components/Infotip', + name: 'Infotip', + meta: { + title: 'router.infotip' + } + }, + { + path: 'input-password', + component: 'views/Components/InputPassword', + name: 'InputPassword', + meta: { + title: 'router.inputPassword' + } + }, + { + path: 'waterfall', + component: 'views/Components/Waterfall', + name: 'Waterfall', + meta: { + title: 'router.waterfall' + } + }, + { + path: 'image-cropping', + component: 'views/Components/ImageCropping', + name: 'ImageCropping', + meta: { + title: 'router.imageCropping' + } + }, + { + path: 'video-player', + component: 'views/Components/VideoPlayer', + name: 'VideoPlayer', + meta: { + title: 'router.videoPlayer' + } + }, + { + path: 'avatars', + component: 'views/Components/Avatars', + name: 'Avatars', + meta: { + title: 'router.avatars' + } + }, + { + path: 'i-agree', + component: 'views/Components/IAgree', + name: 'IAgree', + meta: { + title: 'router.iAgree' + } + }, + { + path: 'tree', + component: 'views/Components/Tree', + name: 'Tree', + meta: { + title: 'router.tree' + } + } + ] + }, + { + path: '/function', + component: '#', + redirect: '/function/multipleTabs', + name: 'Function', + meta: { + title: 'router.function', + icon: 'vi-ri:function-fill', + alwaysShow: true + }, + children: [ + { + path: 'multipleTabs', + component: 'views/Function/MultipleTabs', + name: 'MultipleTabs', + meta: { + title: 'router.multipleTabs' + } + }, + { + path: 'multiple-tabs-demo/:id', + component: 'views/Function/MultipleTabsDemo', + name: 'MultipleTabsDemo', + meta: { + hidden: true, + title: 'router.details', + canTo: true + } + }, + { + path: 'request', + component: 'views/Function/Request', + name: 'Request', + meta: { + title: 'router.request' + } + }, + { + path: 'test', + component: 'views/Function/Test', + name: 'Test', + meta: { + title: 'router.permission', + permission: ['add', 'edit', 'delete'] + } + } + ] + }, + { + path: '/hooks', + component: '#', + redirect: '/hooks/useWatermark', + name: 'Hooks', + meta: { + title: 'hooks', + icon: 'vi-ic:outline-webhook', + alwaysShow: true + }, + children: [ + { + path: 'useWatermark', + component: 'views/hooks/useWatermark', + name: 'UseWatermark', + meta: { + title: 'useWatermark' + } + }, + { + path: 'useTagsView', + component: 'views/hooks/useTagsView', + name: 'UseTagsView', + meta: { + title: 'useTagsView' + } + }, + { + path: 'useValidator', + component: 'views/hooks/useValidator', + name: 'UseValidator', + meta: { + title: 'useValidator' + } + }, + { + path: 'useCrudSchemas', + component: 'views/hooks/useCrudSchemas', + name: 'UseCrudSchemas', + meta: { + title: 'useCrudSchemas' + } + }, + { + path: 'useClipboard', + component: 'views/hooks/useClipboard', + name: 'UseClipboard', + meta: { + title: 'useClipboard' + } + }, + { + path: 'useNetwork', + component: 'views/hooks/useNetwork', + name: 'UseNetwork', + meta: { + title: 'useNetwork' + } + } + ] + }, + { + path: '/level', + component: '#', + redirect: '/level/menu1/menu1-1/menu1-1-1', + name: 'Level', + meta: { + title: 'router.level', + icon: 'vi-carbon:skill-level-advanced' + }, + children: [ + { + path: 'menu1', + name: 'Menu1', + component: '##', + redirect: '/level/menu1/menu1-1/menu1-1-1', + meta: { + title: 'router.menu1' + }, + children: [ + { + path: 'menu1-1', + name: 'Menu11', + component: '##', + redirect: '/level/menu1/menu1-1/menu1-1-1', + meta: { + title: 'router.menu11', + alwaysShow: true + }, + children: [ + { + path: 'menu1-1-1', + name: 'Menu111', + component: 'views/Level/Menu111', + meta: { + title: 'router.menu111' + } + } + ] + }, + { + path: 'menu1-2', + name: 'Menu12', + component: 'views/Level/Menu12', + meta: { + title: 'router.menu12' + } + } + ] + }, + { + path: 'menu2', + name: 'Menu2Demo', + component: 'views/Level/Menu2', + meta: { + title: 'router.menu2' + } + } + ] + }, + { + path: '/example', + component: '#', + redirect: '/example/example-dialog', + name: 'Example', + meta: { + title: 'router.example', + icon: 'vi-ep:management', + alwaysShow: true + }, + children: [ + { + path: 'example-dialog', + component: 'views/Example/Dialog/ExampleDialog', + name: 'ExampleDialog', + meta: { + title: 'router.exampleDialog' + } + }, + { + path: 'example-page', + component: 'views/Example/Page/ExamplePage', + name: 'ExamplePage', + meta: { + title: 'router.examplePage' + } + }, + { + path: 'example-add', + component: 'views/Example/Page/ExampleAdd', + name: 'ExampleAdd', + meta: { + title: 'router.exampleAdd', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page' + } + }, + { + path: 'example-edit', + component: 'views/Example/Page/ExampleEdit', + name: 'ExampleEdit', + meta: { + title: 'router.exampleEdit', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page' + } + }, + { + path: 'example-detail', + component: 'views/Example/Page/ExampleDetail', + name: 'ExampleDetail', + meta: { + title: 'router.exampleDetail', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page' + } + } + ] + }, + { + path: '/error', + component: '#', + redirect: '/error/404', + name: 'Error', + meta: { + title: 'router.errorPage', + icon: 'vi-ci:error', + alwaysShow: true + }, + children: [ + { + path: '404-demo', + component: 'views/Error/404', + name: '404Demo', + meta: { + title: '404' + } + }, + { + path: '403-demo', + component: 'views/Error/403', + name: '403Demo', + meta: { + title: '403' + } + }, + { + path: '500-demo', + component: 'views/Error/500', + name: '500Demo', + meta: { + title: '500' + } + } + ] + }, + { + path: '/authorization', + component: '#', + redirect: '/authorization/user', + name: 'Authorization', + meta: { + title: 'router.authorization', + icon: 'vi-eos-icons:role-binding', + alwaysShow: true + }, + children: [ + { + path: 'department', + component: 'views/Authorization/Department/Department', + name: 'Department', + meta: { + title: 'router.department' + } + }, + { + path: 'user', + component: 'views/Authorization/User/User', + name: 'User', + meta: { + title: 'router.user' + } + }, + { + path: 'menu', + component: 'views/Authorization/Menu/Menu', + name: 'Menu', + meta: { + title: 'router.menuManagement' + } + }, + { + path: 'role', + component: 'views/Authorization/Role/Role', + name: 'Role', + meta: { + title: 'router.role' + } + } + ] + } +] + +const testList: string[] = [ + '/dashboard', + '/dashboard/analysis', + '/dashboard/workplace', + 'external-link', + 'https://element-plus-admin-doc.cn/', + '/guide', + '/guide/index', + '/components', + '/components/form', + '/components/form/default-form', + '/components/form/use-form', + '/components/form/ref-form', + '/components/table', + '/components/table/default-table', + '/components/table/use-table', + '/components/table/tree-table', + '/components/table/table-image-preview', + '/components/table/table-video-preview', + '/components/table/ref-table', + '/components/table/card-table', + '/components/editor-demo', + '/components/editor-demo/editor', + '/components/editor-demo/json-editor', + '/components/editor-demo/code-editor', + '/components/search', + '/components/descriptions', + '/components/image-viewer', + '/components/dialog', + '/components/icon', + '/components/iconPicker', + '/components/echart', + '/components/count-to', + '/components/qrcode', + '/components/highlight', + '/components/infotip', + '/components/input-password', + '/components/waterfall', + '/components/image-cropping', + '/components/video-player', + '/components/avatars', + '/components/i-agree', + 'function', + '/function/multiple-tabs', + '/function/multiple-tabs-demo/:id', + '/function/request', + '/function/test', + '/hooks', + '/hooks/useWatermark', + '/hooks/useTagsView', + '/hooks/useValidator', + '/hooks/useCrudSchemas', + '/hooks/useClipboard', + '/hooks/useNetwork', + '/level', + '/level/menu1', + '/level/menu1/menu1-1', + '/level/menu1/menu1-1/menu1-1-1', + '/level/menu1/menu1-2', + '/level/menu2', + '/example', + '/example/example-dialog', + '/example/example-page', + '/example/example-add', + '/example/example-edit', + '/example/example-detail', + '/authorization', + '/authorization/department', + '/authorization/user', + '/authorization/role', + '/authorization/menu', + '/error', + '/error/404-demo', + '/error/403-demo', + '/error/500-demo' +] + +const List: any[] = [] + +const roleNames = ['超级管理员', '管理员', '普通用户', '游客'] +const menus = [ + [ + { + path: '/dashboard', + component: '#', + redirect: '/dashboard/analysis', + name: 'Dashboard', + status: Mock.Random.integer(0, 1), + id: 1, + meta: { + title: '首页', + icon: 'vi-ant-design:dashboard-filled', + alwaysShow: true + }, + children: [ + { + path: 'analysis', + component: 'views/Dashboard/Analysis', + name: 'Analysis', + status: Mock.Random.integer(0, 1), + id: 2, + meta: { + title: '分析页', + noCache: true + } + }, + { + path: 'workplace', + component: 'views/Dashboard/Workplace', + name: 'Workplace', + status: Mock.Random.integer(0, 1), + id: 3, + meta: { + title: '工作台', + noCache: true + } + } + ] + }, + { + path: '/external-link', + component: '#', + meta: { + title: '文档', + icon: 'vi-clarity:document-solid' + }, + name: 'ExternalLink', + status: Mock.Random.integer(0, 1), + id: 4, + children: [ + { + path: 'https://element-plus-admin-doc.cn/', + name: 'DocumentLink', + status: Mock.Random.integer(0, 1), + id: 5, + meta: { + title: '文档' + } + } + ] + }, + { + path: '/level', + component: '#', + redirect: '/level/menu1/menu1-1/menu1-1-1', + name: 'Level', + status: Mock.Random.integer(0, 1), + id: 6, + meta: { + title: '菜单', + icon: 'vi-carbon:skill-level-advanced' + }, + children: [ + { + path: 'menu1', + name: 'Menu1', + component: '##', + status: Mock.Random.integer(0, 1), + id: 7, + redirect: '/level/menu1/menu1-1/menu1-1-1', + meta: { + title: '菜单1' + }, + children: [ + { + path: 'menu1-1', + name: 'Menu11', + component: '##', + status: Mock.Random.integer(0, 1), + id: 8, + redirect: '/level/menu1/menu1-1/menu1-1-1', + meta: { + title: '菜单1-1', + alwaysShow: true + }, + children: [ + { + path: 'menu1-1-1', + name: 'Menu111', + component: 'views/Level/Menu111', + status: Mock.Random.integer(0, 1), + id: 9, + permission: ['edit', 'add', 'delete'], + meta: { + title: '菜单1-1-1', + permission: ['edit', 'add', 'delete'] + } + } + ] + }, + { + path: 'menu1-2', + name: 'Menu12', + component: 'views/Level/Menu12', + status: Mock.Random.integer(0, 1), + id: 10, + permission: ['edit', 'add', 'delete'], + meta: { + title: '菜单1-2', + permission: ['edit', 'add', 'delete'] + } + } + ] + }, + { + path: 'menu2', + name: 'Menu2Demo', + component: 'views/Level/Menu2', + status: Mock.Random.integer(0, 1), + id: 11, + permission: ['edit', 'add', 'delete'], + meta: { + title: '菜单2', + permission: ['edit', 'add', 'delete'] + } + } + ] + }, + { + path: '/example', + component: '#', + redirect: '/example/example-dialog', + name: 'Example', + status: Mock.Random.integer(0, 1), + id: 12, + meta: { + title: '综合示例', + icon: 'vi-ep:management', + alwaysShow: true + }, + children: [ + { + path: 'example-dialog', + component: 'views/Example/Dialog/ExampleDialog', + name: 'ExampleDialog', + status: Mock.Random.integer(0, 1), + id: 13, + permission: ['edit', 'add', 'delete'], + meta: { + title: '综合示例-弹窗', + permission: ['edit', 'add', 'delete'] + } + }, + { + path: 'example-page', + component: 'views/Example/Page/ExamplePage', + name: 'ExamplePage', + status: Mock.Random.integer(0, 1), + id: 14, + permission: ['edit', 'add', 'delete'], + meta: { + title: '综合示例-页面', + permission: ['edit', 'add', 'delete'] + } + }, + { + path: 'example-add', + component: 'views/Example/Page/ExampleAdd', + name: 'ExampleAdd', + status: Mock.Random.integer(0, 1), + id: 15, + permission: ['edit', 'add', 'delete'], + meta: { + title: '综合示例-新增', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page', + permission: ['edit', 'add', 'delete'] + } + }, + { + path: 'example-edit', + component: 'views/Example/Page/ExampleEdit', + name: 'ExampleEdit', + status: Mock.Random.integer(0, 1), + id: 16, + permission: ['edit', 'add', 'delete'], + meta: { + title: '综合示例-编辑', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page', + permission: ['edit', 'add', 'delete'] + } + }, + { + path: 'example-detail', + component: 'views/Example/Page/ExampleDetail', + name: 'ExampleDetail', + status: Mock.Random.integer(0, 1), + id: 17, + permission: ['edit', 'add', 'delete'], + meta: { + title: '综合示例-详情', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page', + permission: ['edit', 'add', 'delete'] + } + } + ] + } + ], + [ + { + path: '/dashboard', + component: '#', + redirect: '/dashboard/analysis', + name: 'Dashboard', + status: Mock.Random.integer(0, 1), + id: 1, + meta: { + title: '首页', + icon: 'vi-ant-design:dashboard-filled', + alwaysShow: true + }, + children: [ + { + path: 'analysis', + component: 'views/Dashboard/Analysis', + name: 'Analysis', + status: Mock.Random.integer(0, 1), + id: 2, + meta: { + title: '分析页', + noCache: true + } + }, + { + path: 'workplace', + component: 'views/Dashboard/Workplace', + name: 'Workplace', + status: Mock.Random.integer(0, 1), + id: 3, + meta: { + title: '工作台', + noCache: true + } + } + ] + } + ], + [ + { + path: '/external-link', + component: '#', + meta: { + title: '文档', + icon: 'vi-clarity:document-solid' + }, + name: 'ExternalLink', + status: Mock.Random.integer(0, 1), + id: 4, + children: [ + { + path: 'https://element-plus-admin-doc.cn/', + name: 'DocumentLink', + status: Mock.Random.integer(0, 1), + id: 5, + meta: { + title: '文档' + } + } + ] + }, + { + path: '/level', + component: '#', + redirect: '/level/menu1/menu1-1/menu1-1-1', + name: 'Level', + status: Mock.Random.integer(0, 1), + id: 6, + meta: { + title: '菜单', + icon: 'vi-carbon:skill-level-advanced' + }, + children: [ + { + path: 'menu1', + name: 'Menu1', + component: '##', + status: Mock.Random.integer(0, 1), + id: 7, + redirect: '/level/menu1/menu1-1/menu1-1-1', + meta: { + title: '菜单1' + }, + children: [ + { + path: 'menu1-1', + name: 'Menu11', + component: '##', + status: Mock.Random.integer(0, 1), + id: 8, + redirect: '/level/menu1/menu1-1/menu1-1-1', + meta: { + title: '菜单1-1', + alwaysShow: true + }, + children: [ + { + path: 'menu1-1-1', + name: 'Menu111', + component: 'views/Level/Menu111', + status: Mock.Random.integer(0, 1), + id: 9, + permission: ['edit', 'add', 'delete'], + meta: { + title: '菜单1-1-1', + permission: ['edit', 'add', 'delete'] + } + } + ] + }, + { + path: 'menu1-2', + name: 'Menu12', + component: 'views/Level/Menu12', + status: Mock.Random.integer(0, 1), + id: 10, + permission: ['edit', 'add', 'delete'], + meta: { + title: '菜单1-2', + permission: ['edit', 'add', 'delete'] + } + } + ] + }, + { + path: 'menu2', + name: 'Menu2Demo', + component: 'views/Level/Menu2', + status: Mock.Random.integer(0, 1), + id: 11, + permission: ['edit', 'add', 'delete'], + meta: { + title: '菜单2', + permission: ['edit', 'add', 'delete'] + } + } + ] + } + ], + [ + { + path: '/example', + component: '#', + redirect: '/example/example-dialog', + name: 'Example', + status: Mock.Random.integer(0, 1), + id: 12, + meta: { + title: '综合示例', + icon: 'vi-ep:management', + alwaysShow: true + }, + children: [ + { + path: 'example-detail', + component: 'views/Example/Page/ExampleDetail', + name: 'ExampleDetail', + status: Mock.Random.integer(0, 1), + id: 17, + permission: ['edit', 'add', 'delete'], + meta: { + title: '综合示例-详情', + noTagsView: true, + noCache: true, + hidden: true, + showMainRoute: true, + activeMenu: '/example/example-page', + permission: ['edit', 'add', 'delete'] + } + } + ] + } + ] +] + +for (let i = 0; i < 4; i++) { + List.push( + Mock.mock({ + id: toAnyString(), + // timestamp: +Mock.Random.date('T'), + roleName: roleNames[i], + role: '@first', + status: Mock.Random.integer(0, 1), + createTime: '@datetime', + remark: '@cword(10, 15)', + menu: menus[i] + }) + ) +} + +export default [ + // 列表接口 + { + url: '/mock/role/list', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: adminList + } + } + }, + { + url: '/mock/role/table', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: { + list: List, + total: 4 + } + } + } + }, + // 列表接口 + { + url: '/mock/role/list2', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: testList + } + } + }, + { + url: '/mock/role/table', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: { + list: List, + total: 4 + } + } + } + } +] diff --git a/promo-ui2/mock/table/index.mock.ts b/promo-ui2/mock/table/index.mock.ts new file mode 100644 index 0000000..a83ed14 --- /dev/null +++ b/promo-ui2/mock/table/index.mock.ts @@ -0,0 +1,319 @@ +import Mock from 'mockjs' +import { SUCCESS_CODE } from '@/constants' +import { toAnyString } from '@/utils' + +const timeout = 1000 +const count = 100 + +const baseContent = + '

I am testing data, I am testing data.

' + +interface ListProps { + id: string + author: string + title: string + content: string + importance: number + display_time: any + pageviews: number + image_uri: string + video_uri?: string +} + +interface TreeListProps { + id: string + author: string + title: string + content: string + importance: number + display_time: any + image_uri: string + pageviews: number + video_uri?: string + children?: TreeListProps[] +} + +let List: ListProps[] = [] + +for (let i = 0; i < count; i++) { + List.push( + Mock.mock({ + id: toAnyString(), + // timestamp: +Mock.Random.date('T'), + author: '@first', + title: '@title(5, 10)', + content: baseContent, + importance: '@integer(1, 3)', + display_time: '@datetime', + pageviews: '@integer(100, 500)', + image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)'), + video_uri: + '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4' + }) + ) +} + +const treeList: TreeListProps[] = [] + +for (let i = 0; i < count; i++) { + treeList.push( + Mock.mock({ + id: toAnyString(), + // timestamp: +Mock.Random.date('T'), + author: '@first', + title: '@title(5, 10)', + content: baseContent, + importance: '@integer(1, 3)', + display_time: '@datetime', + pageviews: '@integer(300, 5000)', + image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)'), + children: [ + { + id: toAnyString(), + // timestamp: +Mock.Random.date('T'), + author: '@first', + title: '@title(5, 10)', + content: baseContent, + importance: '@integer(1, 3)', + display_time: '@datetime', + pageviews: '@integer(300, 5000)', + image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)'), + children: [ + { + id: toAnyString(), + // timestamp: +Mock.Random.date('T'), + author: '@first', + title: '@title(5, 10)', + content: baseContent, + importance: '@integer(1, 3)', + display_time: '@datetime', + pageviews: '@integer(300, 5000)', + image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)') + }, + { + id: toAnyString(), + // timestamp: +Mock.Random.date('T'), + author: '@first', + title: '@title(5, 10)', + content: baseContent, + importance: '@integer(1, 3)', + display_time: '@datetime', + pageviews: '@integer(300, 5000)', + image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)') + } + ] + }, + { + id: toAnyString(), + // timestamp: +Mock.Random.date('T'), + author: '@first', + title: '@title(5, 10)', + content: baseContent, + importance: '@integer(1, 3)', + display_time: '@datetime', + pageviews: '@integer(300, 5000)', + image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)') + }, + { + id: toAnyString(), + // timestamp: +Mock.Random.date('T'), + author: '@first', + title: '@title(5, 10)', + content: baseContent, + importance: '@integer(1, 3)', + display_time: '@datetime', + pageviews: '@integer(300, 5000)', + image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)') + }, + { + id: toAnyString(), + // timestamp: +Mock.Random.date('T'), + author: '@first', + title: '@title(5, 10)', + content: baseContent, + importance: '@integer(1, 3)', + display_time: '@datetime', + pageviews: '@integer(300, 5000)', + image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)') + } + ] + // image_uri + }) + ) +} + +const cardList = [ + { + logo: 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', + name: 'Alipay', + desc: '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。' + }, + { + logo: 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', + name: 'Angular', + desc: '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。' + }, + { + logo: 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', + name: 'Bootstrap', + desc: '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。' + }, + { + logo: 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', + name: 'React', + desc: '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。' + }, + { + logo: 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', + name: 'Vue', + desc: '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。' + }, + { + logo: 'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', + name: 'Webpack', + desc: '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。' + } +] + +export default [ + // 树形列表接口 + { + url: '/mock/example/treeList', + method: 'get', + timeout, + response: ({ query }) => { + const { title, pageIndex, pageSize } = query + const mockList = treeList.filter((item) => { + if (title && item.title.indexOf(title) < 0) return false + return true + }) + const pageList = mockList.filter( + (_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1) + ) + return { + code: SUCCESS_CODE, + data: { + total: mockList.length, + list: pageList + } + } + } + }, + // 列表接口 + { + url: '/mock/example/list', + method: 'get', + timeout, + response: ({ query }) => { + const { title, pageIndex, pageSize } = query + const mockList = List.filter((item) => { + if (title && item.title.indexOf(title) < 0) return false + return true + }) + const pageList = mockList.filter( + (_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1) + ) + return { + code: SUCCESS_CODE, + data: { + total: mockList.length, + list: pageList + } + } + } + }, + // 保存接口 + { + url: '/mock/example/save', + method: 'post', + timeout, + response: ({ body }) => { + if (!body.id) { + List = [ + Object.assign(body, { + id: toAnyString() + }) + ].concat(List) + return { + code: SUCCESS_CODE, + data: 'success' + } + } else { + List.map((item) => { + if (item.id === body.id) { + for (const key in item) { + item[key] = body[key] + } + } + }) + return { + code: SUCCESS_CODE, + data: 'success' + } + } + } + }, + // 详情接口 + { + url: '/mock/example/detail', + method: 'get', + response: ({ query }) => { + const { id } = query + for (const example of List) { + if (example.id === id) { + return { + code: SUCCESS_CODE, + data: example + } + } + } + } + }, + // 删除接口 + { + url: '/mock/example/delete', + method: 'post', + response: ({ body }) => { + const ids = body.ids + if (!ids) { + return { + code: 500, + message: '请选择需要删除的数据' + } + } else { + let i = List.length + while (i--) { + if (ids.indexOf(List[i].id) !== -1) { + List.splice(i, 1) + } + } + return { + code: SUCCESS_CODE, + data: 'success' + } + } + } + }, + { + url: '/mock/card/list', + method: 'get', + timeout, + response: ({ query }) => { + const { name, pageIndex, pageSize } = query + const mockList = cardList.filter((item) => { + if (name && item.name.indexOf(name) < 0) return false + return true + }) + const pageList = mockList.filter( + (_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1) + ) + return { + code: SUCCESS_CODE, + data: { + total: mockList.length, + list: pageList + } + } + } + } +] diff --git a/promo-ui2/mock/user/index.mock.ts b/promo-ui2/mock/user/index.mock.ts new file mode 100644 index 0000000..4e5918c --- /dev/null +++ b/promo-ui2/mock/user/index.mock.ts @@ -0,0 +1,90 @@ +import { SUCCESS_CODE } from '@/constants' + +const timeout = 1000 + +const List: { + username: string + password: string + role: string + roleId: string + permissions: string | string[] +}[] = [ + { + username: 'admin', + password: 'admin', + role: 'admin', + roleId: '1', + permissions: ['*.*.*'] + }, + { + username: 'test', + password: 'test', + role: 'test', + roleId: '2', + permissions: ['example:dialog:create', 'example:dialog:delete'] + } +] + +export default [ + // 列表接口 + { + url: '/mock/user/list', + method: 'get', + response: ({ query }) => { + const { username, pageIndex, pageSize } = query + + const mockList = List.filter((item) => { + if (username && item.username.indexOf(username) < 0) return false + return true + }) + const pageList = mockList.filter( + (_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1) + ) + + return { + code: SUCCESS_CODE, + data: { + total: mockList.length, + list: pageList + } + } + } + }, + // 登录接口 + { + url: '/mock/user/login', + method: 'post', + timeout, + response: ({ body }) => { + const data = body + let hasUser = false + for (const user of List) { + if (user.username === data.username && user.password === data.password) { + hasUser = true + return { + code: SUCCESS_CODE, + data: user + } + } + } + if (!hasUser) { + return { + code: 500, + message: '账号或密码错误' + } + } + } + }, + // 退出接口 + { + url: '/mock/user/loginOut', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: null + } + } + } +] diff --git a/promo-ui2/mock/workplace/index.mock.ts b/promo-ui2/mock/workplace/index.mock.ts new file mode 100644 index 0000000..593ba27 --- /dev/null +++ b/promo-ui2/mock/workplace/index.mock.ts @@ -0,0 +1,169 @@ +import { SUCCESS_CODE } from '@/constants' + +const timeout = 1000 + +export default [ + // 获取统计 + { + url: '/mock/workplace/total', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: { + project: 40, + access: 2340, + todo: 10 + } + } + } + }, + // 获取项目 + { + url: '/mock/workplace/project', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: [ + { + name: 'Github', + icon: 'akar-icons:github-fill', + message: 'workplace.introduction', + personal: 'Archer', + time: new Date() + }, + { + name: 'Vue', + icon: 'logos:vue', + message: 'workplace.introduction', + personal: 'Archer', + time: new Date() + }, + { + name: 'Angular', + icon: 'logos:angular-icon', + message: 'workplace.introduction', + personal: 'Archer', + time: new Date() + }, + { + name: 'React', + icon: 'logos:react', + message: 'workplace.introduction', + personal: 'Archer', + time: new Date() + }, + { + name: 'Webpack', + icon: 'logos:webpack', + message: 'workplace.introduction', + personal: 'Archer', + time: new Date() + }, + { + name: 'Vite', + icon: 'vscode-icons:file-type-vite', + message: 'workplace.introduction', + personal: 'Archer', + time: new Date() + } + ] + } + } + }, + // 获取动态 + { + url: '/mock/workplace/dynamic', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: [ + { + keys: ['workplace.push', 'Github'], + time: new Date() + }, + { + keys: ['workplace.push', 'Github'], + time: new Date() + }, + { + keys: ['workplace.push', 'Github'], + time: new Date() + }, + { + keys: ['workplace.push', 'Github'], + time: new Date() + }, + { + keys: ['workplace.push', 'Github'], + time: new Date() + }, + { + keys: ['workplace.push', 'Github'], + time: new Date() + } + ] + } + } + }, + // 获取团队信息 + { + url: '/mock/workplace/team', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: [ + { + name: 'Github', + icon: 'akar-icons:github-fill' + }, + { + name: 'Vue', + icon: 'logos:vue' + }, + { + name: 'Angular', + icon: 'logos:angular-icon' + }, + { + name: 'React', + icon: 'logos:react' + }, + { + name: 'Webpack', + icon: 'logos:webpack' + }, + { + name: 'Vite', + icon: 'vscode-icons:file-type-vite' + } + ] + } + } + }, + // 获取指数 + { + url: '/mock/workplace/radar', + method: 'get', + timeout, + response: () => { + return { + code: SUCCESS_CODE, + data: [ + { name: 'workplace.quote', max: 65, personal: 42, team: 50 }, + { name: 'workplace.contribution', max: 160, personal: 30, team: 140 }, + { name: 'workplace.hot', max: 300, personal: 20, team: 28 }, + { name: 'workplace.yield', max: 130, personal: 35, team: 35 }, + { name: 'workplace.follow', max: 100, personal: 80, team: 90 } + ] + } + } + } +] diff --git a/promo-ui2/package.json b/promo-ui2/package.json new file mode 100644 index 0000000..492d3d8 --- /dev/null +++ b/promo-ui2/package.json @@ -0,0 +1,141 @@ +{ + "name": "vue-element-plus-admin", + "version": "2.9.0", + "description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。", + "author": "Archer <502431556@qq.com>", + "private": false, + "type": "module", + "scripts": { + "i": "pnpm install", + "dev": "pnpm vite --mode base", + "ts:check": "pnpm vue-tsc --noEmit --skipLibCheck", + "build:pro": "pnpm vite build --mode pro", + "build:gitee": "pnpm vite build --mode gitee", + "build:dev": "pnpm vite build --mode dev", + "build:test": "pnpm vite build --mode test", + "serve:pro": "pnpm vite preview --mode pro", + "serve:dev": "pnpm vite preview --mode dev", + "serve:test": "pnpm vite preview --mode test", + "npm:check": "pnpx npm-check-updates -u", + "clean": "pnpx rimraf node_modules", + "clean:cache": "pnpx rimraf node_modules/.cache", + "lint:eslint": "eslint . --fix \"src/**/*.{js,ts,tsx,vue,html}\"", + "lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,vue,html,md}\"", + "lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", + "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.cjs", + "prepare": "husky install", + "p": "plop", + "icon": "esno ./scripts/icon.ts" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@iconify/iconify": "^3.1.1", + "@iconify/vue": "^4.3.0", + "@vueuse/core": "^12.3.0", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^5.1.10", + "@zxcvbn-ts/core": "^3.0.4", + "animate.css": "^4.1.1", + "axios": "^1.7.9", + "cropperjs": "^1.6.2", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.13", + "driver.js": "^1.3.1", + "echarts": "^5.6.0", + "echarts-wordcloud": "^2.1.0", + "element-plus": "2.9.2", + "lodash-es": "^4.17.21", + "mitt": "^3.0.1", + "monaco-editor": "^0.52.2", + "nprogress": "^0.2.0", + "pinia": "^2.3.0", + "pinia-plugin-persistedstate": "^4.2.0", + "qrcode": "^1.5.4", + "qs": "^6.13.1", + "url": "^0.11.4", + "vue": "3.5.13", + "vue-draggable-plus": "^0.6.0", + "vue-i18n": "11.0.1", + "vue-json-pretty": "^2.4.0", + "vue-router": "^4.5.0", + "vue-types": "^5.1.3", + "xgplayer": "^3.0.20" + }, + "devDependencies": { + "@commitlint/cli": "^19.6.1", + "@commitlint/config-conventional": "^19.6.0", + "@iconify/json": "^2.2.293", + "@intlify/unplugin-vue-i18n": "^6.0.3", + "@types/fs-extra": "^11.0.4", + "@types/inquirer": "^9.0.7", + "@types/lodash-es": "^4.17.12", + "@types/mockjs": "^1.0.10", + "@types/node": "^22.10.5", + "@types/nprogress": "^0.2.3", + "@types/qrcode": "^1.5.5", + "@types/qs": "^6.9.17", + "@types/sortablejs": "^1.15.8", + "@typescript-eslint/eslint-plugin": "^8.19.1", + "@typescript-eslint/parser": "^8.19.1", + "@unocss/transformer-variant-group": "^0.65.4", + "@vitejs/plugin-legacy": "^6.0.0", + "@vitejs/plugin-vue": "^5.2.1", + "@vitejs/plugin-vue-jsx": "^4.1.1", + "autoprefixer": "^10.4.20", + "chalk": "^5.4.1", + "consola": "^3.3.3", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", + "eslint-define-config": "^2.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-vue": "^9.32.0", + "esno": "^4.8.0", + "fs-extra": "^11.2.0", + "husky": "^9.1.7", + "inquirer": "^12.3.0", + "less": "^4.2.1", + "lint-staged": "^15.3.0", + "mockjs": "^1.1.0", + "plop": "^4.0.1", + "postcss": "^8.4.49", + "postcss-html": "^1.7.0", + "postcss-less": "^6.0.0", + "prettier": "^3.4.2", + "rimraf": "^6.0.1", + "rollup": "^4.30.1", + "rollup-plugin-visualizer": "^5.14.0", + "stylelint": "^16.12.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-recommended": "^14.0.1", + "stylelint-config-standard": "^36.0.1", + "stylelint-order": "^6.0.4", + "terser": "^5.37.0", + "typescript": "5.7.3", + "typescript-eslint": "^8.19.1", + "unocss": "^0.65.4", + "vite": "6.0.7", + "vite-plugin-ejs": "^1.7.0", + "vite-plugin-eslint": "^1.8.1", + "vite-plugin-mock": "2.9.6", + "vite-plugin-progress": "^0.0.7", + "vite-plugin-purge-icons": "^0.10.0", + "vite-plugin-style-import": "2.0.0", + "vite-plugin-svg-icons": "^2.0.1", + "vite-plugin-url-copy": "^1.1.4", + "vue-tsc": "^2.2.0" + }, + "packageManager": "pnpm@9.15.3", + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8.1.0" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/kailong321200875/vue-element-plus-admin.git" + }, + "bugs": { + "url": "https://github.com/kailong321200875/vue-element-plus-admin/issues" + }, + "homepage": "https://github.com/kailong321200875/vue-element-plus-admin" +} diff --git a/promo-ui2/plop/component/component.hbs b/promo-ui2/plop/component/component.hbs new file mode 100644 index 0000000..21e5653 --- /dev/null +++ b/promo-ui2/plop/component/component.hbs @@ -0,0 +1,11 @@ + + + diff --git a/promo-ui2/plop/component/index.hbs b/promo-ui2/plop/component/index.hbs new file mode 100644 index 0000000..aca6f0a --- /dev/null +++ b/promo-ui2/plop/component/index.hbs @@ -0,0 +1,3 @@ +import {{ upperFirstName }} from './src/{{ upperFirstName }}.vue' + +export { {{ upperFirstName }} } diff --git a/promo-ui2/plop/component/prompt.cjs b/promo-ui2/plop/component/prompt.cjs new file mode 100644 index 0000000..98a852f --- /dev/null +++ b/promo-ui2/plop/component/prompt.cjs @@ -0,0 +1,38 @@ +const toUpperCase = (str) => str.charAt(0).toUpperCase() + str.slice(1) + +module.exports = { + description: 'Create vue component', + prompts: [ + { + type: 'input', + name: 'name', + message: '请输入组件名称(Please enter the component name)' + } + ], + actions: (data) => { + const { name } = data + const upperFirstName = toUpperCase(name) + + const actions = [] + if (name) { + actions.push({ + type: 'add', + path: `./src/components/${upperFirstName}/src/${upperFirstName}.vue`, + templateFile: './plop/component/component.hbs', + data: { + name, + upperFirstName + } + }, { + type: 'add', + path: `./src/components/${upperFirstName}/index.ts`, + templateFile: './plop/component/index.hbs', + data: { + upperFirstName + } + }) + } + + return actions + } +} diff --git a/promo-ui2/plop/view/prompt.cjs b/promo-ui2/plop/view/prompt.cjs new file mode 100644 index 0000000..dbf324f --- /dev/null +++ b/promo-ui2/plop/view/prompt.cjs @@ -0,0 +1,37 @@ +const toUpperCase = (str) => str.charAt(0).toUpperCase() + str.slice(1) + +module.exports = { + description: 'Create vue view', + prompts: [ + { + type: 'input', + name: 'path', + message: '请输入路径(Please enter a path)', + default: 'views' + }, + { + type: 'input', + name: 'name', + message: '请输入模块名称(Please enter module name)' + } + ], + actions: (data) => { + const { name, path } = data + const upperFirstName = toUpperCase(name) + + const actions = [] + if (name) { + actions.push({ + type: 'add', + path: `./src/${path}/${upperFirstName}.vue`, + templateFile: './plop/view/view.hbs', + data: { + name, + upperFirstName + } + }) + } + + return actions + } +} diff --git a/promo-ui2/plop/view/view.hbs b/promo-ui2/plop/view/view.hbs new file mode 100644 index 0000000..0ae9c15 --- /dev/null +++ b/promo-ui2/plop/view/view.hbs @@ -0,0 +1,7 @@ + + + diff --git a/promo-ui2/plopfile.cjs b/promo-ui2/plopfile.cjs new file mode 100644 index 0000000..ad90fa1 --- /dev/null +++ b/promo-ui2/plopfile.cjs @@ -0,0 +1,7 @@ +const viewGenerator = require('./plop/view/prompt.cjs') +const componentGenerator = require('./plop/component/prompt.cjs') + +module.exports = function (plop) { + plop.setGenerator('view', viewGenerator) + plop.setGenerator('component', componentGenerator) +} diff --git a/promo-ui2/pnpm-lock.yaml b/promo-ui2/pnpm-lock.yaml new file mode 100644 index 0000000..e9b48b1 --- /dev/null +++ b/promo-ui2/pnpm-lock.yaml @@ -0,0 +1,11894 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.13(typescript@5.7.3)) + '@iconify/iconify': + specifier: ^3.1.1 + version: 3.1.1 + '@iconify/vue': + specifier: ^4.3.0 + version: 4.3.0(vue@3.5.13(typescript@5.7.3)) + '@vueuse/core': + specifier: ^12.3.0 + version: 12.3.0(typescript@5.7.3) + '@wangeditor/editor': + specifier: ^5.1.23 + version: 5.1.23 + '@wangeditor/editor-for-vue': + specifier: ^5.1.10 + version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.13(typescript@5.7.3)) + '@zxcvbn-ts/core': + specifier: ^3.0.4 + version: 3.0.4 + animate.css: + specifier: ^4.1.1 + version: 4.1.1 + axios: + specifier: ^1.7.9 + version: 1.7.9 + cropperjs: + specifier: ^1.6.2 + version: 1.6.2 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + driver.js: + specifier: ^1.3.1 + version: 1.3.1 + echarts: + specifier: ^5.6.0 + version: 5.6.0 + echarts-wordcloud: + specifier: ^2.1.0 + version: 2.1.0(echarts@5.6.0) + element-plus: + specifier: 2.9.2 + version: 2.9.2(vue@3.5.13(typescript@5.7.3)) + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + mitt: + specifier: ^3.0.1 + version: 3.0.1 + monaco-editor: + specifier: ^0.52.2 + version: 0.52.2 + nprogress: + specifier: ^0.2.0 + version: 0.2.0 + pinia: + specifier: ^2.3.0 + version: 2.3.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) + pinia-plugin-persistedstate: + specifier: ^4.2.0 + version: 4.2.0(pinia@2.3.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))(rollup@4.30.1) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + qs: + specifier: ^6.13.1 + version: 6.13.1 + url: + specifier: ^0.11.4 + version: 0.11.4 + vue: + specifier: 3.5.13 + version: 3.5.13(typescript@5.7.3) + vue-draggable-plus: + specifier: ^0.6.0 + version: 0.6.0(@types/sortablejs@1.15.8) + vue-i18n: + specifier: 11.0.1 + version: 11.0.1(vue@3.5.13(typescript@5.7.3)) + vue-json-pretty: + specifier: ^2.4.0 + version: 2.4.0(vue@3.5.13(typescript@5.7.3)) + vue-router: + specifier: ^4.5.0 + version: 4.5.0(vue@3.5.13(typescript@5.7.3)) + vue-types: + specifier: ^5.1.3 + version: 5.1.3(vue@3.5.13(typescript@5.7.3)) + xgplayer: + specifier: ^3.0.20 + version: 3.0.20(core-js@3.40.0) + devDependencies: + '@commitlint/cli': + specifier: ^19.6.1 + version: 19.6.1(@types/node@22.10.5)(typescript@5.7.3) + '@commitlint/config-conventional': + specifier: ^19.6.0 + version: 19.6.0 + '@iconify/json': + specifier: ^2.2.293 + version: 2.2.293 + '@intlify/unplugin-vue-i18n': + specifier: ^6.0.3 + version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.17.0(jiti@2.4.2))(rollup@4.30.1)(typescript@5.7.3)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)) + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 + '@types/inquirer': + specifier: ^9.0.7 + version: 9.0.7 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/mockjs': + specifier: ^1.0.10 + version: 1.0.10 + '@types/node': + specifier: ^22.10.5 + version: 22.10.5 + '@types/nprogress': + specifier: ^0.2.3 + version: 0.2.3 + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.5 + '@types/qs': + specifier: ^6.9.17 + version: 6.9.17 + '@types/sortablejs': + specifier: ^1.15.8 + version: 1.15.8 + '@typescript-eslint/eslint-plugin': + specifier: ^8.19.1 + version: 8.19.1(@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.19.1 + version: 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@unocss/transformer-variant-group': + specifier: ^0.65.4 + version: 0.65.4 + '@vitejs/plugin-legacy': + specifier: ^6.0.0 + version: 6.0.0(terser@5.37.0)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.1(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)) + '@vitejs/plugin-vue-jsx': + specifier: ^4.1.1 + version: 4.1.1(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.49) + chalk: + specifier: ^5.4.1 + version: 5.4.1 + consola: + specifier: ^3.3.3 + version: 3.3.3 + eslint: + specifier: ^9.17.0 + version: 9.17.0(jiti@2.4.2) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.17.0(jiti@2.4.2)) + eslint-define-config: + specifier: ^2.1.0 + version: 2.1.0 + eslint-plugin-prettier: + specifier: ^5.2.1 + version: 5.2.1(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(prettier@3.4.2) + eslint-plugin-vue: + specifier: ^9.32.0 + version: 9.32.0(eslint@9.17.0(jiti@2.4.2)) + esno: + specifier: ^4.8.0 + version: 4.8.0 + fs-extra: + specifier: ^11.2.0 + version: 11.2.0 + husky: + specifier: ^9.1.7 + version: 9.1.7 + inquirer: + specifier: ^12.3.0 + version: 12.3.0(@types/node@22.10.5) + less: + specifier: ^4.2.1 + version: 4.2.1 + lint-staged: + specifier: ^15.3.0 + version: 15.3.0 + mockjs: + specifier: ^1.1.0 + version: 1.1.0 + plop: + specifier: ^4.0.1 + version: 4.0.1 + postcss: + specifier: ^8.4.49 + version: 8.4.49 + postcss-html: + specifier: ^1.7.0 + version: 1.7.0 + postcss-less: + specifier: ^6.0.0 + version: 6.0.0(postcss@8.4.49) + prettier: + specifier: ^3.4.2 + version: 3.4.2 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + rollup: + specifier: ^4.30.1 + version: 4.30.1 + rollup-plugin-visualizer: + specifier: ^5.14.0 + version: 5.14.0(rollup@4.30.1) + stylelint: + specifier: ^16.12.0 + version: 16.12.0(typescript@5.7.3) + stylelint-config-html: + specifier: ^1.1.0 + version: 1.1.0(postcss-html@1.7.0)(stylelint@16.12.0(typescript@5.7.3)) + stylelint-config-recommended: + specifier: ^14.0.1 + version: 14.0.1(stylelint@16.12.0(typescript@5.7.3)) + stylelint-config-standard: + specifier: ^36.0.1 + version: 36.0.1(stylelint@16.12.0(typescript@5.7.3)) + stylelint-order: + specifier: ^6.0.4 + version: 6.0.4(stylelint@16.12.0(typescript@5.7.3)) + terser: + specifier: ^5.37.0 + version: 5.37.0 + typescript: + specifier: 5.7.3 + version: 5.7.3 + typescript-eslint: + specifier: ^8.19.1 + version: 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + unocss: + specifier: ^0.65.4 + version: 0.65.4(postcss@8.4.49)(rollup@4.30.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)) + vite: + specifier: 6.0.7 + version: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + vite-plugin-ejs: + specifier: ^1.7.0 + version: 1.7.0(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-plugin-eslint: + specifier: ^1.8.1 + version: 1.8.1(eslint@9.17.0(jiti@2.4.2))(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-plugin-mock: + specifier: 2.9.6 + version: 2.9.6(mockjs@1.1.0)(rollup@4.30.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-plugin-progress: + specifier: ^0.0.7 + version: 0.0.7(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-plugin-purge-icons: + specifier: ^0.10.0 + version: 0.10.0(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-plugin-style-import: + specifier: 2.0.0 + version: 2.0.0(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-plugin-svg-icons: + specifier: ^2.0.1 + version: 2.0.1(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-plugin-url-copy: + specifier: ^1.1.4 + version: 1.1.4(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vue-tsc: + specifier: ^2.2.0 + version: 2.2.0(typescript@5.7.3) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@0.4.1': + resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.3': + resolution: {integrity: sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.3': + resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.25.9': + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.26.3': + resolution: {integrity: sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.3': + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-member-expression-to-functions@7.25.9': + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.25.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.25.9': + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.25.9': + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.25.9': + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.3': + resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.25.9': + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.25.9': + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.25.9': + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.25.9': + resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.25.9': + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.25.9': + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.26.0': + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.25.9': + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.25.9': + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.25.9': + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.25.9': + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.25.9': + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.25.9': + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.26.3': + resolution: {integrity: sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.25.9': + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.25.9': + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.25.9': + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.25.9': + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.25.9': + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9': + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.25.9': + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.25.9': + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.26.3': + resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.25.9': + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.25.9': + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.25.9': + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9': + resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.25.9': + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.25.9': + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.25.9': + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.25.9': + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.25.9': + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.25.9': + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.25.9': + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.25.9': + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.25.9': + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.25.9': + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.26.0': + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.25.9': + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.25.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.25.9': + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.25.9': + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.25.9': + resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.25.9': + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.26.3': + resolution: {integrity: sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.25.9': + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.25.9': + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.25.9': + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9': + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.26.0': + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/standalone@7.26.4': + resolution: {integrity: sha512-SF+g7S2mhTT1b7CHyfNjDkPU1corxg4LPYsyP0x5KuCl+EbtBQHRLqr9N3q7e7+x7NQ5LYxQf8mJ2PmzebLr0A==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.26.4': + resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.3': + resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + engines: {node: '>=6.9.0'} + + '@commitlint/cli@19.6.1': + resolution: {integrity: sha512-8hcyA6ZoHwWXC76BoC8qVOSr8xHy00LZhZpauiD0iO0VYbVhMnED0da85lTfIULxl7Lj4c6vZgF0Wu/ed1+jlQ==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@19.6.0': + resolution: {integrity: sha512-DJT40iMnTYtBtUfw9ApbsLZFke1zKh6llITVJ+x9mtpHD08gsNXaIRqHTmwTZL3dNX5+WoyK7pCN/5zswvkBCQ==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@19.5.0': + resolution: {integrity: sha512-CHtj92H5rdhKt17RmgALhfQt95VayrUo2tSqY9g2w+laAXyk7K/Ef6uPm9tn5qSIwSmrLjKaXK9eiNuxmQrDBw==} + engines: {node: '>=v18'} + + '@commitlint/ensure@19.5.0': + resolution: {integrity: sha512-Kv0pYZeMrdg48bHFEU5KKcccRfKmISSm9MvgIgkpI6m+ohFTB55qZlBW6eYqh/XDfRuIO0x4zSmvBjmOwWTwkg==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@19.5.0': + resolution: {integrity: sha512-aqyGgytXhl2ejlk+/rfgtwpPexYyri4t8/n4ku6rRJoRhGZpLFMqrZ+YaubeGysCP6oz4mMA34YSTaSOKEeNrg==} + engines: {node: '>=v18'} + + '@commitlint/format@19.5.0': + resolution: {integrity: sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@19.6.0': + resolution: {integrity: sha512-Ov6iBgxJQFR9koOupDPHvcHU9keFupDgtB3lObdEZDroiG4jj1rzky60fbQozFKVYRTUdrBGICHG0YVmRuAJmw==} + engines: {node: '>=v18'} + + '@commitlint/lint@19.6.0': + resolution: {integrity: sha512-LRo7zDkXtcIrpco9RnfhOKeg8PAnE3oDDoalnrVU/EVaKHYBWYL1DlRR7+3AWn0JiBqD8yKOfetVxJGdEtZ0tg==} + engines: {node: '>=v18'} + + '@commitlint/load@19.6.1': + resolution: {integrity: sha512-kE4mRKWWNju2QpsCWt428XBvUH55OET2N4QKQ0bF85qS/XbsRGG1MiTByDNlEVpEPceMkDr46LNH95DtRwcsfA==} + engines: {node: '>=v18'} + + '@commitlint/message@19.5.0': + resolution: {integrity: sha512-R7AM4YnbxN1Joj1tMfCyBryOC5aNJBdxadTZkuqtWi3Xj0kMdutq16XQwuoGbIzL2Pk62TALV1fZDCv36+JhTQ==} + engines: {node: '>=v18'} + + '@commitlint/parse@19.5.0': + resolution: {integrity: sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw==} + engines: {node: '>=v18'} + + '@commitlint/read@19.5.0': + resolution: {integrity: sha512-TjS3HLPsLsxFPQj6jou8/CZFAmOP2y+6V4PGYt3ihbQKTY1Jnv0QG28WRKl/d1ha6zLODPZqsxLEov52dhR9BQ==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@19.5.0': + resolution: {integrity: sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA==} + engines: {node: '>=v18'} + + '@commitlint/rules@19.6.0': + resolution: {integrity: sha512-1f2reW7lbrI0X0ozZMesS/WZxgPa4/wi56vFuJENBmed6mWq5KsheN/nxqnl/C23ioxpPO/PL6tXpiiFy5Bhjw==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@19.5.0': + resolution: {integrity: sha512-R772oj3NHPkodOSRZ9bBVNq224DOxQtNef5Pl8l2M8ZnkkzQfeSTr4uxawV2Sd3ui05dUVzvLNnzenDBO1KBeQ==} + engines: {node: '>=v18'} + + '@commitlint/top-level@19.5.0': + resolution: {integrity: sha512-IP1YLmGAk0yWrImPRRc578I3dDUI5A2UBJx9FbSOjxe9sTlzFiwVJ+zeMLgAtHMtGZsC8LUnzmW1qRemkFU4ng==} + engines: {node: '>=v18'} + + '@commitlint/types@19.5.0': + resolution: {integrity: sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==} + engines: {node: '>=v18'} + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.2': + resolution: {integrity: sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@dual-bundle/import-meta-resolve@4.1.0': + resolution: {integrity: sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.19.1': + resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.9.1': + resolution: {integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.17.0': + resolution: {integrity: sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.5': + resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.4': + resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.6.9': + resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} + + '@floating-ui/dom@1.6.13': + resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + + '@iconify/iconify@2.1.2': + resolution: {integrity: sha512-QcUzFeEWkE/mW+BVtEGmcWATClcCOIJFiYUD/PiCWuTcdEA297o8D4oN6Ra44WrNOHu1wqNW4J0ioaDIiqaFOQ==} + + '@iconify/iconify@3.1.1': + resolution: {integrity: sha512-1nemfyD/OJzh9ALepH7YfuuP8BdEB24Skhd8DXWh0hzcOxImbb1ZizSZkpCzAwSZSGcJFmscIBaBQu+yLyWaxQ==} + + '@iconify/json@2.2.293': + resolution: {integrity: sha512-eMRJNfQa+MXmE7I9noABNSVPxwsCkgmfgzeh84IKS2Su1cdQhi5FtsZa6YjEXK8y1cpbfv6pYk7KKJ6fV3mvOw==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.2.1': + resolution: {integrity: sha512-0/7J7hk4PqXmxo5PDBDxmnecw5PxklZJfNjIVG9FM0mEfVrvfudS22rYWsqVk6gR3UJ/mSYS90X4R3znXnqfNA==} + + '@iconify/vue@4.3.0': + resolution: {integrity: sha512-Xq0h6zMrHBbrW8jXJ9fISi+x8oDQllg5hTDkDuxnWiskJ63rpJu9CvJshj8VniHVTbsxCg9fVoPAaNp3RQI5OQ==} + peerDependencies: + vue: '>=3' + + '@inquirer/checkbox@4.0.4': + resolution: {integrity: sha512-fYAKCAcGNMdfjL6hZTRUwkIByQ8EIZCXKrIQZH7XjADnN/xvRUhj8UdBbpC4zoUzvChhkSC/zRKaP/tDs3dZpg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/confirm@5.1.1': + resolution: {integrity: sha512-vVLSbGci+IKQvDOtzpPTCOiEJCNidHcAq9JYVoWTW0svb5FiwSLotkM+JXNXejfjnzVYV9n0DTBythl9+XgTxg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/core@10.1.2': + resolution: {integrity: sha512-bHd96F3ezHg1mf/J0Rb4CV8ndCN0v28kUlrHqP7+ECm1C/A+paB7Xh2lbMk6x+kweQC+rZOxM/YeKikzxco8bQ==} + engines: {node: '>=18'} + + '@inquirer/editor@4.2.1': + resolution: {integrity: sha512-xn9aDaiP6nFa432i68JCaL302FyL6y/6EG97nAtfIPnWZ+mWPgCMLGc4XZ2QQMsZtu9q3Jd5AzBPjXh10aX9kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/expand@4.0.4': + resolution: {integrity: sha512-GYocr+BPyxKPxQ4UZyNMqZFSGKScSUc0Vk17II3J+0bDcgGsQm0KYQNooN1Q5iBfXsy3x/VWmHGh20QnzsaHwg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/figures@1.0.9': + resolution: {integrity: sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==} + engines: {node: '>=18'} + + '@inquirer/input@4.1.1': + resolution: {integrity: sha512-nAXAHQndZcXB+7CyjIW3XuQZZHbQQ0q8LX6miY6bqAWwDzNa9JUioDBYrFmOUNIsuF08o1WT/m2gbBXvBhYVxg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/number@3.0.4': + resolution: {integrity: sha512-DX7a6IXRPU0j8kr2ovf+QaaDiIf+zEKaZVzCWdLOTk7XigqSXvoh4cul7x68xp54WTQrgSnW7P1WBJDbyY3GhA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/password@4.0.4': + resolution: {integrity: sha512-wiliQOWdjM8FnBmdIHtQV2Ca3S1+tMBUerhyjkRCv1g+4jSvEweGu9GCcvVEgKDhTBT15nrxvk5/bVrGUqSs1w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/prompts@7.2.1': + resolution: {integrity: sha512-v2JSGri6/HXSfoGIwuKEn8sNCQK6nsB2BNpy2lSX6QH9bsECrMv93QHnj5+f+1ZWpF/VNioIV2B/PDox8EvGuQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/rawlist@4.0.4': + resolution: {integrity: sha512-IsVN2EZdNHsmFdKWx9HaXb8T/s3FlR/U1QPt9dwbSyPtjFbMTlW9CRFvnn0bm/QIsrMRD2oMZqrQpSWPQVbXXg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/search@3.0.4': + resolution: {integrity: sha512-tSkJk2SDmC2MEdTIjknXWmCnmPr5owTs9/xjfa14ol1Oh95n6xW7SYn5fiPk4/vrJPys0ggSWiISdPze4LTa7A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/select@4.0.4': + resolution: {integrity: sha512-ZzYLuLoUzTIW9EJm++jBpRiTshGqS3Q1o5qOEQqgzaBlmdsjQr6pA4TUNkwu6OBYgM2mIRbCz6mUhFDfl/GF+w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/type@3.0.2': + resolution: {integrity: sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@intlify/bundle-utils@10.0.0': + resolution: {integrity: sha512-BR5yLOkF2dzrARTbAg7RGAIPcx9Aark7p1K/0O285F7rfzso9j2dsa+S4dA67clZ0rToZ10NSSTfbyUptVu7Bg==} + engines: {node: '>= 18'} + peerDependencies: + petite-vue-i18n: '*' + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@intlify/core-base@11.0.1': + resolution: {integrity: sha512-NAmhw1l/llM0HZRpagR/ChJTNymW4ll6/4EDSJML5c8L5Hl/+k6UyF8EIgE6DeHpfheQujkSRngauViHqq6jJQ==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@11.0.1': + resolution: {integrity: sha512-5RFH8x+Mn3mbjcHXnb6KCXGiczBdiQkWkv99iiA0JpKrNuTAQeW59Pjq/uObMB0eR0shnKYGTkIJxum+DbL3sw==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@12.0.0-alpha.4': + resolution: {integrity: sha512-F4yHuJzI2ZXtXTW2B1VhuNULAOjR5bniFSC/Z0rLWRYPO43pVgKLu+FPDoJq13OjTFq4X5hx2s855M4gqPU/pA==} + engines: {node: '>= 22'} + + '@intlify/shared@11.0.1': + resolution: {integrity: sha512-lH164+aDDptHZ3dBDbIhRa1dOPQUp+83iugpc+1upTOWCnwyC1PVis6rSWNMMJ8VQxvtHQB9JMib48K55y0PvQ==} + engines: {node: '>= 16'} + + '@intlify/shared@11.4.8': + resolution: {integrity: sha512-XbRgrv+XEuvDr7UCY55oibVrh+o4u+A0VB6nSL0F5Z8LcZxE/8j573LYG6bCrOigIcHdGpSNI7Rh5UpC5/B/eg==} + engines: {node: '>= 22'} + + '@intlify/shared@12.0.0-alpha.4': + resolution: {integrity: sha512-MfunEN3/yQjD1Q9HnD4Qtvg/qLYnR+tK3mEz/EEvikJO+Hu8ICe7Rqh6uusLSjpWy6ECVt2p5HGXlYA12UBweA==} + engines: {node: '>= 22'} + + '@intlify/unplugin-vue-i18n@6.0.3': + resolution: {integrity: sha512-9ZDjBlhUHtgjRl23TVcgfJttgu8cNepwVhWvOv3mUMRDAhjW0pur1mWKEUKr1I8PNwE4Gvv2IQ1xcl4RL0nG0g==} + engines: {node: '>= 18'} + peerDependencies: + petite-vue-i18n: '*' + vue: ^3.2.25 + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@intlify/vue-i18n-extensions@8.0.0': + resolution: {integrity: sha512-w0+70CvTmuqbskWfzeYhn0IXxllr6mU+IeM2MU0M+j9OW64jkrvqY+pYFWrUnIIC9bEdij3NICruicwd5EgUuQ==} + engines: {node: '>= 18'} + peerDependencies: + '@intlify/shared': ^9.0.0 || ^10.0.0 || ^11.0.0 + '@vue/compiler-dom': ^3.0.0 + vue: ^3.0.0 + vue-i18n: ^9.0.0 || ^10.0.0 || ^11.0.0 + peerDependenciesMeta: + '@intlify/shared': + optional: true + '@vue/compiler-dom': + optional: true + vue: + optional: true + vue-i18n: + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha1-dhnC6yGyVIP20WdUi0z9WnSIw9U=} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha1-W9Jir5Tp0lvR5xsF3u1Eh2oiLos=} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha1-6Vc36LtnRt3t9pxVaVNJTxlv5po=} + engines: {node: '>= 8'} + + '@nuxt/kit@3.15.1': + resolution: {integrity: sha512-7cVWjzfz3L6CsZrg6ppDZa7zGrZxCSfZjEQDIvVFn4mFKtJlK9k2izf5EewL6luzWwIQojkZAC3iq/1wtgI0Xw==} + engines: {node: '>=18.20.5'} + + '@nuxt/schema@3.15.1': + resolution: {integrity: sha512-n5kOHt8uUyUM9z4Wu/8tIZkBYh3KTCGvyruG6oD9bfeT4OaS21+X3M7XsTXFMe+eYBZA70IFFlWn1JJZIPsKeA==} + engines: {node: ^14.18.0 || >=16.10.0} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + + '@purge-icons/core@0.10.0': + resolution: {integrity: sha512-AtJbZv5Yy+vWX5v32DPTr+CW7AkSK8HJx52orDbrYt/9s4lGM2t4KKAmwaTQEH2HYr2HVh1mlqs54/S1s3WT1g==} + + '@purge-icons/generated@0.10.0': + resolution: {integrity: sha512-I+1yN7/yDy/eZzfhAZqKF8Z6FM8D/O1vempbPrHJ0m9HlZwvf8sWXOArPJ2qRQGB6mJUVSpaXkoGBuoz1GQX5A==} + + '@rollup/plugin-node-resolve@13.3.0': + resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^2.42.0 + + '@rollup/pluginutils@3.1.0': + resolution: {integrity: sha1-cGtFJO5tyLEDs8mVUz5a1oDAK5s=} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.30.1': + resolution: {integrity: sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.30.1': + resolution: {integrity: sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.30.1': + resolution: {integrity: sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.30.1': + resolution: {integrity: sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.30.1': + resolution: {integrity: sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.30.1': + resolution: {integrity: sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': + resolution: {integrity: sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.30.1': + resolution: {integrity: sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.30.1': + resolution: {integrity: sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.30.1': + resolution: {integrity: sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': + resolution: {integrity: sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': + resolution: {integrity: sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.30.1': + resolution: {integrity: sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-s390x-gnu@4.30.1': + resolution: {integrity: sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.30.1': + resolution: {integrity: sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.30.1': + resolution: {integrity: sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-win32-arm64-msvc@4.30.1': + resolution: {integrity: sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.30.1': + resolution: {integrity: sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.30.1': + resolution: {integrity: sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==} + cpu: [x64] + os: [win32] + + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@transloadit/prettier-bytes@0.0.7': + resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==} + + '@trysound/sax@0.2.0': + resolution: {integrity: sha1-zMqrdYr1Z2Hre/N69vA/Mm3XmK0=} + engines: {node: '>=10.13.0'} + + '@types/conventional-commits-parser@5.0.1': + resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} + + '@types/eslint@8.56.12': + resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} + + '@types/estree@0.0.39': + resolution: {integrity: sha1-4Xfmme4bjCLSMXTKqnQiZEOJUJ8=} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/event-emitter@0.3.5': + resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==} + + '@types/fined@1.1.5': + resolution: {integrity: sha512-2N93vadEGDFhASTIRbizbl4bNqpMOId5zZfj6hHqYZfEzEfO9onnU4Im8xvzo8uudySDveDHBOOSlTWf38ErfQ==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/inquirer@9.0.7': + resolution: {integrity: sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/liftoff@4.0.3': + resolution: {integrity: sha512-UgbL2kR5pLrWICvr8+fuSg0u43LY250q7ZMkC+XKC3E+rs/YBDEnQIzsnhU5dYsLlwMi3R75UvCL87pObP1sxw==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.14': + resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==} + + '@types/mockjs@1.0.10': + resolution: {integrity: sha512-SXgrhajHG7boLv6oU93CcmdDm0HYRiceuz6b+7z+/2lCJPTWDv0V5YiwFHT2ejE4bQqgSXQiVPQYPWv7LGsK1g==} + + '@types/node@10.17.60': + resolution: {integrity: sha1-NfPWIT2u2V2n8Pc+dbzGmA6QWXs=} + + '@types/node@22.10.5': + resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==} + + '@types/nprogress@0.2.3': + resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} + + '@types/qrcode@1.5.5': + resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} + + '@types/qs@6.9.17': + resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} + + '@types/resolve@1.17.1': + resolution: {integrity: sha1-Ov1q2JZ8d+Q3bFmKgt3Vj0bsRdY=} + + '@types/sortablejs@1.15.8': + resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + + '@types/svgo@2.6.4': + resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==} + + '@types/through@0.0.33': + resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@typescript-eslint/eslint-plugin@8.19.1': + resolution: {integrity: sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/parser@8.19.1': + resolution: {integrity: sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/scope-manager@8.19.1': + resolution: {integrity: sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.19.1': + resolution: {integrity: sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/types@8.19.1': + resolution: {integrity: sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.19.1': + resolution: {integrity: sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/utils@8.19.1': + resolution: {integrity: sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/visitor-keys@8.19.1': + resolution: {integrity: sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unocss/astro@0.65.4': + resolution: {integrity: sha512-ex1CJOQ6yeftBEPcbA9/W47/YoV+mhQnrAoc8MA1VVrvvFKDitICFU62+nSt3NWRe53XL/fXnQbcbCb8AAgKlA==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 + peerDependenciesMeta: + vite: + optional: true + + '@unocss/cli@0.65.4': + resolution: {integrity: sha512-D/4hY5Hezh3QETscl4i+ojb+q8YU9Cl9AYJ8v3gsjc/GjTmEuIOD5V4x+/aN25vY5wjqgoApOgaIDGCV3b+2Ig==} + engines: {node: '>=14'} + hasBin: true + + '@unocss/config@0.65.4': + resolution: {integrity: sha512-/vCt4AXnJ4p4Ow6xqsYwdrelF9533yhZjzkg3SQmL3rKeSkicPayKpeq8nkYECdhDI03VTCVD+6oh5Y/26Hg7A==} + engines: {node: '>=14'} + + '@unocss/core@0.65.4': + resolution: {integrity: sha512-a2JOoFutrhqd5RgPhIR5FIXrDoHDU3gwCbPrpT6KYTjsqlSc/fv02yZ+JGOZFN3MCFhCmaPTs+idDFtwb3xU8g==} + + '@unocss/extractor-arbitrary-variants@0.65.4': + resolution: {integrity: sha512-GbvTgsDaHplfWfsQtOY8RrvEZvptmvR9k9NwQ5NsZBNIG1JepYVel93CVQvsxT5KioKcoWngXxTYLNOGyxLs0g==} + + '@unocss/inspector@0.65.4': + resolution: {integrity: sha512-byg9x549Ul17U4Ety7ufDwC0UOygypoq4QnLEPzhlZ0KJG1f7WmXKYanOhupeg3h4qCj6Nc/xdZYMGbHl9QRIg==} + + '@unocss/postcss@0.65.4': + resolution: {integrity: sha512-8peDRo0+rNQsnKh/H2uZEVy67sV2cC16rAeSLpgbVJUMNfZlmF0rC2DNGsOV17uconUXSwz7+mGcHKNiv+8YlQ==} + engines: {node: '>=14'} + peerDependencies: + postcss: ^8.4.21 + + '@unocss/preset-attributify@0.65.4': + resolution: {integrity: sha512-zxE9hJJ5b37phjdzDdZsxX559ZlmH9rFlY5LVEcQySTnsfY0znviHxPbD2iRpCBCRd+YC5HfFd2jb3XlnTKMJQ==} + + '@unocss/preset-icons@0.65.4': + resolution: {integrity: sha512-5sSzTN72X2Ag3VH48xY1pYudeWnql9jqdMiwgZuLJcmvETBNGelXy2wGxm7tsUUEx/l40Yr04Ck8XRPGT9jLBw==} + + '@unocss/preset-mini@0.65.4': + resolution: {integrity: sha512-dcO2PzSl87qN1KdQWcfZDIKEhpdFeImWbYfiXtE7k6pi1393FJkdHEopgI/1ZciIQN1CkTvQJ5c7EpEVWftYRA==} + + '@unocss/preset-tagify@0.65.4': + resolution: {integrity: sha512-qll6koqdFEkvmz594vKnxj9+3nfM3ugkJxYHrTkqtwx7DAnTgtM8fInFFGZelvjwUzR3o3+Zw6uMhFkLTVTfvg==} + + '@unocss/preset-typography@0.65.4': + resolution: {integrity: sha512-Dl940ATrviWD9Vh+4fcN0QZXb6wA7al+c7QkdVAzW7I+NtdN2ELvLcN0cY22KnLRpwztzmg52Qp2J/1QnqrLTw==} + + '@unocss/preset-uno@0.65.4': + resolution: {integrity: sha512-56bdBtf476i+soQCQmT36uGzcF2z+7DGCnG1hwWiw6XAbL6gmRMQsubwi1c8z8TcTQNBsOFUnOziFil0gbWufw==} + + '@unocss/preset-web-fonts@0.65.4': + resolution: {integrity: sha512-UB/MvXHUTqMNVH1bbiKZ/ZtZUI5tsYlTYAvBrnXPO1Cztuwr8hJKSi4RCfI9g+YYtKHX4uYuxUbW5bcN85gmBQ==} + + '@unocss/preset-wind@0.65.4': + resolution: {integrity: sha512-0rbNbw5E8Lvh2yf4R1Mq+lxI/wL5Tm6+r+crE0uAAhCPe9kxPHW4k+x1cWKDIwq6Vudlm3cNX85N49wN5tYgdA==} + + '@unocss/reset@0.65.4': + resolution: {integrity: sha512-m685H0KFvVMz6R2i5GDIFv4RS9Z7y2G8hJK7xg2OWli+7w8l2ZMihYvXKofPsst4q/ms8EgKXpWc/qqUOTucvA==} + + '@unocss/rule-utils@0.65.4': + resolution: {integrity: sha512-+EzdJEWcqGcO6HwbBTe7vEdBRpuKkBiz4MycQeLD6GEio04T45y6VHHO7/WTqxltbO4YwwW9/s2TKRMxKtoG8g==} + engines: {node: '>=14'} + + '@unocss/transformer-attributify-jsx@0.65.4': + resolution: {integrity: sha512-n438EzWdTKlLCOlAUSpFjmH6FflctqzIReMzMZSJDkmkorymc+C5GpjN3Nty2cKRJXIl6Vwq0oxPuB59RT+FIw==} + + '@unocss/transformer-compile-class@0.65.4': + resolution: {integrity: sha512-n1yHDC/iIbcj/9fBUTXkSoASKfLBuRoCN7P1a0ecPc8Gu+uOGfoxafOhrlqC+tpD3hlQGoL+0h74BHSKh+L23Q==} + + '@unocss/transformer-directives@0.65.4': + resolution: {integrity: sha512-zkoDEwzPkgXi6ohW7P11gbArwfTRMZ9knYSUYoPEltQz+UZYzeRQ85exiAmdz5MsbCAuhQEr577Kd/CWfhjEuA==} + + '@unocss/transformer-variant-group@0.65.4': + resolution: {integrity: sha512-ggO6xMGeOeoD5GHS2xXBJrYFuzqyiZ25tM0zHAMJn9QU9GIu1NwWvcXluvLCF/MRIygBJGPpAE98aEICI6ifEA==} + + '@unocss/vite@0.65.4': + resolution: {integrity: sha512-02pRcVLfb5UUxMJwudnjS/0ZQdSlskjuXVHdpZpLBZCA8hhoru2uEOsPbUOBRNNMjDj6ld00pmgk/+im07M35Q==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 + + '@uppy/companion-client@2.2.2': + resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==} + + '@uppy/core@2.3.4': + resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==} + + '@uppy/store-default@2.1.1': + resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==} + + '@uppy/utils@4.1.3': + resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==} + + '@uppy/xhr-upload@2.1.3': + resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==} + peerDependencies: + '@uppy/core': ^2.3.3 + + '@vitejs/plugin-legacy@6.0.0': + resolution: {integrity: sha512-pWt9cWaGJAKYw+67VLpN8hSP+G+yAQnrf5Pqh/NzSDKFl/4KpxTtwb5OLQezHoZOxghahO/ha3IpvblBbX/t6A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + peerDependencies: + terser: ^5.16.0 + vite: ^6.0.0 + + '@vitejs/plugin-vue-jsx@4.1.1': + resolution: {integrity: sha512-uMJqv/7u1zz/9NbWAD3XdjaY20tKTf17XVfQ9zq4wY1BjsB/PjpJPMe2xiG39QpP4ZdhYNhm4Hvo66uJrykNLA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.0.0 + + '@vitejs/plugin-vue@5.2.1': + resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.11': + resolution: {integrity: sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==} + + '@volar/source-map@2.4.11': + resolution: {integrity: sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==} + + '@volar/typescript@2.4.11': + resolution: {integrity: sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==} + + '@vue/babel-helper-vue-transform-on@1.2.5': + resolution: {integrity: sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==} + + '@vue/babel-plugin-jsx@1.2.5': + resolution: {integrity: sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.2.5': + resolution: {integrity: sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vueuse/core@12.3.0': + resolution: {integrity: sha512-cnV8QDKZrsyKC7tWjPbeEUz2cD9sa9faxF2YkR8QqNwfofgbOhmfIgvSYmkp+ttSvfOw4E6hLcQx15mRPr0yBA==} + + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + + '@vueuse/metadata@12.3.0': + resolution: {integrity: sha512-M/iQHHjMffOv2npsw2ihlUx1CTiBwPEgb7DzByLq7zpg1+Ke8r7s9p5ybUWc5OIeGewtpY4Xy0R2cKqFqM8hFg==} + + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + + '@vueuse/shared@12.3.0': + resolution: {integrity: sha512-X3YD35GUeW0d5Gajcwv9jdLAJTV2Jdb/Ll6Ii2JIYcKLYZqv5wxyLeKtiQkqWmHg3v0J0ZWjDUMVOw2E7RCXfA==} + + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + + '@wangeditor/basic-modules@1.1.7': + resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.throttle: ^4.1.1 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/code-highlight@1.0.3': + resolution: {integrity: sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/core@1.1.19': + resolution: {integrity: sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==} + peerDependencies: + '@uppy/core': ^2.1.1 + '@uppy/xhr-upload': ^2.0.3 + dom7: ^3.0.0 + is-hotkey: ^0.2.0 + lodash.camelcase: ^4.3.0 + lodash.clonedeep: ^4.5.0 + lodash.debounce: ^4.0.8 + lodash.foreach: ^4.5.0 + lodash.isequal: ^4.5.0 + lodash.throttle: ^4.1.1 + lodash.toarray: ^4.4.0 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/editor-for-vue@5.1.12': + resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==} + peerDependencies: + '@wangeditor/editor': '>=5.1.0' + vue: ^3.0.5 + + '@wangeditor/editor@5.1.23': + resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==} + + '@wangeditor/list-module@1.0.5': + resolution: {integrity: sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/table-module@1.1.4': + resolution: {integrity: sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.isequal: ^4.5.0 + lodash.throttle: ^4.1.1 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/upload-image-module@1.0.2': + resolution: {integrity: sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==} + peerDependencies: + '@uppy/core': ^2.0.3 + '@uppy/xhr-upload': ^2.0.3 + '@wangeditor/basic-modules': 1.x + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.foreach: ^4.5.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/video-module@1.1.4': + resolution: {integrity: sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==} + peerDependencies: + '@uppy/core': ^2.1.4 + '@uppy/xhr-upload': ^2.0.7 + '@wangeditor/core': 1.x + dom7: ^3.0.0 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@zxcvbn-ts/core@3.0.4': + resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==} + + JSONStream@1.3.5: + resolution: {integrity: sha1-MgjB8I06TZkmGrZPkjArwV4RHKA=} + hasBin: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + aggregate-error@4.0.1: + resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} + engines: {node: '>=12'} + + ajv@6.12.6: + resolution: {integrity: sha1-uvWmLoArB9l3A0WG+MO69a3ybfQ=} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + + animate.css@4.1.1: + resolution: {integrity: sha1-YU7FqBEx1+TcNipYFD90BqvWgHU=} + + ansi-escapes@4.3.2: + resolution: {integrity: sha1-ayKR0dt9mLZSHV8e+kLQ86n+tl4=} + engines: {node: '>=8'} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-regex@2.1.1: + resolution: {integrity: sha1-w7M6te42DYbg5ijwRorn7yfWVN8=} + engines: {node: '>=0.10.0'} + + ansi-regex@5.0.1: + resolution: {integrity: sha1-CCyyyJyf6GWaMRpTvWpNxTAdswQ=} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@2.2.1: + resolution: {integrity: sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=} + engines: {node: '>=0.10.0'} + + ansi-styles@4.3.0: + resolution: {integrity: sha1-7dgDYornHATIWuegkG7a00tkiTc=} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha1-JG9Q88p4oyQPbJl+ipvR6sSeSzg=} + + arr-diff@4.0.0: + resolution: {integrity: sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=} + engines: {node: '>=0.10.0'} + + arr-flatten@1.1.0: + resolution: {integrity: sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=} + engines: {node: '>=0.10.0'} + + arr-union@3.1.0: + resolution: {integrity: sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=} + engines: {node: '>=0.10.0'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-each@1.0.1: + resolution: {integrity: sha1-p5SvDAWrF1KEbudTofIRoFugxE8=} + engines: {node: '>=0.10.0'} + + array-ify@1.0.0: + resolution: {integrity: sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=} + + array-slice@1.1.0: + resolution: {integrity: sha1-42jqFfibxwaff/uJrsOmx9SsItQ=} + engines: {node: '>=0.10.0'} + + array-union@2.1.0: + resolution: {integrity: sha1-t5hCCtvrHego2ErNii4j0+/oXo0=} + engines: {node: '>=8'} + + array-unique@0.3.2: + resolution: {integrity: sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=} + engines: {node: '>=0.10.0'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assign-symbols@1.0.0: + resolution: {integrity: sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=} + engines: {node: '>=0.10.0'} + + astral-regex@2.0.0: + resolution: {integrity: sha1-SDFDxWeu7UeFdZwIZXhtx319LjE=} + engines: {node: '>=8'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} + + atob@2.1.2: + resolution: {integrity: sha1-bZUX654DDSQ2ZmZR6GvZ9vE1M8k=} + engines: {node: '>= 4.5.0'} + hasBin: true + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@0.26.1: + resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} + + axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + + babel-plugin-polyfill-corejs2@0.4.12: + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.10.6: + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.3: + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=} + + balanced-match@2.0.0: + resolution: {integrity: sha1-3HD5INeNuLhYU1eVhnv0j4IGM9k=} + + base64-js@1.5.1: + resolution: {integrity: sha1-GxtEAWClv3rUC2UPCVljSBkDkwo=} + + base@0.11.2: + resolution: {integrity: sha1-e95c7RRbbVUakNuH+DxVi060io8=} + engines: {node: '>=0.10.0'} + + big.js@5.2.2: + resolution: {integrity: sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg=} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha1-RRU1JkGCvsL7vIOmKrmM8R2fezo=} + + bluebird@3.7.2: + resolution: {integrity: sha1-nyKcFb4nJFT/qXOs4NvueaGww28=} + + boolbase@1.0.0: + resolution: {integrity: sha1-aN/1++YMUes3cl6p4+0xDcwed24=} + + brace-expansion@1.1.11: + resolution: {integrity: sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=} + + brace-expansion@2.0.1: + resolution: {integrity: sha1-HtxFng8MVISG7Pn8mfIiE2S5oK4=} + + braces@2.3.2: + resolution: {integrity: sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=} + engines: {node: '>=0.10.0'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist-to-esbuild@2.1.1: + resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + browserslist: '*' + + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha1-KxRqb9cugLT1XSVfNe1Zo6mkG9U=} + + buffer@5.7.1: + resolution: {integrity: sha1-umLnwTEzBTWCGXFghRqPZI6Z7tA=} + + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + c12@2.0.1: + resolution: {integrity: sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cache-base@1.0.1: + resolution: {integrity: sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=} + engines: {node: '>=0.10.0'} + + call-bind-apply-helpers@1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M=} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha1-lygHKpVPgFIoIlpt7qazhGHhvVo=} + + camelcase@5.3.1: + resolution: {integrity: sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001690: + resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==} + + capital-case@1.0.4: + resolution: {integrity: sha1-nRMCkjU8kkn2sA+lhSvuOKcX5mk=} + + chalk@1.1.3: + resolution: {integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=} + engines: {node: '>=0.10.0'} + + chalk@4.1.2: + resolution: {integrity: sha1-qsTit3NKdAhnrrFr8CqtVWoeegE=} + engines: {node: '>=10'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + change-case@4.1.2: + resolution: {integrity: sha1-/t/F8TYEXiOYwEEO5EH5VwRkHhI=} + + chardet@0.7.0: + resolution: {integrity: sha1-kAlISfCTfy7twkJdDSip5fDLrZ4=} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@2.0.0: + resolution: {integrity: sha1-Fb++U9LqtM9w8YqM1o6+Wzyx3s4=} + engines: {node: '>=10'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + class-utils@0.3.6: + resolution: {integrity: sha1-+TNprouafOAv1B+q0MqDAzGQxGM=} + engines: {node: '>=0.10.0'} + + clean-stack@4.2.0: + resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} + engines: {node: '>=12'} + + cli-cursor@3.1.0: + resolution: {integrity: sha1-JkMFp65JDR0Dvwybp8kl0XU68wc=} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + clipboardy@4.0.0: + resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} + engines: {node: '>=18'} + + cliui@6.0.0: + resolution: {integrity: sha1-UR1wLAxOQcoVbX0OlgIfI+EyJbE=} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4=} + engines: {node: '>=0.8'} + + clone@2.1.2: + resolution: {integrity: sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=} + engines: {node: '>=0.8'} + + collection-visit@1.0.0: + resolution: {integrity: sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=} + engines: {node: '>=0.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha1-w9RaizT9cwYxoRCoolIGgrMdWn8=} + engines: {node: '>= 0.8'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@13.0.0: + resolution: {integrity: sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=} + + commander@7.2.0: + resolution: {integrity: sha1-o2y1fQtQHOEI5NIFWaFQo5HZerc=} + engines: {node: '>= 10'} + + compare-func@2.0.0: + resolution: {integrity: sha1-+2XnXtvd/S5WhVTotbBf/3pR/LM=} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + compute-scroll-into-view@1.0.20: + resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} + + concat-map@0.0.1: + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + connect@3.7.0: + resolution: {integrity: sha1-XUk0iRDKpeB6AYALAw0MNfIEhPg=} + engines: {node: '>= 0.10.0'} + + consola@3.3.3: + resolution: {integrity: sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg==} + engines: {node: ^14.18.0 || >=16.10.0} + + console@0.7.2: + resolution: {integrity: sha1-+aQzEkkpFZG3v5v/qOIFNW8gqfA=} + + constant-case@3.0.4: + resolution: {integrity: sha1-O4Sprq9M8x7EXmv13pG9+wWJ+vE=} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@2.0.6: + resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} + + copy-descriptor@0.1.1: + resolution: {integrity: sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=} + engines: {node: '>=0.10.0'} + + core-js-compat@3.40.0: + resolution: {integrity: sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==} + + core-js@3.40.0: + resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} + + cors@2.8.5: + resolution: {integrity: sha1-6sEdpRWS3Ya58G9uesKTs9+HXSk=} + engines: {node: '>= 0.10'} + + cosmiconfig-typescript-loader@6.1.0: + resolution: {integrity: sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cropperjs@1.6.2: + resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained. + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-tree@1.1.3: + resolution: {integrity: sha1-60hw+2/XcHMn7JXC/yqwm16NuR0=} + engines: {node: '>=8.0.0'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha1-N3QZGZA7hoVl4cCep0dEXNGJg+4=} + engines: {node: '>=4'} + hasBin: true + + csso@4.2.0: + resolution: {integrity: sha1-6jpWE0bo3J9UbW/r7dUBh884lSk=} + engines: {node: '>=8.0.0'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + danmu.js@1.1.13: + resolution: {integrity: sha512-knFd0/cB2HA4FFWiA7eB2suc5vCvoHdqio33FyyCSfP7C+1A+zQcTvnvwfxaZhrxsGj4qaQI2I8XiTqedRaVmg==} + + dargs@8.1.0: + resolution: {integrity: sha1-o0hZ6lCcvORUheWqNW/vcL/McnI=} + engines: {node: '>=12'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + de-indent@1.0.2: + resolution: {integrity: sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=} + + debug@2.6.9: + resolution: {integrity: sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=} + engines: {node: '>=0.10.0'} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deep-is@0.1.4: + resolution: {integrity: sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=} + + deep-pick-omit@1.2.1: + resolution: {integrity: sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha1-P3rkIRKbyqrJvHSQXJigAJ7J7n8=} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + define-property@0.2.5: + resolution: {integrity: sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=} + engines: {node: '>=0.10.0'} + + define-property@1.0.0: + resolution: {integrity: sha1-dp66rz9KY6rTr56NMEybvnm/sOY=} + engines: {node: '>=0.10.0'} + + define-property@2.0.2: + resolution: {integrity: sha1-1Flono1lS6d+AqgX+HENcCyxbp0=} + engines: {node: '>=0.10.0'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + del@7.1.0: + resolution: {integrity: sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==} + engines: {node: '>=14.16'} + + delayed-stream@1.0.0: + resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=} + engines: {node: '>=0.4.0'} + + delegate@3.2.0: + resolution: {integrity: sha1-tmtxwxWFIuirV0T3INjKDCr1kWY=} + + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + + detect-file@1.0.0: + resolution: {integrity: sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=} + engines: {node: '>=0.10.0'} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dir-glob@3.0.1: + resolution: {integrity: sha1-Vtv3PZkqSpO6FYT0U0Bj/S5BcX8=} + engines: {node: '>=8'} + + dom-serializer@0.2.2: + resolution: {integrity: sha1-GvuB9TNxcXXUeGVd68XjMtn5u1E=} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + dom7@3.0.0: + resolution: {integrity: sha1-uGHOXWemvs16qjrQKUL/FLEkAzE=} + + domelementtype@1.3.1: + resolution: {integrity: sha1-0EjESzew0Qp/Kj1f7j9DM9eQSB8=} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@2.4.2: + resolution: {integrity: sha1-iAUJfpM9ZehVRvcm1g9euItE+AM=} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@1.7.0: + resolution: {integrity: sha1-Vuo0HoNOBuZ0ivehyyXaZ+qfjCo=} + + domutils@2.8.0: + resolution: {integrity: sha1-RDfe9dtuLR9dbuhZvZXKfQIEgTU=} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-case@3.0.4: + resolution: {integrity: sha1-mytnDQCkMWZ6inW6Kc0bmICc51E=} + + dot-prop@5.3.0: + resolution: {integrity: sha1-kMzOcIzZzYLMTcjD3dmr3VWyDog=} + engines: {node: '>=8'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + downloadjs@1.4.7: + resolution: {integrity: sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw=} + + driver.js@1.3.1: + resolution: {integrity: sha512-MvUdXbqSgEsgS/H9KyWb5Rxy0aE6BhOVT4cssi2x2XjmXea6qQfgdx32XKVLLSqTaIw7q/uxU5Xl3NV7+cN6FQ==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha1-Or5DrvODX4rgd9E23c4PJ2sEAOY=} + + eastasianwidth@0.2.0: + resolution: {integrity: sha1-aWzi7Aqg5uqTo5f/zySqeEDIJ8s=} + + echarts-wordcloud@2.1.0: + resolution: {integrity: sha512-Kt1JmbcROgb+3IMI48KZECK2AP5lG6bSsOEs+AsuwaWJxQom31RTNd6NFYI01E/YaI1PFZeueaupjlmzSQasjQ==} + peerDependencies: + echarts: ^5.0.1 + + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + + ee-first@1.1.1: + resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.79: + resolution: {integrity: sha512-nYOxJNxQ9Om4EC88BE4pPoNI8xwSFf8pU/BAeOl4Hh/b/i6V4biTAzwV7pXi3ARKeoYO5JZKMIXTryXSVer5RA==} + + element-plus@2.9.2: + resolution: {integrity: sha512-HS+Cc5mmy70DixJuoN3cMxPPoNWXkjHzUw2PcGmysk6NHQzzUtwi2Vc+dlmbmRxj3eNqgC1xpPQV5Nf9uDtQRg==} + peerDependencies: + vue: ^3.2.0 + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha1-6Bj9ac5cz8tARZT4QpY79TFkzDc=} + + emoji-regex@9.2.2: + resolution: {integrity: sha1-hAyIA7DYBH9P8M+WMXazLU7z7XI=} + + emojis-list@3.0.0: + resolution: {integrity: sha1-VXBmIEatKeLpFucariYKvf9Pang=} + engines: {node: '>= 4'} + + encodeurl@1.0.2: + resolution: {integrity: sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=} + engines: {node: '>= 0.8'} + + entities@1.1.2: + resolution: {integrity: sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=} + + entities@2.2.0: + resolution: {integrity: sha1-CY3JDruD2N/6CJ1VJWs1HTTE2lU=} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha1-QgOZ1BbOH76bwKB8Yvpo1n/Q+PI=} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + error-ex@1.3.2: + resolution: {integrity: sha1-tKxAZIEH/c3PriQvQovqihTU8b8=} + + es-abstract@1.23.9: + resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@0.9.3: + resolution: {integrity: sha1-bxPbAMw4QXE32vdDZvU1yOtDjxk=} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha1-p96IkUGgWpSwhUQDstCg+/qY87c=} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + esbuild@0.11.3: + resolution: {integrity: sha1-tXFluQe+T/umUfZFBTjOjYwdXrA=} + hasBin: true + + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha1-RoMSa1ALYXYvLb66zhgG6L4xscg=} + engines: {node: '>=12'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-define-config@2.1.0: + resolution: {integrity: sha512-QUp6pM9pjKEVannNAbSJNeRuYwW3LshejfyBBpjeMGaJjaDUpVps4C6KVR8R7dWZnD3i0synmrE36znjTkJvdQ==} + engines: {node: '>=18.0.0', npm: '>=9.0.0', pnpm: '>=8.6.0'} + + eslint-plugin-prettier@5.2.1: + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@9.32.0: + resolution: {integrity: sha512-b/Y05HYmnB/32wqVcjxjHZzNpwxj1onBOvqW89W+V+XNG1dRuaFbNd3vT9CLbr2LXjEoq+3vn8DanWf7XU22Ug==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.17.0: + resolution: {integrity: sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + esno@4.8.0: + resolution: {integrity: sha512-acMtooReAQGzLU0zcuEDHa8S62meh5aIyi8jboYxyvAePdmuWx2Mpwmt0xjwO0bs9/SXf+dvXJ0QJoDWw814Iw==} + hasBin: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha1-eteWTWeauyi+5yzsY3WLHF0smSE=} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha1-LupSkHAvJquP5TcDcP+GyWXSESM=} + engines: {node: '>=4.0'} + + estree-walker@1.0.1: + resolution: {integrity: sha1-MbxdYSyWtwQQa0d+bdXYqhOMtwA=} + + estree-walker@2.0.2: + resolution: {integrity: sha1-UvAQF4wqTBF6d1fP6UKtt9LaTKw=} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=} + engines: {node: '>= 0.6'} + + event-emitter@0.3.5: + resolution: {integrity: sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=} + + eventemitter3@4.0.7: + resolution: {integrity: sha1-Lem2j2Uo1WRO9cWVJqG0oHMGFp8=} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + expand-brackets@2.1.4: + resolution: {integrity: sha1-t3c14xXOMPa27/D4OwQVGiJEliI=} + engines: {node: '>=0.10.0'} + + expand-tilde@2.0.2: + resolution: {integrity: sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=} + engines: {node: '>=0.10.0'} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + extend-shallow@2.0.1: + resolution: {integrity: sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=} + + external-editor@3.1.0: + resolution: {integrity: sha1-ywP3QL764D6k0oPK7SdBqD8zVJU=} + engines: {node: '>=4'} + + extglob@2.0.4: + resolution: {integrity: sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=} + + fast-uri@3.0.5: + resolution: {integrity: sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.18.0: + resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-entry-cache@9.1.0: + resolution: {integrity: sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==} + engines: {node: '>=18'} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@4.0.0: + resolution: {integrity: sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=} + engines: {node: '>=0.10.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.1.2: + resolution: {integrity: sha1-t+fQAP/RGTjQ/bBTUG9uur6fWH0=} + engines: {node: '>= 0.8'} + + find-up@4.1.0: + resolution: {integrity: sha1-l6/n1s3AvFkoWEt8jXsW6KmqXRk=} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha1-TJKBnstwg1YeT0okCoa+UZj1Nvw=} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + findup-sync@5.0.0: + resolution: {integrity: sha1-VDgK2WWn7coAzI9jETVZqtxUG9I=} + engines: {node: '>= 10.13.0'} + + fined@2.0.0: + resolution: {integrity: sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==} + engines: {node: '>= 10.13.0'} + + flagged-respawn@2.0.0: + resolution: {integrity: sha1-q/OXGdz+GsBshslGYIHFQcaCmHs=} + engines: {node: '>= 10.13.0'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat-cache@5.0.0: + resolution: {integrity: sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==} + engines: {node: '>=18'} + + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.3: + resolution: {integrity: sha1-abRH6IoKXTLD5whPPxcQA0shN24=} + + for-in@1.0.2: + resolution: {integrity: sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=} + engines: {node: '>=0.10.0'} + + for-own@1.0.0: + resolution: {integrity: sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=} + engines: {node: '>=0.10.0'} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fragment-cache@0.2.1: + resolution: {integrity: sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=} + engines: {node: '>=0.10.0'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + + fs-minipass@2.1.0: + resolution: {integrity: sha1-f1A2/b8SxjwWkZDL5BmchSJx+fs=} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha1-MqbudsPX9S1GsrGuXZP+qFgKJeA=} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-intrinsic@1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + get-value@2.0.6: + resolution: {integrity: sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=} + engines: {node: '>=0.10.0'} + + giget@1.2.3: + resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} + hasBin: true + + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha1-hpgyxYA0/mikCTwX3BXoNA2EAcQ=} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha1-bSN9mQg5UMeSkPJMdkKj3poo+eM=} + engines: {node: '>=10.13.0'} + + glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + global-modules@1.0.0: + resolution: {integrity: sha1-bXcPDrUjrHgWTXK15xqIdyZcw+o=} + engines: {node: '>=0.10.0'} + + global-modules@2.0.0: + resolution: {integrity: sha1-mXYFrSNF8n9RU5vqJldEISFcd4A=} + engines: {node: '>=6'} + + global-prefix@1.0.2: + resolution: {integrity: sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=} + engines: {node: '>=0.10.0'} + + global-prefix@3.0.0: + resolution: {integrity: sha1-/IX3MGTfafUEIfR/iD/luRO6m5c=} + engines: {node: '>=6'} + + globals@11.12.0: + resolution: {integrity: sha1-q4eVM4hooLq9hSV1gBjCp+uVxC4=} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.14.0: + resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} + + globjoin@0.1.4: + resolution: {integrity: sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + gzip-size@6.0.0: + resolution: {integrity: sha1-BlNn/VDCOcBnHLy61b4+LusQ5GI=} + engines: {node: '>=10'} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-ansi@2.0.0: + resolution: {integrity: sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=} + engines: {node: '>=0.10.0'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@1.0.0: + resolution: {integrity: sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=} + engines: {node: '>=0.10.0'} + + has-flag@4.0.0: + resolution: {integrity: sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-value@0.3.1: + resolution: {integrity: sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=} + engines: {node: '>=0.10.0'} + + has-value@1.0.0: + resolution: {integrity: sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=} + engines: {node: '>=0.10.0'} + + has-values@0.1.4: + resolution: {integrity: sha1-bWHeldkd/Km5oCCJrThL/49it3E=} + engines: {node: '>=0.10.0'} + + has-values@1.0.0: + resolution: {integrity: sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=} + engines: {node: '>=0.10.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha1-hK5l+n6vsWX922FWauFLrwVmTw8=} + hasBin: true + + header-case@2.0.4: + resolution: {integrity: sha1-WkLmO1UXc0nPQFvrjXdayruSwGM=} + + homedir-polyfill@1.0.3: + resolution: {integrity: sha1-dDKYzvTlrz4ZQWH7rcwhUdOgWOg=} + engines: {node: '>=0.10.0'} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-void-elements@2.0.1: + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + + htmlparser2@3.10.1: + resolution: {integrity: sha1-vWedw/WYl7ajS7EHSchVu1OpOS8=} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + i18next@20.6.1: + resolution: {integrity: sha1-U15fbluutoXH0l33DbY788wKo0U=} + + iconv-lite@0.4.24: + resolution: {integrity: sha1-ICK0sl+93CHS9SSXSkdKr+czkIs=} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha1-pS+AvzjaGVLrXGgXkHGYcaGnJQE=} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha1-jrehCmP/8l0VpXsAFYbRd9Gw01I=} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@6.0.2: + resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==} + engines: {node: '>= 4'} + + ignore@7.0.0: + resolution: {integrity: sha512-lcX8PNQygAa22u/0BysEY8VhaFRzlOkvdlKczDPnJvrkJD1EuqzEky5VYYKM2iySIuaVIDv9N190DfSreSLw2A==} + engines: {node: '>= 4'} + + image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + + import-fresh@3.3.0: + resolution: {integrity: sha1-NxYsJfy566oublPVtNiM4X2eDCs=} + engines: {node: '>=6'} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + importx@0.5.1: + resolution: {integrity: sha512-YrRaigAec1sC2CdIJjf/hCH1Wp9Ii8Cq5ROw4k5nJ19FVl2FcJUHZ5gGIb1vs8+JNYIyOJpc2fcufS2330bxDw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=} + engines: {node: '>=0.8.19'} + + indent-string@5.0.0: + resolution: {integrity: sha1-T9KYD8yvhiLRTGTWlPTPM8gZUaU=} + engines: {node: '>=12'} + + inflight@1.0.6: + resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} + + inherits@2.0.4: + resolution: {integrity: sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=} + + ini@1.3.8: + resolution: {integrity: sha1-op2kJbSIBvNHZ6Tvzjlyaa8oQyw=} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + inquirer@12.3.0: + resolution: {integrity: sha512-3NixUXq+hM8ezj2wc7wC37b32/rHq1MwNZDYdvx+d6jokOD+r+i8Q4Pkylh9tISYP114A128LCX8RKhopC5RfQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + inquirer@9.3.7: + resolution: {integrity: sha512-LJKFHCSeIRq9hanN14IlOtPSTe3lNES7TYDTE2xxdAy1LS5rYphajK1qtwvj3YmQXvvk0U2Vbmcni8P9EIQW9w==} + engines: {node: '>=18'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + + is-absolute@1.0.0: + resolution: {integrity: sha1-OV4a6EsR8mrReV5zwXN45IowFXY=} + engines: {node: '>=0.10.0'} + + is-accessor-descriptor@1.0.1: + resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} + engines: {node: '>= 0.10'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=} + + is-async-function@2.1.0: + resolution: {integrity: sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha1-6h9/O4DwZCNug0cPhsCcJU+0Wwk=} + engines: {node: '>=8'} + + is-boolean-object@1.2.1: + resolution: {integrity: sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==} + engines: {node: '>= 0.4'} + + is-buffer@1.1.6: + resolution: {integrity: sha1-76ouqdqg16suoTqXsritUf776L4=} + + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-descriptor@0.1.7: + resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.3: + resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha1-M+6r4jz+hvFL3kQIoCwM+4U6zao=} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha1-kAk6oxBid9inelkQ265xdH4VogA=} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha1-8Rb4Bk/pCz94RKOJl8C3UFEmnx0=} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha1-+uMWfHKedGP4RhzlErCApJJoqog=} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=} + engines: {node: '>=0.10.0'} + + is-hotkey@0.2.0: + resolution: {integrity: sha1-GDWmgXGpHlyUYIadljNpR8g0DO8=} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@1.0.0: + resolution: {integrity: sha1-zqbmrlyHCnsKAAQHC3tYfgJSkS4=} + engines: {node: '>=8'} + + is-interactive@2.0.0: + resolution: {integrity: sha1-QMV2FFk4JtoRAK3mBZd41ZfxbpA=} + engines: {node: '>=12'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@3.0.0: + resolution: {integrity: sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha1-Rz+wXZc3BeP9liBUUBjKjiLvSYI=} + engines: {node: '>=8'} + + is-path-cwd@3.0.0: + resolution: {integrity: sha1-iJtB5VyFiLHrKpamHQV0CmdFIcc=} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-path-inside@4.0.0: + resolution: {integrity: sha1-gFrrYsR8GxL8P9E7+z7R50MAcds=} + engines: {node: '>=12'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha1-caUMhCnfync8kqOQpKA7OfzVHT4=} + engines: {node: '>=0.10.0'} + + is-plain-object@2.0.4: + resolution: {integrity: sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=} + engines: {node: '>=0.10.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha1-RCf1CrNCnpAl6n1S6QQ6nvQVk0Q=} + engines: {node: '>=0.10.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-relative@1.0.0: + resolution: {integrity: sha1-obtpNc6MXboei5dUubLcwCDiJg0=} + engines: {node: '>=0.10.0'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@3.0.0: + resolution: {integrity: sha1-5r/XqmvvafT0cs6btoHj5XtDGaw=} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-text-path@2.0.0: + resolution: {integrity: sha1-skhOK3IKYz/rLoW2fcGT/3LHVjY=} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-unc-path@1.0.0: + resolution: {integrity: sha1-1zHoiY7QkKEsNSrS6u1Qla0yLJ0=} + engines: {node: '>=0.10.0'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha1-PybHaoCVk7Ur+i7LVxDtJ3m1Iqc=} + engines: {node: '>=10'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-url@1.2.4: + resolution: {integrity: sha1-BKTfRtKMTP89c9Af8Gq+sxihqlI=} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.0: + resolution: {integrity: sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-what@3.14.1: + resolution: {integrity: sha1-4SIvRt3ahd6tD9HJ3xMXYOd3VcE=} + + is-windows@1.0.2: + resolution: {integrity: sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha1-dKTHbnfKn9P5MvKQwX6jJs0VcnE=} + engines: {node: '>=8'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + is64bit@2.0.0: + resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} + engines: {node: '>=18'} + + isarray@1.0.0: + resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=} + + isarray@2.0.5: + resolution: {integrity: sha1-ivHkwSISRMxiRZ+vOJQNTmRKVyM=} + + isbinaryfile@5.0.4: + resolution: {integrity: sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==} + engines: {node: '>= 18.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} + + isobject@2.1.0: + resolution: {integrity: sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=} + engines: {node: '>=0.10.0'} + + isobject@3.0.1: + resolution: {integrity: sha1-TkMekrEalzFjaqH5yNHMvP2reN8=} + engines: {node: '>=0.10.0'} + + jackspeak@4.0.2: + resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} + engines: {node: 20 || >=22} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-base64@2.6.4: + resolution: {integrity: sha1-9OaGxd4eofhn28rT1G2WlCjfmMQ=} + + js-tokens@4.0.0: + resolution: {integrity: sha1-GSA/tZmR35jjoocFDUZHzerzJJk=} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha1-wftl+PUBeQHN0slRhkuhhFihBgI=} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha1-u4sJpll7pCZCXy5KByRcPQC5ND4=} + engines: {node: '>=6'} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha1-fEeAWpQxmSjgV3dAXcEuH3pO4C0=} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha1-afaofZUTq4u4/mO9sJecRI5oRmA=} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha1-rnvLNlard6c7pcSb9lTzjmtoYOI=} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-eslint-parser@2.4.0: + resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + jsonfile@6.1.0: + resolution: {integrity: sha1-vFWyY0eTxnnsZAMJTrE2mKbsCq4=} + + jsonparse@1.3.1: + resolution: {integrity: sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=} + engines: {'0': node >= 0.2.0} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@3.2.2: + resolution: {integrity: sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=} + engines: {node: '>=0.10.0'} + + kind-of@4.0.0: + resolution: {integrity: sha1-IIE989cSkosgc3hpGkUGb65y3Vc=} + engines: {node: '>=0.10.0'} + + kind-of@5.1.0: + resolution: {integrity: sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=} + engines: {node: '>=0.10.0'} + + kind-of@6.0.3: + resolution: {integrity: sha1-B8BQNKbDSfoG4k+jWqdttFgM5N0=} + engines: {node: '>=0.10.0'} + + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + + knitwork@1.2.0: + resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==} + + known-css-properties@0.35.0: + resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + less@4.2.1: + resolution: {integrity: sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==} + engines: {node: '>=6'} + hasBin: true + + levn@0.4.1: + resolution: {integrity: sha1-rkViwAdHO5MqYgDUAyaN0v/8at4=} + engines: {node: '>= 0.8.0'} + + liftoff@4.0.0: + resolution: {integrity: sha512-rMGwYF8q7g2XhG2ulBmmJgWv25qBsqRbDn5gH0+wnuyeFt7QBJlHJmtg5qEdn4pN6WVAUMgXnIxytMFRX9c1aA==} + engines: {node: '>=10.13.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha1-7KKE910pZQeTCdwK2SVauy68FjI=} + + lint-staged@15.3.0: + resolution: {integrity: sha512-vHFahytLoF2enJklgtOtCtIjZrKD/LoxlaUusd5nh7dWv/dkKQJY74ndFSzxCdv7g0ueGg1ORgTSt4Y9LPZn9A==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.2.5: + resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} + engines: {node: '>=18.0.0'} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + loader-utils@1.4.2: + resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} + engines: {node: '>=4.0.0'} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + locate-path@5.0.0: + resolution: {integrity: sha1-Gvujlq/WdqbUJQTQpno6frn2KqA=} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha1-VTIeswn+u8WcSAHZMackUqaB0oY=} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.21: + resolution: {integrity: sha1-Q+YmxG5lkbd1C+srUBFzkMYJ4+4=} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.camelcase@4.3.0: + resolution: {integrity: sha1-soqmKIorn8ZRA1x3EfZathkDMaY=} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=} + + lodash.debounce@4.0.8: + resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=} + + lodash.foreach@4.5.0: + resolution: {integrity: sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=} + + lodash.get@4.4.2: + resolution: {integrity: sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=} + + lodash.isequal@4.5.0: + resolution: {integrity: sha1-QVxEePK8wwEgwizhDtMib30+GOA=} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha1-hImxyw0p/4gZXM7KRI/21swpXDY=} + + lodash.merge@4.6.2: + resolution: {integrity: sha1-VYqlO0O2YeGSWgr9+japoQhf5Xo=} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha1-YXEh+JrFX1kEfHrsHM1mVMZZD1U=} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=} + + lodash.startcase@4.4.0: + resolution: {integrity: sha1-lDbjTtJgk+1/+uGTYUQ1CRXZrdg=} + + lodash.throttle@4.1.1: + resolution: {integrity: sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=} + + lodash.toarray@4.4.0: + resolution: {integrity: sha1-JMS/zWsvuji/0FlNsRedjptlZWE=} + + lodash.truncate@4.4.2: + resolution: {integrity: sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=} + + lodash.uniq@4.5.0: + resolution: {integrity: sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=} + + lodash@4.17.21: + resolution: {integrity: sha1-Z5WRxWTDv/quhFTPCz3zcMPWkRw=} + + log-symbols@4.1.0: + resolution: {integrity: sha1-P727lbRoOsn8eFER55LlWNSr1QM=} + engines: {node: '>=10'} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + lower-case@2.0.2: + resolution: {integrity: sha1-b6I3xj29xKgsoP2ILkci3F5jTig=} + + lru-cache@11.0.2: + resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha1-HaJ+ZxAnGUdpXa9oSOhH8B2EuSA=} + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-iterator@1.0.1: + resolution: {integrity: sha1-KbM/MSqo9UfEpeSQ9Wr87JkTOtY=} + engines: {node: '>=0.10.0'} + + map-cache@0.2.2: + resolution: {integrity: sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=} + engines: {node: '>=0.10.0'} + + map-visit@1.0.0: + resolution: {integrity: sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=} + engines: {node: '>=0.10.0'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha1-TdrdZzCOeAzxakdoWHjuJ7c2oKM=} + + mdn-data@2.0.14: + resolution: {integrity: sha1-cRP8QoGRfWPOKbQ0RvcB5owlulA=} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + memoize-one@6.0.0: + resolution: {integrity: sha1-slkbhx7YKUiu5HJ9xqvO7qyMEEU=} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-options@1.0.1: + resolution: {integrity: sha1-KmSyRFe+zU5NxggoMkfpTOWJqjI=} + engines: {node: '>=4'} + + merge-stream@2.0.0: + resolution: {integrity: sha1-UoI2KaFN0AyXcPtq1H3GMQ8sH2A=} + + merge2@1.4.1: + resolution: {integrity: sha1-Q2iJL4hekHRVpv19xVwMnUBJkK4=} + engines: {node: '>= 8'} + + micromatch@3.1.0: + resolution: {integrity: sha1-UQLU6vILaZfWAI46z+HESj+oFeI=} + engines: {node: '>=0.10.0'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-match@1.0.2: + resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha1-ftLCzMyvhNP/y3pptXcR/CCDQBs=} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha1-YKkFUNXLCyOcymXYk7GlOymHHsw=} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha1-6Q00Zrogm5MkUVCKEc49NjIUWTE=} + engines: {node: '>= 8'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mixin-deep@1.3.2: + resolution: {integrity: sha1-ESC0PcNZp4Xc5ltVuC4lfM9HlWY=} + engines: {node: '>=0.10.0'} + + mkdirp@1.0.4: + resolution: {integrity: sha1-PrXtYmInVteaXw4qIh3+utdcL34=} + engines: {node: '>=10'} + hasBin: true + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.7.3: + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + + mockjs@1.1.0: + resolution: {integrity: sha1-5qDDeOkZBtuv8gkRzAJzs8fXWwY=} + hasBin: true + + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + ms@2.0.0: + resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=} + + ms@2.1.3: + resolution: {integrity: sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + namespace-emitter@2.0.1: + resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==} + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanomatch@1.2.13: + resolution: {integrity: sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=} + engines: {node: '>=0.10.0'} + + natural-compare@1.4.0: + resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=} + + needle@3.3.1: + resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} + engines: {node: '>= 4.4.x'} + hasBin: true + + neo-async@2.6.2: + resolution: {integrity: sha1-tKr7k+OustgXTKU88WOrfXMIMF8=} + + next-tick@1.1.0: + resolution: {integrity: sha1-GDbuMK1W1n7ygbIr0Zn3CUSbNes=} + + no-case@3.0.4: + resolution: {integrity: sha1-02H9XJgA9VhVGoNp/A3NRmK2Ek0=} + + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-plop@0.32.0: + resolution: {integrity: sha512-lKFSRSRuDHhwDKMUobdsvaWCbbDRbV3jMUSMiajQSQux1aNUevAZVxUHc2JERI//W8ABPRbi3ebYuSuIzkNIpQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=} + engines: {node: '>=0.10.0'} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nprogress@0.2.0: + resolution: {integrity: sha1-y480xTIT2JVyP8urkH6UIq28r7E=} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nypm@0.3.12: + resolution: {integrity: sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=} + engines: {node: '>=0.10.0'} + + object-copy@0.1.0: + resolution: {integrity: sha1-fn2Fi3gb18mRpBupde04EnVOmYw=} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha1-HEfyct8nfzsdrwYWd9nILiMixg4=} + engines: {node: '>= 0.4'} + + object-visit@1.0.1: + resolution: {integrity: sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=} + engines: {node: '>=0.10.0'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.defaults@1.1.0: + resolution: {integrity: sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=} + engines: {node: '>=0.10.0'} + + object.map@1.0.1: + resolution: {integrity: sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=} + engines: {node: '>=0.10.0'} + + object.pick@1.3.0: + resolution: {integrity: sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=} + engines: {node: '>=0.10.0'} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + ohash@1.1.4: + resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} + + on-finished@2.3.0: + resolution: {integrity: sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} + + onetime@5.1.2: + resolution: {integrity: sha1-0Oluu1awdHbfHdnEgG5SN5hcpF4=} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha1-fCTBjtH9LpvKS9JoBqM2E8d9NLQ=} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha1-GyZ4Qmr0rEpQkAjl5KyemVnbnhg=} + engines: {node: '>=10'} + + ora@8.1.1: + resolution: {integrity: sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==} + engines: {node: '>=18'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=} + engines: {node: '>=0.10.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@2.3.0: + resolution: {integrity: sha1-PdM8ZHohT9//2DWTPrCG2g3CHbE=} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha1-4drMvnjQ0TiMoYxk/qOOPlfjcGs=} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha1-kUr2VE7TK/pUZwsGHK/L0EmEtkQ=} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@4.1.0: + resolution: {integrity: sha1-o0KLtwiLOmApL2aRkni3wpetTwc=} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha1-g8gxXGeFAF470CGDlBHJ4RDm2DQ=} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha1-PamknUk0uQEIncozAvpl3FoFwE8=} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@5.5.0: + resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==} + engines: {node: '>=12'} + + p-try@2.2.0: + resolution: {integrity: sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@0.2.8: + resolution: {integrity: sha512-ts9KSdroZisdvKMWVAVCXiKqnqNfXz4+IbrBG8/BWx/TR5le+jfenvoBuIZ6UWM9nz47W7AbD9qYfAwfWMIwzA==} + + param-case@3.0.4: + resolution: {integrity: sha1-fRf+SqEr3jTUp32RrPtiGcqtAcU=} + + parent-module@1.0.1: + resolution: {integrity: sha1-aR0nCeeMefrjoVZiJFLQB2LKqqI=} + engines: {node: '>=6'} + + parse-filepath@1.0.2: + resolution: {integrity: sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=} + engines: {node: '>=0.8'} + + parse-json@5.2.0: + resolution: {integrity: sha1-x2/Gbe5UIxyWKyK8yKcs8vmXU80=} + engines: {node: '>=8'} + + parse-node-version@1.0.1: + resolution: {integrity: sha1-4rXb7eAOf6m8NjYH9TMn6LBzGJs=} + engines: {node: '>= 0.10'} + + parse-passwd@1.0.0: + resolution: {integrity: sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=} + engines: {node: '>=0.10.0'} + + parseurl@1.3.3: + resolution: {integrity: sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha1-tI4O8rmOIF58Ha50fQsVCCN2YOs=} + + pascalcase@0.1.1: + resolution: {integrity: sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=} + engines: {node: '>=0.10.0'} + + path-browserify@1.0.1: + resolution: {integrity: sha1-2YRUqcN1PVeQhg8W9ohnueRr4f0=} + + path-case@3.0.4: + resolution: {integrity: sha1-kWhkUzTrlCZYN1xW+AtMDLX4LG8=} + + path-exists@4.0.0: + resolution: {integrity: sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha1-pqrZSJIAsh+rMeSc8JJ35RFvuec=} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha1-KVWI3DruZBVPh3rbnXgLgcVUvxg=} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha1-+8EUtgykKzDZ2vWFjkvWi77bZzU=} + + path-root-regex@0.1.2: + resolution: {integrity: sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=} + engines: {node: '>=0.10.0'} + + path-root@0.1.1: + resolution: {integrity: sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=} + engines: {node: '>=0.10.0'} + + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-type@4.0.0: + resolution: {integrity: sha1-hO0BwKe6OAr+CdkKjBgNzZ0DBDs=} + engines: {node: '>=8'} + + path-type@5.0.0: + resolution: {integrity: sha1-FLAe166n3fnHw/RhgdTQT5x4W7g=} + engines: {node: '>=12'} + + pathe@0.2.0: + resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.0: + resolution: {integrity: sha512-G7n4uhtk9qJt2hlD+UFfsIGY854wpF+zs2bUbQ3CQEUTcn7v25LRsrmurOxTo4bJgjE4qkyshd9ldsEuY9M6xg==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@4.0.1: + resolution: {integrity: sha1-SyzSXFDVmHNcUCkiJP2MbfQeMjE=} + engines: {node: '>=6'} + + pinia-plugin-persistedstate@4.2.0: + resolution: {integrity: sha512-3buhA7ac+ssbOIx3VRCC8oHkoFwhDM9oHRCjo7nj+O8WUqnW+jRqh7eYT5eS/DNa3H28zp3dYf/nd/Vc8zj8eQ==} + peerDependencies: + '@pinia/nuxt': '>=0.9.0' + pinia: '>=2.3.0' + peerDependenciesMeta: + '@pinia/nuxt': + optional: true + pinia: + optional: true + + pinia@2.3.0: + resolution: {integrity: sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.0: + resolution: {integrity: sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==} + + plop@4.0.1: + resolution: {integrity: sha512-5n8QU93kvL/ObOzBcPAB1siVFtAH1TZM6TntJ3JK5kXT0jIgnQV+j+uaOWWFJlg1cNkzLYm8klgASF65K36q9w==} + engines: {node: '>=18'} + hasBin: true + + pngjs@5.0.0: + resolution: {integrity: sha1-553SshV2f9nARWHAEjbflgvOf7s=} + engines: {node: '>=10.13.0'} + + posix-character-classes@0.1.1: + resolution: {integrity: sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=} + engines: {node: '>=0.10.0'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + postcss-html@1.7.0: + resolution: {integrity: sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==} + engines: {node: ^12 || >=14} + + postcss-less@6.0.0: + resolution: {integrity: sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==} + engines: {node: '>=12'} + peerDependencies: + postcss: ^8.3.5 + + postcss-prefix-selector@1.16.1: + resolution: {integrity: sha512-Umxu+FvKMwlY6TyDzGFoSUnzW+NOfMBLyC1tAkIjgX+Z/qGspJeRjVC903D7mx7TuBpJlwti2ibXtWuA7fKMeQ==} + peerDependencies: + postcss: '>4 <9' + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha1-u0wpiUFxqUvFyZa5owMX70Aq2qE=} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.0.0: + resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==} + engines: {node: '>=4'} + + postcss-sorting@8.0.2: + resolution: {integrity: sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==} + peerDependencies: + postcss: ^8.4.20 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@5.2.18: + resolution: {integrity: sha1-ut+hSX1GJE9jkPWLMZgw2RB4U8U=} + engines: {node: '>=0.12'} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + posthtml-parser@0.2.1: + resolution: {integrity: sha1-NdUw3jhnQMK6JP8usvrznM3ycd0=} + + posthtml-rename-id@1.0.12: + resolution: {integrity: sha1-z39us3FGvxr6wx5o8YxswZrmFDM=} + + posthtml-render@1.4.0: + resolution: {integrity: sha1-QBFAcMRYgcrLkzR9rj7/U6+8/xM=} + engines: {node: '>=10'} + + posthtml-svg-mode@1.0.3: + resolution: {integrity: sha1-q9VU+s6BIjyrDLNn4Y5O/SpOdLA=} + + posthtml@0.9.2: + resolution: {integrity: sha1-9MBtufZ7Yf0XxOJW5+PZUVv3Jv0=} + engines: {node: '>=0.10.0'} + + preact@10.25.4: + resolution: {integrity: sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==} + + prelude-ls@1.2.1: + resolution: {integrity: sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha1-0j1B/hN1ZG3i0BBNNFSjAIgCz3s=} + engines: {node: '>=6.0.0'} + + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + + progress@2.0.3: + resolution: {integrity: sha1-foz42PW48jnBvGi+tOt4Vn1XLvg=} + engines: {node: '>=0.4.0'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha1-4QLxbKNVQkhldV0sno6k8k1Yw+I=} + + prr@1.0.1: + resolution: {integrity: sha1-0/wRS6BplaRexok/SEzrHXj19HY=} + + punycode@1.4.1: + resolution: {integrity: sha1-wNWmOycYgArY4esPpSachN1BhF4=} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.13.1: + resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} + engines: {node: '>=0.6'} + + query-string@4.3.4: + resolution: {integrity: sha1-u7aTucqRXCMlFbIosaArYJBD2+s=} + engines: {node: '>=0.10.0'} + + queue-microtask@1.2.3: + resolution: {integrity: sha1-SSkii7xyTfrEPg77BYyve2z7YkM=} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + rd@2.0.1: + resolution: {integrity: sha1-4YqK9bL3RAwNsVI8oExuD5ZgAD8=} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha1-dKNwvYVxFuJFspzJc0DNQxoCpsc=} + engines: {node: '>=8.10.0'} + + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + + rechoir@0.8.0: + resolution: {integrity: sha1-Sfhm4NMhRhQto62PDv81KzIV/yI=} + engines: {node: '>= 10.13.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha1-uTRtiCfo9aMve6KWN9OYtpAUhIo=} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + + regex-not@1.0.2: + resolution: {integrity: sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=} + engines: {node: '>=0.10.0'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + + repeat-element@1.1.4: + resolution: {integrity: sha1-vmgVIIR6tYx1aKx1+/rSjtQtOek=} + engines: {node: '>=0.10.0'} + + repeat-string@1.6.1: + resolution: {integrity: sha1-jcrkcOHIirwtYA//Sndihtp15jc=} + engines: {node: '>=0.10'} + + require-directory@2.1.1: + resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha1-iaf92TgmEmcxjq/hT5wy5ZjDaQk=} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha1-0LMp7MfMD2Fkn2IhW+aa9UqomJs=} + + resolve-dir@1.0.1: + resolution: {integrity: sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha1-w1IlhD3493bfIcV1V7wIfp39/Gk=} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve-url@0.2.1: + resolution: {integrity: sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha1-OfZ8VLOnpYzqUjbZXPADQjljH34=} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + ret@0.1.15: + resolution: {integrity: sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=} + engines: {node: '>=0.12'} + + reusify@1.0.4: + resolution: {integrity: sha1-kNo4Kx4SbvwCFG6QhFqI2xKSXXY=} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha1-8aVAK6YiCtUswSgrrBrjqkn9Bho=} + hasBin: true + + rimraf@6.0.1: + resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} + engines: {node: 20 || >=22} + hasBin: true + + rollup-plugin-purge-icons@0.10.0: + resolution: {integrity: sha512-GD2ftg4L9G/sagIhtCmBn5vdyzePOisniythubpbywP0Q3ix9rZuDeFvgXTPemOsc22pvH7t22ryYQIl0rwGog==} + engines: {node: '>= 12'} + + rollup-plugin-visualizer@5.14.0: + resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + rolldown: 1.x + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup@2.79.2: + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + engines: {node: '>=10.0.0'} + hasBin: true + + rollup@4.30.1: + resolution: {integrity: sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha1-ZtE2jae9+SHrnZW9GpIp5/IaQ+4=} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha1-Hq+fqb2x/dTsdfWPnNtOa3gn7sY=} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-regex@1.1.0: + resolution: {integrity: sha1-QKNmnzsHfR6UPURinhV91IAjvy4=} + + safer-buffer@2.1.2: + resolution: {integrity: sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + scroll-into-view-if-needed@2.2.31: + resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + sentence-case@3.0.4: + resolution: {integrity: sha1-NkWnuMEXx4f96HAgViJbtipFEx8=} + + set-blocking@2.0.0: + resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + set-value@2.0.1: + resolution: {integrity: sha1-oY1AUw5vB95CKMfe/kInr4ytAFs=} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.0: + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + engines: {node: '>=18'} + + slash@3.0.0: + resolution: {integrity: sha1-ZTm+hwwWWtvVJAIg2+Nh8bxNRjQ=} + engines: {node: '>=8'} + + slash@4.0.0: + resolution: {integrity: sha1-JCI3IXbExsWt214q2oha+YSzlqc=} + engines: {node: '>=12'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slate-history@0.66.0: + resolution: {integrity: sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==} + peerDependencies: + slate: '>=0.65.3' + + slate@0.72.8: + resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==} + + slice-ansi@4.0.0: + resolution: {integrity: sha1-UA6N0P1VsFgVCGJVsxla3ypF/ms=} + engines: {node: '>=10'} + + slice-ansi@5.0.0: + resolution: {integrity: sha1-tzBjxXqpb5zYgWVLFSlNldKFxCo=} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + snabbdom@3.6.2: + resolution: {integrity: sha512-ig5qOnCDbugFntKi6c7Xlib8bA6xiJVk8O+WdFrV3wxbMqeHO0hXFQC4nAhPVWfZfi8255lcZkNhtIBINCc4+Q==} + engines: {node: '>=12.17.0'} + + snake-case@3.0.4: + resolution: {integrity: sha1-Tyu9Vo6ZNavf1ZPzTGkdrbScRSw=} + + snapdragon-node@2.1.1: + resolution: {integrity: sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=} + engines: {node: '>=0.10.0'} + + snapdragon-util@3.0.1: + resolution: {integrity: sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=} + engines: {node: '>=0.10.0'} + + snapdragon@0.8.2: + resolution: {integrity: sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=} + engines: {node: '>=0.10.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-resolve@0.5.3: + resolution: {integrity: sha1-GQhmvs51U+H48mei7oLGBrVQmho=} + + source-map-support@0.5.21: + resolution: {integrity: sha1-BP58f54e0tZiIzwoyys1ufY/bk8=} + + source-map-url@0.4.1: + resolution: {integrity: sha1-CvZmBadFpaL5HPG7+KevvCg97FY=} + + source-map@0.5.7: + resolution: {integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha1-6oBL2UhXQC5pktBaOO8a41qatMQ=} + + split-string@3.1.0: + resolution: {integrity: sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + ssr-window@3.0.0: + resolution: {integrity: sha1-/VuCgBY4lD4MxwTEaRgBQ1r3rDc=} + + stable@0.1.8: + resolution: {integrity: sha1-g26zyDgv4pNv6vVEYxAXzn1Ho88=} + + static-extend@0.1.2: + resolution: {integrity: sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=} + engines: {node: '>=0.10.0'} + + statuses@1.5.0: + resolution: {integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=} + engines: {node: '>= 0.6'} + + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + strict-uri-encode@1.1.0: + resolution: {integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=} + engines: {node: '>=0.10.0'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha1-JpxxF9J7Ba0uU2gwqOyJXvnG0BA=} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.3.0: + resolution: {integrity: sha1-QvEUWUpGzxqOMLCoT1bHjD7awh4=} + + strip-ansi@3.0.1: + resolution: {integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=} + engines: {node: '>=0.10.0'} + + strip-ansi@6.0.1: + resolution: {integrity: sha1-nibGPTD1NEPpSJSVshBdN7Z6hdk=} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha1-UolMMT+/8xiDUoCu1g/3Hr8SuP0=} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha1-MfEoGzgyYwQ0gxwxDAHMzajL4AY=} + engines: {node: '>=8'} + + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + + stylelint-config-html@1.1.0: + resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==} + engines: {node: ^12 || >=14} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recommended@14.0.1: + resolution: {integrity: sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-config-standard@36.0.1: + resolution: {integrity: sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-order@6.0.4: + resolution: {integrity: sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==} + peerDependencies: + stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1 + + stylelint@16.12.0: + resolution: {integrity: sha512-F8zZ3L/rBpuoBZRvI4JVT20ZanPLXfQLzMOZg1tzPflRVh9mKpOZ8qcSIhh1my3FjAjZWG4T2POwGnmn6a6hbg==} + engines: {node: '>=18.12.0'} + hasBin: true + + supports-color@2.0.0: + resolution: {integrity: sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=} + engines: {node: '>=0.8.0'} + + supports-color@3.2.3: + resolution: {integrity: sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=} + engines: {node: '>=0.8.0'} + + supports-color@7.2.0: + resolution: {integrity: sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=} + engines: {node: '>=8'} + + supports-hyperlinks@3.1.0: + resolution: {integrity: sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==} + engines: {node: '>=14.18'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-baker@1.7.0: + resolution: {integrity: sha1-g2f3jYdVUMUv5HVvcwPVxdfC6ac=} + + svg-tags@1.0.0: + resolution: {integrity: sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=} + + svgo@2.8.0: + resolution: {integrity: sha1-T/gMzmcQ3CeV8MfHQQHmdkz8zSQ=} + engines: {node: '>=10.13.0'} + hasBin: true + + synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + + system-architecture@0.1.0: + resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} + engines: {node: '>=18'} + + systemjs@6.15.1: + resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + terser@5.37.0: + resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==} + engines: {node: '>=10'} + hasBin: true + + text-extensions@2.4.0: + resolution: {integrity: sha1-oc/MUM802kG/0EfMdE+ATRaA6jQ=} + engines: {node: '>=8'} + + through@2.3.8: + resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=} + + tiny-warning@1.0.3: + resolution: {integrity: sha1-lKMNtFPfTGQ9D9VmBg1gqHXYR1Q=} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.10: + resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} + engines: {node: '>=12.0.0'} + + title-case@3.0.3: + resolution: {integrity: sha1-vGibRvAuQR8dHh0IH3w97KBImYI=} + + tmp@0.0.33: + resolution: {integrity: sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=} + engines: {node: '>=0.6.0'} + + to-object-path@0.3.0: + resolution: {integrity: sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=} + engines: {node: '>=0.10.0'} + + to-regex-range@2.1.1: + resolution: {integrity: sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=} + engines: {node: '>=0.10.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=} + engines: {node: '>=8.0'} + + to-regex@3.0.2: + resolution: {integrity: sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=} + engines: {node: '>=0.10.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=} + + traverse@0.6.10: + resolution: {integrity: sha512-hN4uFRxbK+PX56DxYiGHsTn2dME3TVr9vbNqlQGcGcPhJAn+tdP126iA+TArMpI4YSgnTkMWyoLl5bf81Hi5TA==} + engines: {node: '>= 0.4'} + + ts-api-utils@2.0.0: + resolution: {integrity: sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.3.0: + resolution: {integrity: sha1-gDuM2rPhK6WBpMpByIObuw2ssJ4=} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha1-B7ggO/pwVsBlcFDjzNLDdzC6uPE=} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha1-G/IH9LKPkVg2ZstfvTJ4hzAc1fQ=} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha1-0mCiSwGYQ24TP6JqUkptZfo7Ljc=} + engines: {node: '>=10'} + + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typedarray.prototype.slice@1.0.5: + resolution: {integrity: sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.19.1: + resolution: {integrity: sha512-LKPUQpdEMVOeKluHi8md7rwLcoXHhwvWp3x+sJkMuq3gGm9yaYJtPo8sRZSblMFJ5pcOGCAak/scKf1mvZDlQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + unc-path-regex@0.1.2: + resolution: {integrity: sha1-5z3T17DXxe2G+6xrCufYxqadUPo=} + engines: {node: '>=0.10.0'} + + unconfig@0.6.0: + resolution: {integrity: sha512-4C67J0nIF2QwSXty2kW3zZx1pMZ3iXabylvJWWgHybWVUcMf9pxwsngoQt0gC+AVstRywFqrRBp3qOXJayhpOw==} + + unctx@2.4.1: + resolution: {integrity: sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha1-VP0W4OyxZ88Ezx91a9zJLrp5dsM=} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unimport@3.14.5: + resolution: {integrity: sha512-tn890SwFFZxqaJSKQPPd+yygfKSATbM8BZWW1aCR2TJBTs1SDrmLamBueaFtYsGjHtQaRgqEbQflOjN2iW12gA==} + + union-value@1.0.1: + resolution: {integrity: sha1-C2/nuDWuzaYcbqTU8CwUIh4QmEc=} + engines: {node: '>=0.10.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unocss@0.65.4: + resolution: {integrity: sha512-KUCW5OzI20Ik6j1zXkkrpWhxZ59TwSKl6+DvmYHEzMfaEcrHlBZaFSApAoSt2CYSvo6SluGiKyr+Im1UTkd4KA==} + engines: {node: '>=14'} + peerDependencies: + '@unocss/webpack': 0.65.4 + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 + peerDependenciesMeta: + '@unocss/webpack': + optional: true + vite: + optional: true + + unpipe@1.0.0: + resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=} + engines: {node: '>= 0.8'} + + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} + engines: {node: '>=14.0.0'} + + unplugin@2.1.2: + resolution: {integrity: sha512-Q3LU0e4zxKfRko1wMV2HmP8lB9KWislY7hxXpxd+lGx0PRInE4vhMBVEZwpdVYHvtqzhSrzuIfErsob6bQfCzw==} + engines: {node: '>=18.12.0'} + + unset-value@1.0.0: + resolution: {integrity: sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=} + engines: {node: '>=0.10.0'} + + untyped@1.5.2: + resolution: {integrity: sha512-eL/8PlhLcMmlMDtNPKhyyz9kEBDS3Uk4yMu/ewlkT2WFbtzScjHWPJLdQLmaGPUKjXzwe9MumOtOgc4Fro96Kg==} + hasBin: true + + update-browserslist-db@1.1.2: + resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + upper-case-first@2.0.2: + resolution: {integrity: sha1-mSwyc/iCq9GdHgKJTMFHEX+EQyQ=} + + upper-case@2.0.2: + resolution: {integrity: sha1-2JgQgj+qsd8VSbfZenb4Ziuub3o=} + + uqr@0.1.2: + resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + + uri-js@4.4.1: + resolution: {integrity: sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=} + + urix@0.1.0: + resolution: {integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=} + + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + + use@3.1.1: + resolution: {integrity: sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=} + engines: {node: '>=0.10.0'} + + util-deprecate@1.0.2: + resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + + utils-merge@1.0.1: + resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=} + engines: {node: '>= 0.4.0'} + + v8flags@4.0.1: + resolution: {integrity: sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==} + engines: {node: '>= 10.13.0'} + + vary@1.1.2: + resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=} + engines: {node: '>= 0.8'} + + vite-plugin-ejs@1.7.0: + resolution: {integrity: sha512-JNP3zQDC4mSbfoJ3G73s5mmZITD8NGjUmLkq4swxyahy/W0xuokK9U9IJGXw7KCggq6UucT6hJ0p+tQrNtqTZw==} + peerDependencies: + vite: '>=5.0.0' + + vite-plugin-eslint@1.8.1: + resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} + peerDependencies: + eslint: '>=7' + vite: '>=2' + + vite-plugin-mock@2.9.6: + resolution: {integrity: sha512-/Rm59oPppe/ncbkSrUuAxIQihlI2YcBmnbR4ST1RA2VzM1C0tEQc1KlbQvnUGhXECAGTaQN2JyasiwXP6EtKgg==} + engines: {node: '>=12.0.0'} + peerDependencies: + mockjs: '>=1.1.0' + vite: '>=2.0.0' + + vite-plugin-progress@0.0.7: + resolution: {integrity: sha512-zyvKdcc/X+6hnw3J1HVV1TKrlFKC4Rh8GnDnWG/2qhRXjqytTcM++xZ+SAPnoDsSyWl8O93ymK0wZRgHAoglEQ==} + engines: {node: '>=14', pnpm: '>=7.0.0'} + peerDependencies: + vite: '>2.0.0-0' + + vite-plugin-purge-icons@0.10.0: + resolution: {integrity: sha512-4fMJKQuBu9lAPJWjqGEytRaxty1pP9bWgQLA68dwbbaCXu6NBrOUb/3kMaUc7TP09kerEk+qTriCk05OZXpjwA==} + engines: {node: '>= 12'} + peerDependencies: + vite: '>=2' + + vite-plugin-style-import@2.0.0: + resolution: {integrity: sha512-qtoHQae5dSUQPo/rYz/8p190VU5y19rtBaeV7ryLa/AYAU/e9CG89NrN/3+k7MR8mJy/GPIu91iJ3zk9foUOSA==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-svg-icons@2.0.1: + resolution: {integrity: sha512-6ktD+DhV6Rz3VtedYvBKKVA2eXF+sAQVaKkKLDSqGUfnhqXl3bj5PPkVTl3VexfTuZy66PmINi8Q6eFnVfRUmA==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-url-copy@1.1.4: + resolution: {integrity: sha512-LuNI0RHdg3gfbyIiY4yzJaIxhq2fAapUTV31eskHIV56CxioddReH+H+bZtRkEdcgr3XkFBdetUVRzBSJjNEyg==} + peerDependencies: + vite: '>=4.0.0' + + vite@6.0.7: + resolution: {integrity: sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-draggable-plus@0.6.0: + resolution: {integrity: sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==} + peerDependencies: + '@types/sortablejs': ^1.15.0 + '@vue/composition-api': '*' + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-flow-layout@0.1.1: + resolution: {integrity: sha512-JdgRRUVrN0Y2GosA0M68DEbKlXMqJ7FQgsK8CjQD2vxvNSqAU6PZEpi4cfcTVtfM2GVOMjHo7GKKLbXxOBqDqA==} + peerDependencies: + vue: ^3.4.37 + + vue-i18n@11.0.1: + resolution: {integrity: sha512-pWAT8CusK8q9/EpN7V3oxwHwxWm6+Kp2PeTZmRGvdZTkUzMQDpbbmHp0TwQ8xw04XKm23cr6B4GL72y3W8Yekg==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue-json-pretty@2.4.0: + resolution: {integrity: sha512-e9bP41DYYIc2tWaB6KuwqFJq5odZ8/GkE6vHQuGcbPn37kGk4a3n1RNw3ZYeDrl66NWXgTlOfS+M6NKkowmkWw==} + engines: {node: '>= 10.0.0', npm: '>= 5.0.0'} + peerDependencies: + vue: '>=3.0.0' + + vue-router@4.5.0: + resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==} + peerDependencies: + vue: ^3.2.0 + + vue-tsc@2.2.0: + resolution: {integrity: sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue-types@5.1.3: + resolution: {integrity: sha512-3Wy6QcZl0VusCCHX3vYrWSILFlrOB2EQDoySnuYmASM5cUp1FivJGfkS5lp1CutDgyRb41g32r/1QCmiBj5i1Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + vue: ^2.0.0 || ^3.0.0 + peerDependenciesMeta: + vue: + optional: true + + vue@3.5.13: + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + wcwidth@1.0.1: + resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=} + + webidl-conversions@3.0.1: + resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.18: + resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=} + engines: {node: '>= 8'} + hasBin: true + + wildcard@1.1.2: + resolution: {integrity: sha1-pwIEUwhNjNLv5wup02liY94XEKU=} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=} + + wrap-ansi@6.2.0: + resolution: {integrity: sha1-6Tk7oHEC5skaOyIUePAlfNKFblM=} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha1-Z+FFz/UQpqaYS98RUpEdadLrnkM=} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + xgplayer-subtitles@3.0.20: + resolution: {integrity: sha512-I1bjsIY+aKOrhYQspLdneOkYg+Vf4cJVGPnDSFnNebnxXl9Mhz5SEpWGzYizMYxL9UvsQ9pgjeEY0o4hkwM+kQ==} + peerDependencies: + core-js: '>=3.12.1' + + xgplayer@3.0.20: + resolution: {integrity: sha512-UNKZJRyODOZGdka83ao8fI18xdhzOV8qG4aNEOOkuOQbXFXfXsJMr/dazRHFP+uXmTqiCXr568euee3ch7CS7g==} + peerDependencies: + core-js: '>=3.12.1' + + xml-name-validator@4.0.0: + resolution: {integrity: sha1-eaAG4uYxSahgDxVDDwpHJdFSSDU=} + engines: {node: '>=12'} + + y18n@4.0.3: + resolution: {integrity: sha1-tfJZyCzW4zaSHv17/Yv1YN6e7t8=} + + y18n@5.0.8: + resolution: {integrity: sha1-f0k00PfKjFb5UxSTndzS3ZHOHVU=} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha1-27fa+b/YusmrRev2ArjLrQ1dCP0=} + + yallist@4.0.0: + resolution: {integrity: sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI=} + + yaml-eslint-parser@1.2.3: + resolution: {integrity: sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==} + engines: {node: ^14.17.0 || >=16.0.0} + + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} + engines: {node: '>= 14'} + hasBin: true + + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha1-vmjEl1xrKr9GkjawyHA2L6sJp7A=} + engines: {node: '>=6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha1-DYehbeAa7p2L7Cv7909nhRcw9Pg=} + engines: {node: '>=8'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha1-ApTrPe4FAo0x7hpfosVWpqrxChs=} + engines: {node: '>=10'} + + yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@antfu/install-pkg@0.4.1': + dependencies: + package-manager-detector: 0.2.8 + tinyexec: 0.3.2 + + '@antfu/utils@0.7.10': {} + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.3': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.3': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.26.3 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.26.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + debug: 4.4.0 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.25.9': + dependencies: + '@babel/types': 7.26.3 + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helper-wrap-function@7.25.9': + dependencies: + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + + '@babel/parser@7.26.3': + dependencies: + '@babel/types': 7.26.3 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.26.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 + + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-exponentiation-operator@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-typescript@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/preset-env@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-exponentiation-operator': 7.26.3(@babel/core@7.26.0) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.0) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.40.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/types': 7.26.3 + esutils: 2.0.3 + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/standalone@7.26.4': {} + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + + '@babel/traverse@7.26.4': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.3': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@commitlint/cli@19.6.1(@types/node@22.10.5)(typescript@5.7.3)': + dependencies: + '@commitlint/format': 19.5.0 + '@commitlint/lint': 19.6.0 + '@commitlint/load': 19.6.1(@types/node@22.10.5)(typescript@5.7.3) + '@commitlint/read': 19.5.0 + '@commitlint/types': 19.5.0 + tinyexec: 0.3.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@19.6.0': + dependencies: + '@commitlint/types': 19.5.0 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + ajv: 8.17.1 + + '@commitlint/ensure@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@19.5.0': {} + + '@commitlint/format@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + chalk: 5.4.1 + + '@commitlint/is-ignored@19.6.0': + dependencies: + '@commitlint/types': 19.5.0 + semver: 7.6.3 + + '@commitlint/lint@19.6.0': + dependencies: + '@commitlint/is-ignored': 19.6.0 + '@commitlint/parse': 19.5.0 + '@commitlint/rules': 19.6.0 + '@commitlint/types': 19.5.0 + + '@commitlint/load@19.6.1(@types/node@22.10.5)(typescript@5.7.3)': + dependencies: + '@commitlint/config-validator': 19.5.0 + '@commitlint/execute-rule': 19.5.0 + '@commitlint/resolve-extends': 19.5.0 + '@commitlint/types': 19.5.0 + chalk: 5.4.1 + cosmiconfig: 9.0.0(typescript@5.7.3) + cosmiconfig-typescript-loader: 6.1.0(@types/node@22.10.5)(cosmiconfig@9.0.0(typescript@5.7.3))(typescript@5.7.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@19.5.0': {} + + '@commitlint/parse@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@19.5.0': + dependencies: + '@commitlint/top-level': 19.5.0 + '@commitlint/types': 19.5.0 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 0.3.2 + + '@commitlint/resolve-extends@19.5.0': + dependencies: + '@commitlint/config-validator': 19.5.0 + '@commitlint/types': 19.5.0 + global-directory: 4.0.1 + import-meta-resolve: 4.1.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@19.6.0': + dependencies: + '@commitlint/ensure': 19.5.0 + '@commitlint/message': 19.5.0 + '@commitlint/to-lines': 19.5.0 + '@commitlint/types': 19.5.0 + + '@commitlint/to-lines@19.5.0': {} + + '@commitlint/top-level@19.5.0': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@19.5.0': + dependencies: + '@types/conventional-commits-parser': 5.0.1 + chalk: 5.4.1 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + + '@csstools/media-query-list-parser@4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.0.0)': + dependencies: + postcss-selector-parser: 7.0.0 + + '@ctrl/tinycolor@3.6.1': {} + + '@dual-bundle/import-meta-resolve@4.1.0': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.13(typescript@5.7.3))': + dependencies: + vue: 3.5.13(typescript@5.7.3) + + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/aix-ppc64@0.24.2': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.24.2': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-arm@0.24.2': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/android-x64@0.24.2': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.24.2': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.24.2': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.24.2': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.24.2': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.24.2': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-arm@0.24.2': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.24.2': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.24.2': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.24.2': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.24.2': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.24.2': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.24.2': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.24.2': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.24.2': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.24.2': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.24.2': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + + '@esbuild/win32-x64@0.24.2': + optional: true + + '@eslint-community/eslint-utils@4.4.1(eslint@9.17.0(jiti@2.4.2))': + dependencies: + eslint: 9.17.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.1': + dependencies: + '@eslint/object-schema': 2.1.5 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.4.0 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.17.0': {} + + '@eslint/object-schema@2.1.5': {} + + '@eslint/plugin-kit@0.2.4': + dependencies: + levn: 0.4.1 + + '@floating-ui/core@1.6.9': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.6.13': + dependencies: + '@floating-ui/core': 1.6.9 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/utils@0.2.9': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + + '@iconify/iconify@2.1.2': + dependencies: + cross-fetch: 3.2.0 + transitivePeerDependencies: + - encoding + + '@iconify/iconify@3.1.1': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/json@2.2.293': + dependencies: + '@iconify/types': 2.0.0 + pathe: 1.1.2 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.2.1': + dependencies: + '@antfu/install-pkg': 0.4.1 + '@antfu/utils': 0.7.10 + '@iconify/types': 2.0.0 + debug: 4.4.0 + globals: 15.14.0 + kolorist: 1.8.0 + local-pkg: 0.5.1 + mlly: 1.7.3 + transitivePeerDependencies: + - supports-color + + '@iconify/vue@4.3.0(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.13(typescript@5.7.3) + + '@inquirer/checkbox@4.0.4(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/figures': 1.0.9 + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + + '@inquirer/confirm@5.1.1(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + + '@inquirer/core@10.1.2(@types/node@22.10.5)': + dependencies: + '@inquirer/figures': 1.0.9 + '@inquirer/type': 3.0.2(@types/node@22.10.5) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + transitivePeerDependencies: + - '@types/node' + + '@inquirer/editor@4.2.1(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + external-editor: 3.1.0 + + '@inquirer/expand@4.0.4(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + yoctocolors-cjs: 2.1.2 + + '@inquirer/figures@1.0.9': {} + + '@inquirer/input@4.1.1(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + + '@inquirer/number@3.0.4(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + + '@inquirer/password@4.0.4(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + ansi-escapes: 4.3.2 + + '@inquirer/prompts@7.2.1(@types/node@22.10.5)': + dependencies: + '@inquirer/checkbox': 4.0.4(@types/node@22.10.5) + '@inquirer/confirm': 5.1.1(@types/node@22.10.5) + '@inquirer/editor': 4.2.1(@types/node@22.10.5) + '@inquirer/expand': 4.0.4(@types/node@22.10.5) + '@inquirer/input': 4.1.1(@types/node@22.10.5) + '@inquirer/number': 3.0.4(@types/node@22.10.5) + '@inquirer/password': 4.0.4(@types/node@22.10.5) + '@inquirer/rawlist': 4.0.4(@types/node@22.10.5) + '@inquirer/search': 3.0.4(@types/node@22.10.5) + '@inquirer/select': 4.0.4(@types/node@22.10.5) + '@types/node': 22.10.5 + + '@inquirer/rawlist@4.0.4(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + yoctocolors-cjs: 2.1.2 + + '@inquirer/search@3.0.4(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/figures': 1.0.9 + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + yoctocolors-cjs: 2.1.2 + + '@inquirer/select@4.0.4(@types/node@22.10.5)': + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/figures': 1.0.9 + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + + '@inquirer/type@3.0.2(@types/node@22.10.5)': + dependencies: + '@types/node': 22.10.5 + + '@intlify/bundle-utils@10.0.0(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))': + dependencies: + '@intlify/message-compiler': 12.0.0-alpha.4 + '@intlify/shared': 12.0.0-alpha.4 + acorn: 8.14.0 + escodegen: 2.1.0 + estree-walker: 2.0.2 + jsonc-eslint-parser: 2.4.0 + mlly: 1.7.3 + source-map-js: 1.2.1 + yaml-eslint-parser: 1.2.3 + optionalDependencies: + vue-i18n: 11.0.1(vue@3.5.13(typescript@5.7.3)) + + '@intlify/core-base@11.0.1': + dependencies: + '@intlify/message-compiler': 11.0.1 + '@intlify/shared': 11.0.1 + + '@intlify/message-compiler@11.0.1': + dependencies: + '@intlify/shared': 11.0.1 + source-map-js: 1.2.1 + + '@intlify/message-compiler@12.0.0-alpha.4': + dependencies: + '@intlify/shared': 12.0.0-alpha.4 + source-map-js: 1.2.1 + + '@intlify/shared@11.0.1': {} + + '@intlify/shared@11.4.8': {} + + '@intlify/shared@12.0.0-alpha.4': {} + + '@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.17.0(jiti@2.4.2))(rollup@4.30.1)(typescript@5.7.3)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2)) + '@intlify/bundle-utils': 10.0.0(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3))) + '@intlify/shared': 11.4.8 + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.4.8)(@vue/compiler-dom@3.5.13)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)) + '@rollup/pluginutils': 5.1.4(rollup@4.30.1) + '@typescript-eslint/scope-manager': 8.19.1 + '@typescript-eslint/typescript-estree': 8.19.1(typescript@5.7.3) + debug: 4.4.0 + fast-glob: 3.3.3 + js-yaml: 4.1.0 + json5: 2.2.3 + pathe: 1.1.2 + picocolors: 1.1.1 + source-map-js: 1.2.1 + unplugin: 1.16.1 + vue: 3.5.13(typescript@5.7.3) + optionalDependencies: + vue-i18n: 11.0.1(vue@3.5.13(typescript@5.7.3)) + transitivePeerDependencies: + - '@vue/compiler-dom' + - eslint + - rollup + - supports-color + - typescript + + '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.4.8)(@vue/compiler-dom@3.5.13)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@babel/parser': 7.26.3 + optionalDependencies: + '@intlify/shared': 11.4.8 + '@vue/compiler-dom': 3.5.13 + vue: 3.5.13(typescript@5.7.3) + vue-i18n: 11.0.1(vue@3.5.13(typescript@5.7.3)) + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.18.0 + + '@nuxt/kit@3.15.1(rollup@4.30.1)': + dependencies: + '@nuxt/schema': 3.15.1 + c12: 2.0.1 + consola: 3.3.3 + defu: 6.1.4 + destr: 2.0.3 + globby: 14.0.2 + ignore: 7.0.0 + jiti: 2.4.2 + klona: 2.0.6 + knitwork: 1.2.0 + mlly: 1.7.3 + ohash: 1.1.4 + pathe: 2.0.0 + pkg-types: 1.3.0 + scule: 1.3.0 + semver: 7.6.3 + ufo: 1.5.4 + unctx: 2.4.1 + unimport: 3.14.5(rollup@4.30.1) + untyped: 1.5.2 + transitivePeerDependencies: + - magicast + - rollup + - supports-color + + '@nuxt/schema@3.15.1': + dependencies: + consola: 3.3.3 + defu: 6.1.4 + pathe: 2.0.0 + std-env: 3.8.0 + + '@pkgr/core@0.1.1': {} + + '@polka/url@1.0.0-next.28': {} + + '@purge-icons/core@0.10.0': + dependencies: + '@iconify/iconify': 2.1.2 + axios: 0.26.1(debug@4.4.0) + debug: 4.4.0 + fast-glob: 3.3.3 + fs-extra: 10.1.0 + transitivePeerDependencies: + - encoding + - supports-color + + '@purge-icons/generated@0.10.0': + dependencies: + '@iconify/iconify': 3.1.1 + + '@rollup/plugin-node-resolve@13.3.0(rollup@4.30.1)': + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@4.30.1) + '@types/resolve': 1.17.1 + deepmerge: 4.3.1 + is-builtin-module: 3.2.1 + is-module: 1.0.0 + resolve: 1.22.10 + rollup: 4.30.1 + + '@rollup/pluginutils@3.1.0(rollup@4.30.1)': + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 4.30.1 + + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + + '@rollup/pluginutils@5.1.4(rollup@4.30.1)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.30.1 + + '@rollup/rollup-android-arm-eabi@4.30.1': + optional: true + + '@rollup/rollup-android-arm64@4.30.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.30.1': + optional: true + + '@rollup/rollup-darwin-x64@4.30.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.30.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.30.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.30.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.30.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.30.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.30.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.30.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.30.1': + optional: true + + '@sindresorhus/merge-streams@2.3.0': {} + + '@sxzz/popperjs-es@2.11.7': {} + + '@transloadit/prettier-bytes@0.0.7': {} + + '@trysound/sax@0.2.0': {} + + '@types/conventional-commits-parser@5.0.1': + dependencies: + '@types/node': 22.10.5 + + '@types/eslint@8.56.12': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + + '@types/estree@0.0.39': {} + + '@types/estree@1.0.6': {} + + '@types/event-emitter@0.3.5': {} + + '@types/fined@1.1.5': {} + + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.10.5 + + '@types/inquirer@9.0.7': + dependencies: + '@types/through': 0.0.33 + rxjs: 7.8.1 + + '@types/json-schema@7.0.15': {} + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.10.5 + + '@types/liftoff@4.0.3': + dependencies: + '@types/fined': 1.1.5 + '@types/node': 22.10.5 + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.14 + + '@types/lodash@4.17.14': {} + + '@types/mockjs@1.0.10': {} + + '@types/node@10.17.60': {} + + '@types/node@22.10.5': + dependencies: + undici-types: 6.20.0 + + '@types/nprogress@0.2.3': {} + + '@types/qrcode@1.5.5': + dependencies: + '@types/node': 22.10.5 + + '@types/qs@6.9.17': {} + + '@types/resolve@1.17.1': + dependencies: + '@types/node': 22.10.5 + + '@types/sortablejs@1.15.8': {} + + '@types/svgo@2.6.4': + dependencies: + '@types/node': 22.10.5 + + '@types/through@0.0.33': + dependencies: + '@types/node': 22.10.5 + + '@types/web-bluetooth@0.0.16': {} + + '@types/web-bluetooth@0.0.20': {} + + '@typescript-eslint/eslint-plugin@8.19.1(@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/scope-manager': 8.19.1 + '@typescript-eslint/type-utils': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.19.1 + eslint: 9.17.0(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 2.0.0(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.19.1 + '@typescript-eslint/types': 8.19.1 + '@typescript-eslint/typescript-estree': 8.19.1(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.19.1 + debug: 4.4.0 + eslint: 9.17.0(jiti@2.4.2) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.19.1': + dependencies: + '@typescript-eslint/types': 8.19.1 + '@typescript-eslint/visitor-keys': 8.19.1 + + '@typescript-eslint/type-utils@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.19.1(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + debug: 4.4.0 + eslint: 9.17.0(jiti@2.4.2) + ts-api-utils: 2.0.0(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.19.1': {} + + '@typescript-eslint/typescript-estree@8.19.1(typescript@5.7.3)': + dependencies: + '@typescript-eslint/types': 8.19.1 + '@typescript-eslint/visitor-keys': 8.19.1 + debug: 4.4.0 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 2.0.0(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.19.1 + '@typescript-eslint/types': 8.19.1 + '@typescript-eslint/typescript-estree': 8.19.1(typescript@5.7.3) + eslint: 9.17.0(jiti@2.4.2) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.19.1': + dependencies: + '@typescript-eslint/types': 8.19.1 + eslint-visitor-keys: 4.2.0 + + '@unocss/astro@0.65.4(rollup@4.30.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@unocss/core': 0.65.4 + '@unocss/reset': 0.65.4 + '@unocss/vite': 0.65.4(rollup@4.30.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)) + optionalDependencies: + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - rollup + - supports-color + - vue + + '@unocss/cli@0.65.4(rollup@4.30.1)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.4(rollup@4.30.1) + '@unocss/config': 0.65.4 + '@unocss/core': 0.65.4 + '@unocss/preset-uno': 0.65.4 + cac: 6.7.14 + chokidar: 3.6.0 + colorette: 2.0.20 + consola: 3.3.3 + magic-string: 0.30.17 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + tinyglobby: 0.2.10 + transitivePeerDependencies: + - rollup + - supports-color + + '@unocss/config@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + unconfig: 0.6.0 + transitivePeerDependencies: + - supports-color + + '@unocss/core@0.65.4': {} + + '@unocss/extractor-arbitrary-variants@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + + '@unocss/inspector@0.65.4(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@unocss/core': 0.65.4 + '@unocss/rule-utils': 0.65.4 + colorette: 2.0.20 + gzip-size: 6.0.0 + sirv: 3.0.0 + vue-flow-layout: 0.1.1(vue@3.5.13(typescript@5.7.3)) + transitivePeerDependencies: + - vue + + '@unocss/postcss@0.65.4(postcss@8.4.49)': + dependencies: + '@unocss/config': 0.65.4 + '@unocss/core': 0.65.4 + '@unocss/rule-utils': 0.65.4 + css-tree: 3.1.0 + postcss: 8.4.49 + tinyglobby: 0.2.10 + transitivePeerDependencies: + - supports-color + + '@unocss/preset-attributify@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + + '@unocss/preset-icons@0.65.4': + dependencies: + '@iconify/utils': 2.2.1 + '@unocss/core': 0.65.4 + ofetch: 1.4.1 + transitivePeerDependencies: + - supports-color + + '@unocss/preset-mini@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + '@unocss/extractor-arbitrary-variants': 0.65.4 + '@unocss/rule-utils': 0.65.4 + + '@unocss/preset-tagify@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + + '@unocss/preset-typography@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + '@unocss/preset-mini': 0.65.4 + + '@unocss/preset-uno@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + '@unocss/preset-mini': 0.65.4 + '@unocss/preset-wind': 0.65.4 + '@unocss/rule-utils': 0.65.4 + + '@unocss/preset-web-fonts@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + ofetch: 1.4.1 + + '@unocss/preset-wind@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + '@unocss/preset-mini': 0.65.4 + '@unocss/rule-utils': 0.65.4 + + '@unocss/reset@0.65.4': {} + + '@unocss/rule-utils@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + magic-string: 0.30.17 + + '@unocss/transformer-attributify-jsx@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + + '@unocss/transformer-compile-class@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + + '@unocss/transformer-directives@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + '@unocss/rule-utils': 0.65.4 + css-tree: 3.1.0 + + '@unocss/transformer-variant-group@0.65.4': + dependencies: + '@unocss/core': 0.65.4 + + '@unocss/vite@0.65.4(rollup@4.30.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.4(rollup@4.30.1) + '@unocss/config': 0.65.4 + '@unocss/core': 0.65.4 + '@unocss/inspector': 0.65.4(vue@3.5.13(typescript@5.7.3)) + chokidar: 3.6.0 + magic-string: 0.30.17 + tinyglobby: 0.2.10 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - rollup + - supports-color + - vue + + '@uppy/companion-client@2.2.2': + dependencies: + '@uppy/utils': 4.1.3 + namespace-emitter: 2.0.1 + + '@uppy/core@2.3.4': + dependencies: + '@transloadit/prettier-bytes': 0.0.7 + '@uppy/store-default': 2.1.1 + '@uppy/utils': 4.1.3 + lodash.throttle: 4.1.1 + mime-match: 1.0.2 + namespace-emitter: 2.0.1 + nanoid: 3.3.8 + preact: 10.25.4 + + '@uppy/store-default@2.1.1': {} + + '@uppy/utils@4.1.3': + dependencies: + lodash.throttle: 4.1.1 + + '@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)': + dependencies: + '@uppy/companion-client': 2.2.2 + '@uppy/core': 2.3.4 + '@uppy/utils': 4.1.3 + nanoid: 3.3.8 + + '@vitejs/plugin-legacy@6.0.0(terser@5.37.0)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))': + dependencies: + '@babel/core': 7.26.0 + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + browserslist: 4.24.4 + browserslist-to-esbuild: 2.1.1(browserslist@4.24.4) + core-js: 3.40.0 + magic-string: 0.30.17 + regenerator-runtime: 0.14.1 + systemjs: 6.15.1 + terser: 5.37.0 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue-jsx@4.1.1(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-transform-typescript': 7.26.3(@babel/core@7.26.0) + '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.26.0) + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + vue: 3.5.13(typescript@5.7.3) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue@5.2.1(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))': + dependencies: + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + vue: 3.5.13(typescript@5.7.3) + + '@volar/language-core@2.4.11': + dependencies: + '@volar/source-map': 2.4.11 + + '@volar/source-map@2.4.11': {} + + '@volar/typescript@2.4.11': + dependencies: + '@volar/language-core': 2.4.11 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + + '@vue/babel-helper-vue-transform-on@1.2.5': {} + + '@vue/babel-plugin-jsx@1.2.5(@babel/core@7.26.0)': + dependencies: + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + '@vue/babel-helper-vue-transform-on': 1.2.5 + '@vue/babel-plugin-resolve-type': 1.2.5(@babel/core@7.26.0) + html-tags: 3.3.1 + svg-tags: 1.0.0 + optionalDependencies: + '@babel/core': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.2.5(@babel/core@7.26.0)': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/parser': 7.26.3 + '@vue/compiler-sfc': 3.5.13 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.26.3 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.26.3 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.4.49 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/language-core@2.2.0(typescript@5.7.3)': + dependencies: + '@volar/language-core': 2.4.11 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.13 + alien-signals: 0.4.14 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.7.3 + + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.7.3) + + '@vue/shared@3.5.13': {} + + '@vueuse/core@12.3.0(typescript@5.7.3)': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 12.3.0 + '@vueuse/shared': 12.3.0(typescript@5.7.3) + vue: 3.5.13(typescript@5.7.3) + transitivePeerDependencies: + - typescript + + '@vueuse/core@9.13.0(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.13(typescript@5.7.3)) + vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@12.3.0': {} + + '@vueuse/metadata@9.13.0': {} + + '@vueuse/shared@12.3.0(typescript@5.7.3)': + dependencies: + vue: 3.5.13(typescript@5.7.3) + transitivePeerDependencies: + - typescript + + '@vueuse/shared@9.13.0(vue@3.5.13(typescript@5.7.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + is-url: 1.2.4 + lodash.throttle: 4.1.1 + nanoid: 3.3.8 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/code-highlight@1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + prismjs: 1.29.0 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@types/event-emitter': 0.3.5 + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + dom7: 3.0.0 + event-emitter: 0.3.5 + html-void-elements: 2.0.1 + i18next: 20.6.1 + is-hotkey: 0.2.0 + lodash.camelcase: 4.3.0 + lodash.clonedeep: 4.5.0 + lodash.debounce: 4.0.8 + lodash.foreach: 4.5.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + lodash.toarray: 4.4.0 + nanoid: 3.3.8 + scroll-into-view-if-needed: 2.2.31 + slate: 0.72.8 + slate-history: 0.66.0(slate@0.72.8) + snabbdom: 3.6.2 + + '@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@wangeditor/editor': 5.1.23 + vue: 3.5.13(typescript@5.7.3) + + '@wangeditor/editor@5.1.23': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/code-highlight': 1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/list-module': 1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/table-module': 1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/upload-image-module': 1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/video-module': 1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + is-hotkey: 0.2.0 + lodash.camelcase: 4.3.0 + lodash.clonedeep: 4.5.0 + lodash.debounce: 4.0.8 + lodash.foreach: 4.5.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + lodash.toarray: 4.4.0 + nanoid: 3.3.8 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/list-module@1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/table-module@1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + nanoid: 3.3.8 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/upload-image-module@1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + lodash.foreach: 4.5.0 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/video-module@1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.8)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + nanoid: 3.3.8 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@zxcvbn-ts/core@3.0.4': + dependencies: + fastest-levenshtein: 1.0.16 + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + aggregate-error@4.0.1: + dependencies: + clean-stack: 4.2.0 + indent-string: 5.0.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@0.4.14: {} + + animate.css@4.1.1: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-regex@2.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@2.2.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + arr-diff@4.0.0: {} + + arr-flatten@1.1.0: {} + + arr-union@3.1.0: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.3 + is-array-buffer: 3.0.5 + + array-each@1.0.1: {} + + array-ify@1.0.0: {} + + array-slice@1.1.0: {} + + array-union@2.1.0: {} + + array-unique@0.3.2: {} + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + is-array-buffer: 3.0.5 + + assign-symbols@1.0.0: {} + + astral-regex@2.0.0: {} + + async-validator@4.2.5: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + atob@2.1.2: {} + + autoprefixer@10.4.20(postcss@8.4.49): + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001690 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + axios@0.26.1(debug@4.4.0): + dependencies: + follow-redirects: 1.15.9(debug@4.4.0) + transitivePeerDependencies: + - debug + + axios@1.7.9: + dependencies: + follow-redirects: 1.15.9(debug@4.4.0) + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0): + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.40.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + base64-js@1.5.1: {} + + base@0.11.2: + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.1 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + + big.js@5.2.2: {} + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird@3.7.2: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@2.3.2: + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist-to-esbuild@2.1.1(browserslist@4.24.4): + dependencies: + browserslist: 4.24.4 + meow: 13.2.0 + + browserslist@4.24.4: + dependencies: + caniuse-lite: 1.0.30001690 + electron-to-chromium: 1.5.79 + node-releases: 2.0.19 + update-browserslist-db: 1.1.2(browserslist@4.24.4) + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builtin-modules@3.3.0: {} + + bundle-require@5.1.0(esbuild@0.24.2): + dependencies: + esbuild: 0.24.2 + load-tsconfig: 0.2.5 + + c12@2.0.1: + dependencies: + chokidar: 4.0.3 + confbox: 0.1.8 + defu: 6.1.4 + dotenv: 16.4.7 + giget: 1.2.3 + jiti: 2.4.2 + mlly: 1.7.3 + ohash: 1.1.4 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.3.0 + rc9: 2.1.2 + + cac@6.7.14: {} + + cache-base@1.0.1: + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.1 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + + call-bind-apply-helpers@1.0.1: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + get-intrinsic: 1.2.7 + set-function-length: 1.2.2 + + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.1 + get-intrinsic: 1.2.7 + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001690: {} + + capital-case@1.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + upper-case-first: 2.0.2 + + chalk@1.1.3: + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.4.1: {} + + change-case@4.1.2: + dependencies: + camel-case: 4.1.2 + capital-case: 1.0.4 + constant-case: 3.0.4 + dot-case: 3.0.4 + header-case: 2.0.4 + no-case: 3.0.4 + param-case: 3.0.4 + pascal-case: 3.1.2 + path-case: 3.0.4 + sentence-case: 3.0.4 + snake-case: 3.0.4 + tslib: 2.8.1 + + chardet@0.7.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.0.2 + + chownr@2.0.0: {} + + citty@0.1.6: + dependencies: + consola: 3.3.3 + + class-utils@0.3.6: + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + + clean-stack@4.2.0: + dependencies: + escape-string-regexp: 5.0.0 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cli-width@4.1.0: {} + + clipboardy@4.0.0: + dependencies: + execa: 8.0.1 + is-wsl: 3.1.0 + is64bit: 2.0.0 + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + clone@2.1.2: {} + + collection-visit@1.0.0: + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@12.1.0: {} + + commander@13.0.0: {} + + commander@2.20.3: {} + + commander@7.2.0: {} + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + component-emitter@1.3.1: {} + + compute-scroll-into-view@1.0.20: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + consola@3.3.3: {} + + console@0.7.2: {} + + constant-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + upper-case: 2.0.2 + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + convert-source-map@2.0.0: {} + + copy-anything@2.0.6: + dependencies: + is-what: 3.14.1 + + copy-descriptor@0.1.1: {} + + core-js-compat@3.40.0: + dependencies: + browserslist: 4.24.4 + + core-js@3.40.0: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig-typescript-loader@6.1.0(@types/node@22.10.5)(cosmiconfig@9.0.0(typescript@5.7.3))(typescript@5.7.3): + dependencies: + '@types/node': 22.10.5 + cosmiconfig: 9.0.0(typescript@5.7.3) + jiti: 2.4.2 + typescript: 5.7.3 + + cosmiconfig@9.0.0(typescript@5.7.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.7.3 + + cropperjs@1.6.2: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-js@4.2.0: {} + + css-functions-list@3.2.3: {} + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + + cssesc@3.0.0: {} + + csso@4.2.0: + dependencies: + css-tree: 1.1.3 + + csstype@3.1.3: {} + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + + danmu.js@1.1.13: + dependencies: + event-emitter: 0.3.5 + + dargs@8.1.0: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + dayjs@1.11.13: {} + + de-indent@1.0.2: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decode-uri-component@0.2.2: {} + + deep-is@0.1.4: {} + + deep-pick-omit@1.2.1: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + define-property@0.2.5: + dependencies: + is-descriptor: 0.1.7 + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.3 + + define-property@2.0.2: + dependencies: + is-descriptor: 1.0.3 + isobject: 3.0.1 + + defu@6.1.4: {} + + del@7.1.0: + dependencies: + globby: 13.2.2 + graceful-fs: 4.2.11 + is-glob: 4.0.3 + is-path-cwd: 3.0.0 + is-path-inside: 4.0.0 + p-map: 5.5.0 + rimraf: 3.0.2 + slash: 4.0.0 + + delayed-stream@1.0.0: {} + + delegate@3.2.0: {} + + destr@2.0.3: {} + + detect-file@1.0.0: {} + + dijkstrajs@1.0.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dom-serializer@0.2.2: + dependencies: + domelementtype: 2.3.0 + entities: 2.2.0 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + dom7@3.0.0: + dependencies: + ssr-window: 3.0.0 + + domelementtype@1.3.1: {} + + domelementtype@2.3.0: {} + + domhandler@2.4.2: + dependencies: + domelementtype: 1.3.1 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@1.7.0: + dependencies: + dom-serializer: 0.2.2 + domelementtype: 1.3.1 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dotenv@16.4.7: {} + + downloadjs@1.4.7: {} + + driver.js@1.3.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + echarts-wordcloud@2.1.0(echarts@5.6.0): + dependencies: + echarts: 5.6.0 + + echarts@5.6.0: + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-to-chromium@1.5.79: {} + + element-plus@2.9.2(vue@3.5.13(typescript@5.7.3)): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.13(typescript@5.7.3)) + '@floating-ui/dom': 1.6.13 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.14 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.13(typescript@5.7.3)) + async-validator: 4.2.5 + dayjs: 1.11.13 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.13(typescript@5.7.3) + transitivePeerDependencies: + - '@vue/composition-api' + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + emojis-list@3.0.0: {} + + encodeurl@1.0.2: {} + + entities@1.1.2: {} + + entities@2.2.0: {} + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + environment@1.1.0: {} + + errno@0.1.8: + dependencies: + prr: 1.0.1 + optional: true + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.23.9: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.2.7 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.0 + math-intrinsics: 1.1.0 + object-inspect: 1.13.3 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.18 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@0.9.3: {} + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + esbuild@0.11.3: {} + + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-config-prettier@9.1.0(eslint@9.17.0(jiti@2.4.2)): + dependencies: + eslint: 9.17.0(jiti@2.4.2) + + eslint-define-config@2.1.0: {} + + eslint-plugin-prettier@5.2.1(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(prettier@3.4.2): + dependencies: + eslint: 9.17.0(jiti@2.4.2) + prettier: 3.4.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.2 + optionalDependencies: + '@types/eslint': 8.56.12 + eslint-config-prettier: 9.1.0(eslint@9.17.0(jiti@2.4.2)) + + eslint-plugin-vue@9.32.0(eslint@9.17.0(jiti@2.4.2)): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2)) + eslint: 9.17.0(jiti@2.4.2) + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.6.3 + vue-eslint-parser: 9.4.3(eslint@9.17.0(jiti@2.4.2)) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.17.0(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.1 + '@eslint/core': 0.9.1 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.17.0 + '@eslint/plugin-kit': 0.2.4 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + + esno@4.8.0: + dependencies: + tsx: 4.19.2 + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + espree@9.6.1: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@1.0.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.1: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + expand-brackets@2.1.4: + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + ext@1.7.0: + dependencies: + type: 2.7.3 + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + + extend@3.0.2: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + extglob@2.0.4: + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.0.5: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.18.0: + dependencies: + reusify: 1.0.4 + + fdir@6.4.2(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-entry-cache@9.1.0: + dependencies: + flat-cache: 5.0.0 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@4.0.0: + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + findup-sync@5.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 + + fined@2.0.0: + dependencies: + expand-tilde: 2.0.2 + is-plain-object: 5.0.0 + object.defaults: 1.1.0 + object.pick: 1.3.0 + parse-filepath: 1.0.2 + + flagged-respawn@2.0.0: {} + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + + flat-cache@5.0.0: + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + + flatted@3.3.2: {} + + follow-redirects@1.15.9(debug@4.4.0): + optionalDependencies: + debug: 4.4.0 + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + for-in@1.0.2: {} + + for-own@1.0.0: + dependencies: + for-in: 1.0.2 + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fraction.js@4.3.7: {} + + fragment-cache@0.2.1: + dependencies: + map-cache: 0.2.2 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + + get-intrinsic@1.2.7: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.0.0 + + get-stream@8.0.1: {} + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-value@2.0.6: {} + + giget@1.2.3: + dependencies: + citty: 0.1.6 + consola: 3.3.3 + defu: 6.1.4 + node-fetch-native: 1.6.4 + nypm: 0.3.12 + ohash: 1.1.4 + pathe: 1.1.2 + tar: 6.2.1 + + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@11.0.0: + dependencies: + foreground-child: 3.3.0 + jackspeak: 4.0.2 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@11.12.0: {} + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@15.14.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@13.2.2: + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 4.0.0 + + globby@14.0.2: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.3 + ignore: 5.3.2 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + + globjoin@0.1.4: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-ansi@2.0.0: + dependencies: + ansi-regex: 2.1.1 + + has-bigints@1.1.0: {} + + has-flag@1.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-value@0.3.1: + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + + has-value@1.0.0: + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + + has-values@0.1.4: {} + + has-values@1.0.0: + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + header-case@2.0.4: + dependencies: + capital-case: 1.0.4 + tslib: 2.8.1 + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + html-tags@3.3.1: {} + + html-void-elements@2.0.1: {} + + htmlparser2@3.10.1: + dependencies: + domelementtype: 1.3.1 + domhandler: 2.4.2 + domutils: 1.7.0 + entities: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + human-signals@5.0.0: {} + + husky@9.1.7: {} + + i18next@20.6.1: + dependencies: + '@babel/runtime': 7.26.0 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@6.0.2: {} + + ignore@7.0.0: {} + + image-size@0.5.5: {} + + immer@9.0.21: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.1.0: {} + + importx@0.5.1: + dependencies: + bundle-require: 5.1.0(esbuild@0.24.2) + debug: 4.4.0 + esbuild: 0.24.2 + jiti: 2.4.2 + pathe: 1.1.2 + tsx: 4.19.2 + transitivePeerDependencies: + - supports-color + + imurmurhash@0.1.4: {} + + indent-string@5.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + inquirer@12.3.0(@types/node@22.10.5): + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.5) + '@inquirer/prompts': 7.2.1(@types/node@22.10.5) + '@inquirer/type': 3.0.2(@types/node@22.10.5) + '@types/node': 22.10.5 + ansi-escapes: 4.3.2 + mute-stream: 2.0.0 + run-async: 3.0.0 + rxjs: 7.8.1 + + inquirer@9.3.7: + dependencies: + '@inquirer/figures': 1.0.9 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + external-editor: 3.1.0 + mute-stream: 1.0.0 + ora: 5.4.1 + run-async: 3.0.0 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + interpret@3.1.1: {} + + is-absolute@1.0.0: + dependencies: + is-relative: 1.0.0 + is-windows: 1.0.2 + + is-accessor-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.0: + dependencies: + call-bound: 1.0.3 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-buffer@1.1.6: {} + + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-descriptor@0.1.7: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-descriptor@1.0.3: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.3 + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.3 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hotkey@0.2.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@1.0.0: {} + + is-interactive@2.0.0: {} + + is-map@2.0.3: {} + + is-module@1.0.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-path-cwd@3.0.0: {} + + is-path-inside@4.0.0: {} + + is-plain-obj@1.1.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-plain-object@5.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-relative@1.0.0: + dependencies: + is-unc-path: 1.0.0 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.3 + + is-stream@3.0.0: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.3 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.18 + + is-unc-path@1.0.0: + dependencies: + unc-path-regex: 0.1.2 + + is-unicode-supported@0.1.0: {} + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-url@1.2.4: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.0: + dependencies: + call-bound: 1.0.3 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + + is-what@3.14.1: {} + + is-windows@1.0.2: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + is64bit@2.0.0: + dependencies: + system-architecture: 0.1.0 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isbinaryfile@5.0.4: {} + + isexe@2.0.0: {} + + isobject@2.1.0: + dependencies: + isarray: 1.0.0 + + isobject@3.0.1: {} + + jackspeak@4.0.2: + dependencies: + '@isaacs/cliui': 8.0.2 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jiti@2.4.2: {} + + js-base64@2.6.4: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonc-eslint-parser@2.4.0: + dependencies: + acorn: 8.14.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + semver: 7.6.3 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonparse@1.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + kind-of@4.0.0: + dependencies: + is-buffer: 1.1.6 + + kind-of@5.1.0: {} + + kind-of@6.0.3: {} + + klona@2.0.6: {} + + knitwork@1.2.0: {} + + known-css-properties@0.35.0: {} + + kolorist@1.8.0: {} + + less@4.2.1: + dependencies: + copy-anything: 2.0.6 + parse-node-version: 1.0.1 + tslib: 2.8.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + make-dir: 2.1.0 + mime: 1.6.0 + needle: 3.3.1 + source-map: 0.6.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + liftoff@4.0.0: + dependencies: + extend: 3.0.2 + findup-sync: 5.0.0 + fined: 2.0.0 + flagged-respawn: 2.0.0 + is-plain-object: 5.0.0 + object.map: 1.0.1 + rechoir: 0.8.0 + resolve: 1.22.10 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lint-staged@15.3.0: + dependencies: + chalk: 5.4.1 + commander: 12.1.0 + debug: 4.4.0 + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.2.5 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.6.1 + transitivePeerDependencies: + - supports-color + + listr2@8.2.5: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + + load-tsconfig@0.2.5: {} + + loader-utils@1.4.2: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 1.0.2 + + local-pkg@0.5.1: + dependencies: + mlly: 1.7.3 + pkg-types: 1.3.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + + lodash.camelcase@4.3.0: {} + + lodash.clonedeep@4.5.0: {} + + lodash.debounce@4.0.8: {} + + lodash.foreach@4.5.0: {} + + lodash.get@4.4.2: {} + + lodash.isequal@4.5.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.kebabcase@4.1.1: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.throttle@4.1.1: {} + + lodash.toarray@4.4.0: {} + + lodash.truncate@4.4.2: {} + + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-symbols@6.0.0: + dependencies: + chalk: 5.4.1 + is-unicode-supported: 1.3.0 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@11.0.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + optional: true + + make-iterator@1.0.1: + dependencies: + kind-of: 6.0.3 + + map-cache@0.2.2: {} + + map-visit@1.0.0: + dependencies: + object-visit: 1.0.1 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.0.14: {} + + mdn-data@2.12.2: {} + + memoize-one@6.0.0: {} + + meow@12.1.1: {} + + meow@13.2.0: {} + + merge-options@1.0.1: + dependencies: + is-plain-obj: 1.1.0 + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@3.1.0: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 1.0.0 + extend-shallow: 2.0.1 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 5.1.0 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-match@1.0.2: + dependencies: + wildcard: 1.1.2 + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: + optional: true + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mitt@3.0.1: {} + + mixin-deep@1.3.2: + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + + mkdirp@1.0.4: {} + + mkdirp@3.0.1: {} + + mlly@1.7.3: + dependencies: + acorn: 8.14.0 + pathe: 1.1.2 + pkg-types: 1.3.0 + ufo: 1.5.4 + + mockjs@1.1.0: + dependencies: + commander: 13.0.0 + + monaco-editor@0.52.2: {} + + mrmime@2.0.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + mute-stream@1.0.0: {} + + mute-stream@2.0.0: {} + + namespace-emitter@2.0.1: {} + + nanoid@3.3.8: {} + + nanomatch@1.2.13: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + natural-compare@1.4.0: {} + + needle@3.3.1: + dependencies: + iconv-lite: 0.6.3 + sax: 1.4.1 + optional: true + + neo-async@2.6.2: {} + + next-tick@1.1.0: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-fetch-native@1.6.4: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-plop@0.32.0: + dependencies: + '@types/inquirer': 9.0.7 + change-case: 4.1.2 + del: 7.1.0 + globby: 13.2.2 + handlebars: 4.7.8 + inquirer: 9.3.7 + isbinaryfile: 5.0.4 + lodash.get: 4.4.2 + lower-case: 2.0.2 + mkdirp: 3.0.1 + resolve: 1.22.10 + title-case: 3.0.3 + upper-case: 2.0.2 + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + normalize-wheel-es@1.2.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + nprogress@0.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nypm@0.3.12: + dependencies: + citty: 0.1.6 + consola: 3.3.3 + execa: 8.0.1 + pathe: 1.1.2 + pkg-types: 1.3.0 + ufo: 1.5.4 + + object-assign@4.1.1: {} + + object-copy@0.1.0: + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + + object-inspect@1.13.3: {} + + object-keys@1.1.1: {} + + object-visit@1.0.1: + dependencies: + isobject: 3.0.1 + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.defaults@1.1.0: + dependencies: + array-each: 1.0.1 + array-slice: 1.1.0 + for-own: 1.0.0 + isobject: 3.0.1 + + object.map@1.0.1: + dependencies: + for-own: 1.0.0 + make-iterator: 1.0.1 + + object.pick@1.3.0: + dependencies: + isobject: 3.0.1 + + ofetch@1.4.1: + dependencies: + destr: 2.0.3 + node-fetch-native: 1.6.4 + ufo: 1.5.4 + + ohash@1.1.4: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + ora@8.1.1: + dependencies: + chalk: 5.4.1 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + os-tmpdir@1.0.2: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.2.7 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.1.1 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@5.5.0: + dependencies: + aggregate-error: 4.0.1 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@0.2.8: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-filepath@1.0.2: + dependencies: + is-absolute: 1.0.0 + map-cache: 0.2.2 + path-root: 0.1.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-node-version@1.0.1: {} + + parse-passwd@1.0.0: {} + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + pascalcase@0.1.1: {} + + path-browserify@1.0.1: {} + + path-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-root-regex@0.1.2: {} + + path-root@0.1.1: + dependencies: + path-root-regex: 0.1.2 + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.0.2 + minipass: 7.1.2 + + path-to-regexp@6.3.0: {} + + path-type@4.0.0: {} + + path-type@5.0.0: {} + + pathe@0.2.0: {} + + pathe@1.1.2: {} + + pathe@2.0.0: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pidtree@0.6.0: {} + + pify@4.0.1: + optional: true + + pinia-plugin-persistedstate@4.2.0(pinia@2.3.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))(rollup@4.30.1): + dependencies: + '@nuxt/kit': 3.15.1(rollup@4.30.1) + deep-pick-omit: 1.2.1 + defu: 6.1.4 + destr: 2.0.3 + optionalDependencies: + pinia: 2.3.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) + transitivePeerDependencies: + - magicast + - rollup + - supports-color + + pinia@2.3.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.7.3) + vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3)) + optionalDependencies: + typescript: 5.7.3 + transitivePeerDependencies: + - '@vue/composition-api' + + pkg-types@1.3.0: + dependencies: + confbox: 0.1.8 + mlly: 1.7.3 + pathe: 1.1.2 + + plop@4.0.1: + dependencies: + '@types/liftoff': 4.0.3 + chalk: 5.4.1 + interpret: 3.1.1 + liftoff: 4.0.0 + minimist: 1.2.8 + node-plop: 0.32.0 + ora: 8.1.1 + v8flags: 4.0.1 + + pngjs@5.0.0: {} + + posix-character-classes@0.1.1: {} + + possible-typed-array-names@1.0.0: {} + + postcss-html@1.7.0: + dependencies: + htmlparser2: 8.0.2 + js-tokens: 9.0.1 + postcss: 8.4.49 + postcss-safe-parser: 6.0.0(postcss@8.4.49) + + postcss-less@6.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + postcss-prefix-selector@1.16.1(postcss@5.2.18): + dependencies: + postcss: 5.2.18 + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@6.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + postcss-safe-parser@7.0.1(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.0.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sorting@8.0.2(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + postcss-value-parser@4.2.0: {} + + postcss@5.2.18: + dependencies: + chalk: 1.1.3 + js-base64: 2.6.4 + source-map: 0.5.7 + supports-color: 3.2.3 + + postcss@8.4.49: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + posthtml-parser@0.2.1: + dependencies: + htmlparser2: 3.10.1 + isobject: 2.1.0 + + posthtml-rename-id@1.0.12: + dependencies: + escape-string-regexp: 1.0.5 + + posthtml-render@1.4.0: {} + + posthtml-svg-mode@1.0.3: + dependencies: + merge-options: 1.0.1 + posthtml: 0.9.2 + posthtml-parser: 0.2.1 + posthtml-render: 1.4.0 + + posthtml@0.9.2: + dependencies: + posthtml-parser: 0.2.1 + posthtml-render: 1.4.0 + + preact@10.25.4: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.4.2: {} + + prismjs@1.29.0: {} + + progress@2.0.3: {} + + proxy-from-env@1.1.0: {} + + prr@1.0.1: + optional: true + + punycode@1.4.1: {} + + punycode@2.3.1: {} + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + qs@6.13.1: + dependencies: + side-channel: 1.1.0 + + query-string@4.3.4: + dependencies: + object-assign: 4.1.1 + strict-uri-encode: 1.1.0 + + queue-microtask@1.2.3: {} + + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.3 + + rd@2.0.1: + dependencies: + '@types/node': 10.17.60 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.0.2: {} + + rechoir@0.8.0: + dependencies: + resolve: 1.22.10 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.7 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.14.1: {} + + regenerator-transform@0.15.2: + dependencies: + '@babel/runtime': 7.26.0 + + regex-not@1.0.2: + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpu-core@6.2.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + + regjsgen@0.8.0: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + + repeat-element@1.1.4: {} + + repeat-string@1.6.1: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve-url@0.2.1: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + ret@0.1.15: {} + + reusify@1.0.4: {} + + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rimraf@6.0.1: + dependencies: + glob: 11.0.0 + package-json-from-dist: 1.0.1 + + rollup-plugin-purge-icons@0.10.0: + dependencies: + '@purge-icons/core': 0.10.0 + '@purge-icons/generated': 0.10.0 + transitivePeerDependencies: + - encoding + - supports-color + + rollup-plugin-visualizer@5.14.0(rollup@4.30.1): + dependencies: + open: 8.4.2 + picomatch: 4.0.2 + source-map: 0.7.4 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.30.1 + + rollup@2.79.2: + optionalDependencies: + fsevents: 2.3.3 + + rollup@4.30.1: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.30.1 + '@rollup/rollup-android-arm64': 4.30.1 + '@rollup/rollup-darwin-arm64': 4.30.1 + '@rollup/rollup-darwin-x64': 4.30.1 + '@rollup/rollup-freebsd-arm64': 4.30.1 + '@rollup/rollup-freebsd-x64': 4.30.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.30.1 + '@rollup/rollup-linux-arm-musleabihf': 4.30.1 + '@rollup/rollup-linux-arm64-gnu': 4.30.1 + '@rollup/rollup-linux-arm64-musl': 4.30.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.30.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.30.1 + '@rollup/rollup-linux-riscv64-gnu': 4.30.1 + '@rollup/rollup-linux-s390x-gnu': 4.30.1 + '@rollup/rollup-linux-x64-gnu': 4.30.1 + '@rollup/rollup-linux-x64-musl': 4.30.1 + '@rollup/rollup-win32-arm64-msvc': 4.30.1 + '@rollup/rollup-win32-ia32-msvc': 4.30.1 + '@rollup/rollup-win32-x64-msvc': 4.30.1 + fsevents: 2.3.3 + + run-async@3.0.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-regex@1.1.0: + dependencies: + ret: 0.1.15 + + safer-buffer@2.1.2: {} + + sax@1.4.1: + optional: true + + scroll-into-view-if-needed@2.2.31: + dependencies: + compute-scroll-into-view: 1.0.20 + + scule@1.3.0: {} + + semver@5.7.2: + optional: true + + semver@6.3.1: {} + + semver@7.6.3: {} + + sentence-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + upper-case-first: 2.0.2 + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.7 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@3.0.0: + dependencies: + '@polka/url': 1.0.0-next.28 + mrmime: 2.0.0 + totalist: 3.0.1 + + slash@3.0.0: {} + + slash@4.0.0: {} + + slash@5.1.0: {} + + slate-history@0.66.0(slate@0.72.8): + dependencies: + is-plain-object: 5.0.0 + slate: 0.72.8 + + slate@0.72.8: + dependencies: + immer: 9.0.21 + is-plain-object: 5.0.0 + tiny-warning: 1.0.3 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + snabbdom@3.6.2: {} + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + snapdragon-node@2.1.1: + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + + snapdragon-util@3.0.1: + dependencies: + kind-of: 3.2.2 + + snapdragon@0.8.2: + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + source-map-resolve@0.5.3: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-url@0.4.1: {} + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + sourcemap-codec@1.4.8: {} + + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + + split2@4.2.0: {} + + ssr-window@3.0.0: {} + + stable@0.1.8: {} + + static-extend@0.1.2: + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + + statuses@1.5.0: {} + + std-env@3.8.0: {} + + stdin-discarder@0.2.2: {} + + strict-uri-encode@1.1.0: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.0.0 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + + stylelint-config-html@1.1.0(postcss-html@1.7.0)(stylelint@16.12.0(typescript@5.7.3)): + dependencies: + postcss-html: 1.7.0 + stylelint: 16.12.0(typescript@5.7.3) + + stylelint-config-recommended@14.0.1(stylelint@16.12.0(typescript@5.7.3)): + dependencies: + stylelint: 16.12.0(typescript@5.7.3) + + stylelint-config-standard@36.0.1(stylelint@16.12.0(typescript@5.7.3)): + dependencies: + stylelint: 16.12.0(typescript@5.7.3) + stylelint-config-recommended: 14.0.1(stylelint@16.12.0(typescript@5.7.3)) + + stylelint-order@6.0.4(stylelint@16.12.0(typescript@5.7.3)): + dependencies: + postcss: 8.4.49 + postcss-sorting: 8.0.2(postcss@8.4.49) + stylelint: 16.12.0(typescript@5.7.3) + + stylelint@16.12.0(typescript@5.7.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + '@csstools/media-query-list-parser': 4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.0.0) + '@dual-bundle/import-meta-resolve': 4.1.0 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.7.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.0 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 9.1.0 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 6.0.2 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.35.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.4.49) + postcss-selector-parser: 7.0.0 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.1.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + supports-color@2.0.0: {} + + supports-color@3.2.3: + dependencies: + has-flag: 1.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.1.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-baker@1.7.0: + dependencies: + bluebird: 3.7.2 + clone: 2.1.2 + he: 1.2.0 + image-size: 0.5.5 + loader-utils: 1.4.2 + merge-options: 1.0.1 + micromatch: 3.1.0 + postcss: 5.2.18 + postcss-prefix-selector: 1.16.1(postcss@5.2.18) + posthtml-rename-id: 1.0.12 + posthtml-svg-mode: 1.0.3 + query-string: 4.3.4 + traverse: 0.6.10 + transitivePeerDependencies: + - supports-color + + svg-tags@1.0.0: {} + + svgo@2.8.0: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.1.1 + stable: 0.1.8 + + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + + system-architecture@0.1.0: {} + + systemjs@6.15.1: {} + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + terser@5.37.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-extensions@2.4.0: {} + + through@2.3.8: {} + + tiny-warning@1.0.3: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.10: + dependencies: + fdir: 6.4.2(picomatch@4.0.2) + picomatch: 4.0.2 + + title-case@3.0.3: + dependencies: + tslib: 2.8.1 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-object-path@0.3.0: + dependencies: + kind-of: 3.2.2 + + to-regex-range@2.1.1: + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + to-regex@3.0.2: + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + + totalist@3.0.1: {} + + tr46@0.0.3: {} + + traverse@0.6.10: + dependencies: + gopd: 1.2.0 + typedarray.prototype.slice: 1.0.5 + which-typed-array: 1.1.18 + + ts-api-utils@2.0.0(typescript@5.7.3): + dependencies: + typescript: 5.7.3 + + tslib@2.3.0: {} + + tslib@2.8.1: {} + + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type@2.7.3: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.0.0 + reflect.getprototypeof: 1.0.10 + + typedarray.prototype.slice@1.0.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + get-proto: 1.0.1 + math-intrinsics: 1.1.0 + typed-array-buffer: 1.0.3 + typed-array-byte-offset: 1.0.4 + + typescript-eslint@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.19.1(@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.17.0(jiti@2.4.2) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + typescript@5.7.3: {} + + ufo@1.5.4: {} + + uglify-js@3.19.3: + optional: true + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.3 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + unc-path-regex@0.1.2: {} + + unconfig@0.6.0: + dependencies: + '@antfu/utils': 0.7.10 + defu: 6.1.4 + importx: 0.5.1 + transitivePeerDependencies: + - supports-color + + unctx@2.4.1: + dependencies: + acorn: 8.14.0 + estree-walker: 3.0.3 + magic-string: 0.30.17 + unplugin: 2.1.2 + + undici-types@6.20.0: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.2.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unicorn-magic@0.1.0: {} + + unimport@3.14.5(rollup@4.30.1): + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.30.1) + acorn: 8.14.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.3 + local-pkg: 0.5.1 + magic-string: 0.30.17 + mlly: 1.7.3 + pathe: 1.1.2 + picomatch: 4.0.2 + pkg-types: 1.3.0 + scule: 1.3.0 + strip-literal: 2.1.1 + unplugin: 1.16.1 + transitivePeerDependencies: + - rollup + + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + + universalify@2.0.1: {} + + unocss@0.65.4(postcss@8.4.49)(rollup@4.30.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)): + dependencies: + '@unocss/astro': 0.65.4(rollup@4.30.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)) + '@unocss/cli': 0.65.4(rollup@4.30.1) + '@unocss/core': 0.65.4 + '@unocss/postcss': 0.65.4(postcss@8.4.49) + '@unocss/preset-attributify': 0.65.4 + '@unocss/preset-icons': 0.65.4 + '@unocss/preset-mini': 0.65.4 + '@unocss/preset-tagify': 0.65.4 + '@unocss/preset-typography': 0.65.4 + '@unocss/preset-uno': 0.65.4 + '@unocss/preset-web-fonts': 0.65.4 + '@unocss/preset-wind': 0.65.4 + '@unocss/transformer-attributify-jsx': 0.65.4 + '@unocss/transformer-compile-class': 0.65.4 + '@unocss/transformer-directives': 0.65.4 + '@unocss/transformer-variant-group': 0.65.4 + '@unocss/vite': 0.65.4(rollup@4.30.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)) + optionalDependencies: + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - postcss + - rollup + - supports-color + - vue + + unpipe@1.0.0: {} + + unplugin@1.16.1: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + + unplugin@2.1.2: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + + unset-value@1.0.0: + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + + untyped@1.5.2: + dependencies: + '@babel/core': 7.26.0 + '@babel/standalone': 7.26.4 + '@babel/types': 7.26.3 + citty: 0.1.6 + defu: 6.1.4 + jiti: 2.4.2 + knitwork: 1.2.0 + scule: 1.3.0 + transitivePeerDependencies: + - supports-color + + update-browserslist-db@1.1.2(browserslist@4.24.4): + dependencies: + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + upper-case-first@2.0.2: + dependencies: + tslib: 2.8.1 + + upper-case@2.0.2: + dependencies: + tslib: 2.8.1 + + uqr@0.1.2: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + urix@0.1.0: {} + + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.13.1 + + use@3.1.1: {} + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + v8flags@4.0.1: {} + + vary@1.1.2: {} + + vite-plugin-ejs@1.7.0(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + ejs: 3.1.10 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + + vite-plugin-eslint@1.8.1(eslint@9.17.0(jiti@2.4.2))(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + '@rollup/pluginutils': 4.2.1 + '@types/eslint': 8.56.12 + eslint: 9.17.0(jiti@2.4.2) + rollup: 2.79.2 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + + vite-plugin-mock@2.9.6(mockjs@1.1.0)(rollup@4.30.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + '@rollup/plugin-node-resolve': 13.3.0(rollup@4.30.1) + '@types/mockjs': 1.0.10 + chalk: 4.1.2 + chokidar: 3.6.0 + connect: 3.7.0 + debug: 4.4.0 + esbuild: 0.11.3 + fast-glob: 3.3.3 + mockjs: 1.1.0 + path-to-regexp: 6.3.0 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - rollup + - supports-color + + vite-plugin-progress@0.0.7(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + picocolors: 1.1.1 + progress: 2.0.3 + rd: 2.0.1 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + + vite-plugin-purge-icons@0.10.0(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + '@purge-icons/core': 0.10.0 + '@purge-icons/generated': 0.10.0 + rollup-plugin-purge-icons: 0.10.0 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - encoding + - supports-color + + vite-plugin-style-import@2.0.0(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + '@rollup/pluginutils': 4.2.1 + change-case: 4.1.2 + console: 0.7.2 + es-module-lexer: 0.9.3 + fs-extra: 10.1.0 + magic-string: 0.25.9 + pathe: 0.2.0 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + + vite-plugin-svg-icons@2.0.1(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + '@types/svgo': 2.6.4 + cors: 2.8.5 + debug: 4.4.0 + etag: 1.8.1 + fs-extra: 10.1.0 + pathe: 0.2.0 + svg-baker: 1.7.0 + svgo: 2.8.0 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - supports-color + + vite-plugin-url-copy@1.1.4(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + clipboardy: 4.0.0 + consola: 3.3.3 + uqr: 0.1.2 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + + vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0): + dependencies: + esbuild: 0.24.2 + postcss: 8.4.49 + rollup: 4.30.1 + optionalDependencies: + '@types/node': 22.10.5 + fsevents: 2.3.3 + jiti: 2.4.2 + less: 4.2.1 + terser: 5.37.0 + tsx: 4.19.2 + yaml: 2.7.0 + + vscode-uri@3.0.8: {} + + vue-demi@0.14.10(vue@3.5.13(typescript@5.7.3)): + dependencies: + vue: 3.5.13(typescript@5.7.3) + + vue-draggable-plus@0.6.0(@types/sortablejs@1.15.8): + dependencies: + '@types/sortablejs': 1.15.8 + + vue-eslint-parser@9.4.3(eslint@9.17.0(jiti@2.4.2)): + dependencies: + debug: 4.4.0 + eslint: 9.17.0(jiti@2.4.2) + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + vue-flow-layout@0.1.1(vue@3.5.13(typescript@5.7.3)): + dependencies: + vue: 3.5.13(typescript@5.7.3) + + vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)): + dependencies: + '@intlify/core-base': 11.0.1 + '@intlify/shared': 11.0.1 + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.7.3) + + vue-json-pretty@2.4.0(vue@3.5.13(typescript@5.7.3)): + dependencies: + vue: 3.5.13(typescript@5.7.3) + + vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.7.3) + + vue-tsc@2.2.0(typescript@5.7.3): + dependencies: + '@volar/typescript': 2.4.11 + '@vue/language-core': 2.2.0(typescript@5.7.3) + typescript: 5.7.3 + + vue-types@5.1.3(vue@3.5.13(typescript@5.7.3)): + dependencies: + is-plain-object: 5.0.0 + optionalDependencies: + vue: 3.5.13(typescript@5.7.3) + + vue@3.5.13(typescript@5.7.3): + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.7.3)) + '@vue/shared': 3.5.13 + optionalDependencies: + typescript: 5.7.3 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@3.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.1 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.3 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.0 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.0 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.18 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-module@2.0.1: {} + + which-typed-array@1.1.18: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + for-each: 0.3.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wildcard@1.1.2: {} + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + xgplayer-subtitles@3.0.20(core-js@3.40.0): + dependencies: + core-js: 3.40.0 + eventemitter3: 4.0.7 + + xgplayer@3.0.20(core-js@3.40.0): + dependencies: + core-js: 3.40.0 + danmu.js: 1.1.13 + delegate: 3.2.0 + downloadjs: 1.4.7 + eventemitter3: 4.0.7 + xgplayer-subtitles: 3.0.20(core-js@3.40.0) + + xml-name-validator@4.0.0: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml-eslint-parser@1.2.3: + dependencies: + eslint-visitor-keys: 3.4.3 + lodash: 4.17.21 + yaml: 2.7.0 + + yaml@2.6.1: {} + + yaml@2.7.0: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@21.1.1: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.1.1: {} + + yoctocolors-cjs@2.1.2: {} + + zrender@5.6.1: + dependencies: + tslib: 2.3.0 diff --git a/promo-ui2/postcss.config.cjs b/promo-ui2/postcss.config.cjs new file mode 100644 index 0000000..961986e --- /dev/null +++ b/promo-ui2/postcss.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {} + } +} diff --git a/promo-ui2/prettier.config.cjs b/promo-ui2/prettier.config.cjs new file mode 100644 index 0000000..b46239f --- /dev/null +++ b/promo-ui2/prettier.config.cjs @@ -0,0 +1,19 @@ +module.exports = { + printWidth: 100, + tabWidth: 2, + useTabs: false, + semi: false, + vueIndentScriptAndStyle: false, + singleQuote: true, + quoteProps: 'as-needed', + bracketSpacing: true, + trailingComma: 'none', + jsxSingleQuote: false, + arrowParens: 'always', + insertPragma: false, + requirePragma: false, + proseWrap: 'never', + htmlWhitespaceSensitivity: 'strict', + endOfLine: 'auto', + rangeStart: 0 +} diff --git a/promo-ui2/public/favicon.ico b/promo-ui2/public/favicon.ico new file mode 100644 index 0000000..5a7de08 Binary files /dev/null and b/promo-ui2/public/favicon.ico differ diff --git a/promo-ui2/public/logo.png b/promo-ui2/public/logo.png new file mode 100644 index 0000000..b76c71b Binary files /dev/null and b/promo-ui2/public/logo.png differ diff --git a/promo-ui2/public/tac/css/tac.css b/promo-ui2/public/tac/css/tac.css new file mode 100644 index 0000000..46cb50a --- /dev/null +++ b/promo-ui2/public/tac/css/tac.css @@ -0,0 +1,7 @@ +#tianai-captcha-parent{box-shadow:0 0 11px 0 #999;width:318px;height:300px;overflow:hidden;position:relative;z-index:997;box-sizing:border-box;border-radius:5px;padding:8px}#tianai-captcha-parent #tianai-captcha-box{height:260px;width:100%;position:relative;overflow:hidden}#tianai-captcha-parent #tianai-captcha-box .loading{width:120px;height:20px;-webkit-mask:linear-gradient(90deg, #000 70%, rgba(0, 0, 0, 0) 0) 0/20%;background:linear-gradient(#f7b645 0 0) 0/0% no-repeat rgba(221,221,221,.4196078431);animation:cartoon 1s infinite steps(6);margin:120px auto}@keyframes cartoon{100%{background-size:120%}}#tianai-captcha-parent #tianai-captcha-box #tianai-captcha{transform-style:preserve-3d;will-change:transform;transition-duration:.45s;transform:translateX(-300px)}#tianai-captcha-parent #tianai-captcha-bg-img{background-color:#fff;background-position:top;background-size:cover;z-index:-1;width:100%;height:100%;top:0;left:0;position:absolute;border-radius:6px}#tianai-captcha-parent .slider-bottom{height:19px;width:100%}#tianai-captcha-parent .slider-move-shadow{animation:myanimation 2s infinite;height:100%;width:5px;background-color:#fff;position:absolute;top:0;left:0;filter:opacity(0.5);box-shadow:1px 1px 1px #fff;border-radius:50%}#tianai-captcha-parent #tianai-captcha-slider-move-track-mask{border-width:1px;border-style:solid;border-color:#00f4ab;width:0;height:32px;background-color:#a9ffe5;opacity:.5;position:absolute;top:-1px;left:-1px;border-radius:5px} +#tianai-captcha{text-align:left;box-sizing:content-box;width:300px;height:260px;z-index:999}#tianai-captcha .slider-bottom .logo{height:30px}#tianai-captcha .slider-bottom{height:19px;width:100%}#tianai-captcha .content .tianai-captcha-tips{height:25px;width:100%;position:absolute;bottom:-25px;left:0;z-index:999;font-size:15px;line-height:25px;color:#fff;text-align:center;transition:bottom .3s ease-in-out}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-error{background-color:#ff5d39}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-success{background-color:#39c522}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-on{bottom:0}#tianai-captcha .content #tianai-captcha-loading{z-index:9999;background-color:#f5f5f5;text-align:center;height:100%;overflow:hidden;position:relative;display:flex;justify-content:center;align-items:center}#tianai-captcha .content #tianai-captcha-loading img{display:block;width:45px;height:45px}#tianai-captcha #tianai-captcha-slider-bg-canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}#tianai-captcha #tianai-captcha-slider-bg-div{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}#tianai-captcha #tianai-captcha-slider-bg-div .tianai-captcha-slider-bg-div-slice{position:absolute}@keyframes myanimation{from{left:0}to{left:289px}} +#tianai-captcha.tianai-captcha-slider{z-index:999;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-slider .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-slider .bg-img-div{width:100%;height:100%;position:absolute;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .bg-img-div img{height:100%;width:100%;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-img-div{height:100%;position:absolute;left:0;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .slider-img-div #tianai-captcha-slider-move-img{height:100%}#tianai-captcha.tianai-captcha-slider .slider-move{height:34px;width:100%;margin:11px 0;position:relative}#tianai-captcha.tianai-captcha-slider .slider-move-track{position:relative;height:32px;line-height:32px;text-align:center;background:#f5f5f5;color:#999;transition:0s;font-size:14px;box-sizing:content-box;border:1px solid #f5f5f5;border-radius:4px}#tianai-captcha.tianai-captcha-slider .refresh-btn,#tianai-captcha.tianai-captcha-slider .close-btn{display:inline-block}#tianai-captcha.tianai-captcha-slider .slider-move{line-height:38px;font-size:14px;text-align:center;white-space:nowrap;color:#88949d;-moz-user-select:none;-webkit-user-select:none;user-select:none;filter:opacity(0.8)}#tianai-captcha.tianai-captcha-slider .slider-move .slider-move-btn{transform:translate(0px, 0px);position:absolute;top:-6px;left:0;width:63px;height:45px;background-color:#fff;background-repeat:no-repeat;background-size:contain;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-tip{margin-bottom:5px;font-weight:bold;font-size:15px;line-height:normal;color:#000}#tianai-captcha.tianai-captcha-slider .slider-move-btn:hover{cursor:pointer}#tianai-captcha.tianai-captcha-slider{user-select:none} +#tianai-captcha.tianai-captcha-rotate .rotate-img-div{height:100%;text-align:center}#tianai-captcha.tianai-captcha-rotate .rotate-img-div img{height:100%;transform:rotate(0deg);display:inline-block} +#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-img-div{background-size:100% 180px;position:absolute;transform:translate(0px, 0px);z-index:1;width:100%}#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-bg-img{width:100%;height:100%;position:absolute;transform:translate(0px, 0px);background-size:100% 180px} +#tianai-captcha.tianai-captcha-disable{z-index:999;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-disable .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-disable .content .bg-img-div{background-image:url(../../tac/images/dun.jpeg);width:100%;height:100%;overflow:hidden}#tianai-captcha.tianai-captcha-disable .content .bg-img-div #content-span{color:#fff;overflow:hidden;margin-top:132px;display:block;text-align:center} +#tianai-captcha.tianai-captcha-word-click{box-sizing:border-box}#tianai-captcha.tianai-captcha-word-click .click-tip{position:relative;height:40px;width:100%}#tianai-captcha.tianai-captcha-word-click .click-tip .tip-img{height:35px;position:absolute;right:15px}#tianai-captcha.tianai-captcha-word-click .click-tip #tianai-captcha-click-track-font{font-size:18px;display:inline-block;height:40px;line-height:40px;position:absolute}#tianai-captcha.tianai-captcha-word-click .slider-bottom{position:relative;top:6px}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask{width:100%;height:100%;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask .click-span{position:absolute;left:0;top:0;border-radius:50px;background-color:#409eff;width:20px;height:20px;text-align:center;line-height:20px;color:#fff;border:2px solid #fff;box-sizing:content-box}#tianai-captcha.tianai-captcha-word-click .click-confirm-btn{width:100%;height:35px;border-radius:4px;background-image:linear-gradient(173deg, hsl(38.09, 91%, 57.89%) 0%, hsl(38.09, 89.38%, 71.74%) 100%);font-size:15px;text-align:center;box-sizing:border-box;line-height:35px;color:#fff;margin-top:3px}#tianai-captcha.tianai-captcha-word-click .click-confirm-btn:hover{cursor:pointer} diff --git a/promo-ui2/public/tac/images/dun.jpeg b/promo-ui2/public/tac/images/dun.jpeg new file mode 100644 index 0000000..347a371 Binary files /dev/null and b/promo-ui2/public/tac/images/dun.jpeg differ diff --git a/promo-ui2/public/tac/js/tac.min.js b/promo-ui2/public/tac/js/tac.min.js new file mode 100644 index 0000000..22e4e19 --- /dev/null +++ b/promo-ui2/public/tac/js/tac.min.js @@ -0,0 +1 @@ +(()=>{"use strict";var t,e,a={783:(t,e,a)=>{var i=a(618),n=Object.create(null),r="undefined"==typeof document,s=Array.prototype.forEach;function c(){}function o(t,e){if(!e){if(!t.href)return;e=t.href.split("?")[0]}if(l(e)&&!1!==t.isLoaded&&e&&e.indexOf(".css")>-1){t.visited=!0;var a=t.cloneNode();a.isLoaded=!1,a.addEventListener("load",(function(){a.isLoaded||(a.isLoaded=!0,t.parentNode.removeChild(t))})),a.addEventListener("error",(function(){a.isLoaded||(a.isLoaded=!0,t.parentNode.removeChild(t))})),a.href="".concat(e,"?").concat(Date.now()),t.nextSibling?t.parentNode.insertBefore(a,t.nextSibling):t.parentNode.appendChild(a)}}function d(t){if(!t)return!1;var e=document.querySelectorAll("link"),a=!1;return s.call(e,(function(e){if(e.href){var n=function(t,e){var a;return t=i(t),e.some((function(i){t.indexOf(e)>-1&&(a=i)})),a}(e.href,t);l(n)&&!0!==e.visited&&n&&(o(e,n),a=!0)}})),a}function h(){var t=document.querySelectorAll("link");s.call(t,(function(t){!0!==t.visited&&o(t)}))}function l(t){return!!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(t)}t.exports=function(t,e){if(r)return c;var a,s,o,l=function(t){var e=n[t];if(!e){if(document.currentScript)e=document.currentScript.src;else{var a=document.getElementsByTagName("script"),r=a[a.length-1];r&&(e=r.src)}n[t]=e}return function(t){if(!e)return null;var a=e.split(/([^\\/]+)\.js$/),n=a&&a[1];return n&&t?t.split(",").map((function(t){var a=new RegExp("".concat(n,"\\.js$"),"g");return i(e.replace(a,"".concat(t.replace(/{fileName}/g,n),".css")))})):[e.replace(".js",".css")]}}(t);return a=function(){var t=d(l(e.filename));e.locals?h():t||h()},s=50,o=0,function(){var t=this,e=arguments;clearTimeout(o),o=setTimeout((function(){return a.apply(t,e)}),s)}}},618:t=>{t.exports=function(t){if(t=t.trim(),/^data:/i.test(t))return t;var e=-1!==t.indexOf("//")?t.split("//")[0]+"//":"",a=t.replace(new RegExp(e,"i"),"").split("/"),i=a[0].toLowerCase().replace(/\.$/,"");return a[0]="",e+i+a.reduce((function(t,e){switch(e){case"..":t.pop();break;case".":break;default:t.push(e)}return t}),[]).join("/")}},507:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},691:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},448:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},56:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},33:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},779:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},130:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},687:(t,e,a)=>{a(507),a(691),a(130);function i(t){t.preventDefault&&t.preventDefault()}function n(t){f(t).each((t=>{t.addEventListener("touchmove",i,{passive:!1}),t.addEventListener("mousemove",i,{passive:!1})}))}function r(t){if(null!==t.pageX&&void 0!==t.pageX)return{x:Math.round(t.pageX),y:Math.round(t.pageY)};let e;return t.changedTouches?e=t.changedTouches:t.targetTouches?e=t.targetTouches:t.originalEvent&&t.originalEvent.targetTouches&&(e=t.originalEvent.targetTouches),null!==e[0].pageX&&void 0!==e[0].pageX?{x:Math.round(e[0].pageX),y:Math.round(e[0].pageY)}:{x:Math.round(e[0].clientX),y:Math.round(e[0].clientY)}}function s(t,e){const a=r(e);let i=a.x,n=a.y;t.currentCaptchaData.startX=i,t.currentCaptchaData.startY=n;const s=t.currentCaptchaData.trackList;t.currentCaptchaData.startTime=new Date;const o=t.currentCaptchaData.startTime;s.push({x:a.x,y:a.y,type:"down",t:(new Date).getTime()-o.getTime()}),t.__m__=c.bind(null,t),t.__u__=d.bind(null,t),window.addEventListener("mousemove",t.__m__),window.addEventListener("mouseup",t.__u__),window.addEventListener("touchmove",t.__m__,!1),window.addEventListener("touchend",t.__u__,!1),t&&t.doDown&&t.doDown(e,t)}function c(t,e){e.touches&&e.touches.length>0&&(e=e.touches[0]);const a=r(e);let i=a.x,n=a.y;const s=t.currentCaptchaData.startX,c=t.currentCaptchaData.startY,o=t.currentCaptchaData.startTime,d=t.currentCaptchaData.end,h=(t.currentCaptchaData.bgImageWidth,t.currentCaptchaData.trackList);let l=i-s,p=n-c;const u={x:a.x,y:a.y,type:"move",t:(new Date).getTime()-o.getTime()};h.push(u),l<0?l=0:l>d&&(l=d),t.currentCaptchaData.moveX=l,t.currentCaptchaData.moveY=p,t.doMove&&t.doMove(e,t)}function o(t){t&&(t.__m__&&(window.removeEventListener("mousemove",t.__m__),window.removeEventListener("touchmove",t.__m__)),t.__u__&&(window.removeEventListener("mouseup",t.__u__),window.removeEventListener("touchend",t.__u__)))}function d(t,e){o(t);const a=r(e);t.currentCaptchaData.stopTime=new Date;const i=t.currentCaptchaData.startTime,n=t.currentCaptchaData.trackList;t.currentCaptchaData.endX=a.x,t.currentCaptchaData.endY=a.y;const s={x:a.x,y:a.y,type:"up",t:(new Date).getTime()-i.getTime()};n.push(s),t.doUp&&t.doUp(e,t),t.endCallback(t.currentCaptchaData,t)}function h(t,e,a,i,n){const r={startTime:new Date,trackList:[],movePercent:0,clickCount:0,bgImageWidth:Math.round(t),bgImageHeight:Math.round(e),templateImageWidth:Math.round(a),templateImageHeight:Math.round(i),end:n};return r}function l(t,e){f(t).find("#tianai-captcha-tips").removeClass("tianai-captcha-tips-on"),e&&setTimeout(e,.35)}function p(t,e,a,i){const n=f(t).find("#tianai-captcha-tips");n.text(e),1===a?(n.removeClass("tianai-captcha-tips-error"),n.addClass("tianai-captcha-tips-success")):(n.removeClass("tianai-captcha-tips-success"),n.addClass("tianai-captcha-tips-error")),n.addClass("tianai-captcha-tips-on"),setTimeout(i,1e3)}class u{showTips(t,e,a){p(this.el,t,e,a)}closeTips(t,e){l(this.el,t)}}function f(t,e){return new m(t,e)}class m{constructor(t,e){if(e&&"object"==typeof e&&void 0!==e.nodeType)return this.dom=e,void(this.domStr=t);if(t instanceof m)this.dom=t.dom,this.domStr=t.domStr;else if("string"==typeof t)this.dom=document.querySelector(t),this.domStr=t;else{if("object"!=typeof document||void 0===document.nodeType)throw new Error("不支持的类型");this.dom=t,this.domStr=t.nodeName}}each(t){this.getTarget().querySelectorAll("*").forEach(t)}removeClass(t){let e=this.getTarget();if(e.classList)e.classList.remove(t);else{const a=e.className,i=new RegExp("(?:^|\\s)"+t+"(?!\\S)","g");e.className=a.replace(i,"")}return this}addClass(t){const e=this.getTarget();if(e.classList)e.classList.add(t);else{let a=e.className;-1===a.indexOf(t)&&(e.className=a+" "+t)}return this}find(t){const e=this.getTarget().querySelector(t);return e?new m(t,e):null}children(t){const e=this.getTarget().childNodes;for(let a=0;a\n
\n ${t.i18n.slider_title}\n
\n
\n
\n \n \n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n\n`}(this.styleConfig)),this.el=this.boxEl.find("#tianai-captcha"),this.loadStyle(),this.el.find("#tianai-captcha-slider-move-btn").mousedown(s.bind(null,this)),this.el.find("#tianai-captcha-slider-move-btn").touchstart(s.bind(null,this)),this.loadCaptchaForData(this,t),this.endCallback=e,a&&a(this),this}showTips(t,e,a){p(this.el,t,e,a)}closeTips(t){l(this.el,t)}destroy(){const t=this.boxEl.children("#tianai-captcha");t&&t.remove(),o()}doMove(){const t=this.currentCaptchaData.moveX;this.el.find("#tianai-captcha-slider-move-btn").css("transform","translate("+t+"px, 0px)"),this.el.find("#tianai-captcha-slider-img-div").css("transform","translate("+t+"px, 0px)"),this.el.find("#tianai-captcha-slider-move-track-mask").css("width",t+"px")}loadStyle(){let t="",e="#00f4ab",a="#a9ffe5";const i=this.styleConfig;i&&(t=i.btnUrl,a=i.moveTrackMaskBgColor,e=i.moveTrackMaskBorderColor),this.el.find(".slider-move .slider-move-btn").css("background-image","url("+t+")"),this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color",e),this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color",a)}loadCaptchaForData(t,e){const a=t.el.find("#tianai-captcha-slider-bg-img"),i=t.el.find("#tianai-captcha-slider-move-img");a.attr("src",e.data.backgroundImage),i.attr("src",e.data.templateImage),a.on("load",(()=>{t.currentCaptchaData=h(a.width(),a.height(),i.width(),i.height(),242),t.currentCaptchaData.currentCaptchaId=e.data.id}))}};a(779);const g=class extends u{constructor(t,e){super(),this.boxEl=t,this.styleConfig=e,this.type="ROTATE",this.currentCaptchaData={}}init(t,e,a){return this.destroy(),this.boxEl.append(function(t){return`\n
\n
\n ${t.i18n.rotate_title}\n
\n
\n
\n \n \n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n`}(this.styleConfig)),this.el=this.boxEl.find("#tianai-captcha"),this.loadStyle(),this.el.find("#tianai-captcha-slider-move-btn").mousedown(s.bind(null,this)),this.el.find("#tianai-captcha-slider-move-btn").touchstart(s.bind(null,this)),this.loadCaptchaForData(this,t),this.endCallback=e,a&&a(this),this}destroy(){const t=this.boxEl.children("#tianai-captcha");t&&t.remove(),o()}doMove(){const t=this.currentCaptchaData.moveX;this.el.find("#tianai-captcha-slider-move-btn").css("transform","translate("+t+"px, 0px)"),this.el.find("#tianai-captcha-slider-move-img").css("transform","rotate("+t/(this.currentCaptchaData.end/360)+"deg)"),this.el.find("#tianai-captcha-slider-move-track-mask").css("width",t+"px")}loadStyle(){let t="",e="#00f4ab",a="#a9ffe5";const i=this.styleConfig;i&&(t=i.btnUrl,a=i.moveTrackMaskBgColor,e=i.moveTrackMaskBorderColor),this.el.find(".slider-move .slider-move-btn").css("background-image","url("+t+")"),this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color",e),this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color",a)}loadCaptchaForData(t,e){const a=t.el.find("#tianai-captcha-slider-bg-img"),i=t.el.find("#tianai-captcha-slider-move-img");a.attr("src",e.data.backgroundImage),i.attr("src",e.data.templateImage),a.on("load",(()=>{t.currentCaptchaData=h(a.width(),a.height(),i.width(),i.height(),242),t.currentCaptchaData.currentCaptchaId=e.data.id}))}};a(448);const C=class extends u{constructor(t,e){super(),this.boxEl=f(t),this.styleConfig=e,this.type="CONCAT",this.currentCaptchaData={}}init(t,e,a){return this.destroy(),this.boxEl.append((this.styleConfig,'\n
\n
\n 拖动滑块完成拼图\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n ')),this.el=this.boxEl.find("#tianai-captcha"),this.loadStyle(),this.el.find("#tianai-captcha-slider-move-btn").mousedown(s.bind(null,this)),this.el.find("#tianai-captcha-slider-move-btn").touchstart(s.bind(null,this)),n(this.el),window.currentCaptcha=this,this.loadCaptchaForData(this,t),this.endCallback=e,a&&a(this),this}destroy(){o();const t=this.boxEl.children("#tianai-captcha");t&&t.remove()}doMove(){const t=this.currentCaptchaData.moveX;this.el.find("#tianai-captcha-slider-move-btn").css("transform","translate("+t+"px, 0px)"),this.el.find("#tianai-captcha-slider-concat-img-div").css("background-position-x",t+"px"),this.el.find("#tianai-captcha-slider-move-track-mask").css("width",t+"px")}loadStyle(){let t="",e="#00f4ab",a="#a9ffe5";const i=this.styleConfig;i&&(t=i.btnUrl,a=i.moveTrackMaskBgColor,e=i.moveTrackMaskBorderColor),this.el.find(".slider-move .slider-move-btn").css("background-image","url("+t+")"),this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color",e),this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color",a)}loadCaptchaForData(t,e){const a=t.el.find(".tianai-captcha-slider-concat-bg-img"),i=t.el.find("#tianai-captcha-slider-concat-img-div");a.css("background-image","url("+e.data.backgroundImage+")"),i.css("background-image","url("+e.data.backgroundImage+")"),i.css("background-position","0px 0px");var n=e.data.backgroundImageHeight,r=(n-e.data.data.randomY)/n*180;i.css("height",r+"px"),t.currentCaptchaData=h(a.width(),a.height(),i.width(),i.height(),242),t.currentCaptchaData.currentCaptchaId=e.data.id}};a(56);const b=class{constructor(t,e){this.boxEl=t,this.styleConfig=e,this.type="DISABLE",this.currentCaptchaData={}}init(t,e,a){return this.destroy(),this.boxEl.append(function(t){return`\n
\n
\n ${t.i18n.disable_title}\n
\n
\n
\n\x3c!-- --\x3e\n\x3c!-- --\x3e\n\x3c!-- --\x3e\n\x3c!-- --\x3e\n \n
\n
\n
\n `}(this.styleConfig)),this.el=this.boxEl.find("#tianai-captcha"),this.loadCaptchaForData(this,t),this.endCallback=e,a&&a(this),this}destroy(){const t=this.boxEl.find("#tianai-captcha");t&&t.remove()}loadCaptchaForData(t,e){const a=e.msg||e.message||"接口异常";t.el.find("#content-span").text(a)}};a(33);const y=class extends u{constructor(t,e){super(),this.boxEl=t,this.styleConfig=e,this.type="IMAGE_CLICK",this.currentCaptchaData={}}init(t,e,a){this.destroy(),this.boxEl.append(function(t){return`\n
\n
\n ${t.i18n.image_click_title}\n \n
\n
\n
\n \n \n
\n
\n
\n
\n
确定
\n
\n`}(this.styleConfig)),this.el=this.boxEl.find("#tianai-captcha"),this.loadCaptchaForData(this,t),this.endCallback=e;const i=c.bind(null,this);return this.el.find("#bg-img-click-mask").click((t=>{if("click-span"===t.target.className)return;this.currentCaptchaData.clickCount++;const e=this.currentCaptchaData.trackList;1===this.currentCaptchaData.clickCount&&(this.currentCaptchaData.startTime=new Date,window.addEventListener("mousemove",i),this.currentCaptchaData.startX=t.offsetX,this.currentCaptchaData.startY=t.offsetY);const a=this.currentCaptchaData.startTime;e.push({x:Math.round(t.offsetX),y:Math.round(t.offsetY),type:"click",t:(new Date).getTime()-a.getTime()});const n=t.offsetX-10,r=t.offsetY-10;this.el.find("#bg-img-click-mask").append(""+this.currentCaptchaData.clickCount+"")})),this.el.find(".click-confirm-btn").click((()=>{this.currentCaptchaData.clickCount>0&&(this.currentCaptchaData.stopTime=new Date,window.removeEventListener("mousemove",i),this.endCallback(this.currentCaptchaData,this))})),a&&a(this),this}destroy(){const t=this.boxEl.children("#tianai-captcha");t&&t.remove(),o()}loadCaptchaForData(t,e){const a=t.el.find("#tianai-captcha-slider-bg-img"),i=t.el.find("#tianai-captcha-tip-img");a.on("load",(()=>{t.currentCaptchaData=h(a.width(),a.height(),i.width(),i.height()),t.currentCaptchaData.currentCaptchaId=e.data.id})),a.attr("src",e.data.backgroundImage),i.attr("src",e.data.templateImage)}};const w=class extends y{constructor(t,e){super(t,e),this.type="WORD_IMAGE_CLICK"}},E={btnUrl:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAABkCAYAAABU19jRAAAJcUlEQVR4nO2d63MT1xmHf9rV6mr5fgNMuSW+ENsY8N0EE2BMhinJNB8y/dD2Qz/0v+gMf0w/JHTKNJAhICwbsA02TpNAHEMgQIwNBSEb8F2rvXTeY1kjYyA+TmVJmfeZ8YiRWa9299E57/mdI63Dtm3E+RjAKTDMaj4F8AU9uyzMCQBn+EQxb+EjAF+RMH8AcJrPFLMGvCSMzWeKWSN/I2GiAFx8xpi1oPBZYiTQWRhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGCiefrtShGwZiup74+4qqwu12Z/W7lIVJEfN6FDfv3sPXfYOIRRfpm1UQKC7EkQ+PYFtRcdZKw8KkiLsPJ/CfgSFcH7yOxWhU7MSluYQoR44fxdaCoqyUhoVJEfZ8FN99c1N0Sx6PR+zEMAz0XAgBNtB14hi25OXDkWXHxUVvinA4ln6ScTqdsGwbvRd7EPwyiEcvXyDbvpyHhUkRaq4fe/c3wEWSWFZiJySNYZroCYYQPHsBY1OTWSWNevLkyb/TYwa8lt8UAb8ftluDW9UwPj4hDs0Rb3JUVRXd09j9nwELKKgoR4HXlw2Hb3INkyK8mob9NdUwLROq4sCVKwMrdqRpGkzTFN0TaWR2HcKu0rKMr2lYmBTi1jS01dUt7UBx4PKlfvHP5JaGuqseIY0DjmOHsKukNKOPiYVJMU5VRXt9PSwboO+fvHJ5QEiiKEvlIz3S86HuHiiqAhw9iJ0lpRnb0rAwG4CqKHh/Tz0UhwOWaWGg/5oofEkmJLU4wfPdQia765CQJhNHJCzMBkEtSVtdLRw2YNo2hgaGEDMMMWpahrwJBUMUCkM9djgjE2EWZgOhFqW5rlbMKdm2heHBYUT1mCiAEW9pKKfpPh8Sj5mYCLMwG4zLqWJfTZWQgL5S++uhYURjBrR4S0MtUSYnwixMGvBoGvZUV4quh0S4Pjgsaho1XtOIcM8wxJCb+qmu33dljDS/CWEeTb/E/Pw89EUdebkBVBQWrnnbWVjQoMAtsT9asGDQhf8VUbnX5UJ9VaVoZahVuXZ1cMXoiaSJxWIiEab/dPj4UXFczjRrk/VJ70/hp/jhuxF89o9TGP1+FH6fD9OxGHw5Pnicb34/PJ2dweitu7hwLojvb47A9rhQmJeXGLm8iQeP/4uRH27h88/+iZhhYs40UFZQsK7XrqkqigvyYbk18VrHH74+EX74YAzRqI66mupE15UmzKwW5kEkgtFvRxA8ex7hJ2HMzczgzu0f8fjxExRt2YzcgB9udfUJjuo6Tv/7HE6f+pe4GHd//AkwLRhuDeXFRW+U5v7EI4yMjKI3GMLt0Tt4cO8BAoEcWJoTZYXrl6asqBC6U0GOy42HY+MrZi1JmoWFRZQW5sNyuVBeUpxOabJ7aiASjiB4/iKmnj+H5loaacwvLOL2jRF4AjnY8dc/I/DKbTdoSHvr8SO8DD/DzPSMWHrg1JwYvHZdpK2NVZWU26/aF3VDTyLP0N/bh4mJR3C7XZiZnRVdht/nx7u7tsOzzg5qORFWHAocigO9vX2Jronwej24cXMEbq8XrfW169rH/4usnq02o1FEo9FEE47luN22sTAzC0OPrd7ItnHn9h0MDg3D6/WKbZdHJqYRg26ar92XDgvD39zA2Ng4VKdTbEf7mpmeRX/fAPRfeRch+luNNTXICeSu+h3ti7okUzdgp3luO6uFUTUN9lLmnniOCkdKVnML8uB0r76rD72Di4qL4NI0IUnydpZlw/WmGsY00bRvDzZvKhfFKLAU9VOG8v7BdijW+i8kLX649yyMz0+fwVQksur3NILyejzw5efCoaT3kmW1MN68AMq2bBIXXtd18WMZBt6r242DBzvgda3uWhQ4xNzOkeNdohZYXFjA4vwCfD4/Sio2i9bjdeSoGirKylFYXirykehiFHpUR2FJCbZu+x1yXlMrrQWSZWwygv6Ll3DxXBCX+66u6I7o2DRFRWtbM1o62xNdb7rI7lGSqqBs+zZMTj4XLYY/x49t7+zABx8eReWO7ciLL41ctZmqoqRiE/x+P6amp5FbkI9jx7tw+GgncqmbesPuPAEfduzcgenZOTg0FaWby/GXP/0RdZXvrOvlkyzjzyfR81UIoQs9IpRJniqglszt0tDc1oS9bc2o37lTLMhKI2bW35HtRXQRs3MLmH/xUrzzVb8HJQUFyHX/crJCQ+JwOALFqaKspGjNRWtkbg5zc7PQXC5szl/f6Ig6MFqiSavuqHCmumuFLIYBt+ZEY0sTGtua0VBTJQK/NKPzLfzSQEKWL4NiiG5a1gpZzPhMdnNrE/a3N2NPVaUI+jIAnacGNhiShdbx9pzrFgunSA4tqeUQRbuqoLW9BQ0tjSINzhBZBCzMBvPzVAS950KiG6KWJVkWGnXRELrjQBtqG/eioTqzZAELs3FQy3Iv/BR9wUtiUtGOr+tNhoptGt1V7atD4+4aEehlGizMBnH/WRj9wcuiG7LjI7Vllm8d3nnoAKoaakXq+0tzWumChUkxdlyWge4rYt0uzRMpSck01SzUDR3s7MC7e2pFRqSmOZx7GyxMCrESLcsldAd7oCgrEx6xrldRRM1SvbceHfV1K0K7TISFSREx28L41KRIcGmdruOVz82KBFd1oqWjBe/tb0ArLd3McFnAwqSOiclJ9JwP4fLFXtEtJXdDywluU2uTGDpTgZupNcur8GerU8R0eBJDV6+LRVbJLYdIcF2aSHD3tzaL9b20zjdbYGFShB0z4HY6V9QtFNLRXFATxf2U4FZXZkLcLwULkyJoaUXMNMV6HbyS4O6jicQMS3DXCguTInJKC9HU0YoPOg8k1uy0t7eivnmfSHB9WSgLwZOPKcKwLcT0GL69cxe3b46KoK6+ZS92V2zNyAR3jfBsdaox6LPSpiVyf/rEo/rq11JlFzxbnWoomEMW5CtrhWsYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgr6qGx6b4/BZBXUwnzCl4xZI5844g3MCQBn+Kwxb+EjAGcdST3SxwBO8RljXsOnAL4AgP8BXnVIgIvemwsAAAAASUVORK5CYII=",moveTrackMaskBgColor:"#89d2ff",moveTrackMaskBorderColor:"#0298f8",i18n:{tips_success:"验证成功,耗时%s秒",tips_error:"验证失败,请重新尝试!",slider_title:"拖动滑块完成拼图",concat_title:"拖动滑块完成拼图",image_click_title:"请依次点击:",rotate_title:"拖动滑块完成拼图",slider_title_size:"15px",concat_title_size:"15px",image_click_title_size:"20px",rotate_title_size:"15px"}};class k{constructor(t){if(!t.bindEl)throw new Error("[TAC] 必须配置 [bindEl]用于将验证码绑定到该元素上");if(!t.requestCaptchaDataUrl)throw new Error("[TAC] 必须配置 [requestCaptchaDataUrl]请求验证码接口");if(!t.validCaptchaUrl)throw new Error("[TAC] 必须配置 [validCaptchaUrl]验证验证码接口");this.bindEl=t.bindEl,this.domBindEl=f(t.bindEl),this.requestCaptchaDataUrl=t.requestCaptchaDataUrl,this.validCaptchaUrl=t.validCaptchaUrl,this.requestData=t.requestData||{},t.validSuccess&&(this.validSuccess=t.validSuccess),t.validFail&&(this.validFail=t.validFail),t.requestHeaders?this.requestHeaders=t.requestHeaders:this.requestHeaders={},this.requestChain=[],this.timeToTimestamp=t.timeToTimestamp||!0,this.insertRequestChain(0,{preRequest(t,e,a,i){if(this.timeToTimestamp&&e.data)for(let t in e.data)e.data[t]instanceof Date&&(e.data[t]=e.data[t].getTime());return!0}})}addRequestChain(t){this.requestChain.push(t)}insertRequestChain(t,e){this.requestChain.splice(t,0,e)}removeRequestChain(t){this.requestChain.splice(t,1)}requestCaptchaData(){const t={};t.headers=this.requestHeaders||{},t.data=this.requestData||{},this.requestData?.phoneNumber&&(t.data.phoneNumber=this.requestData.phoneNumber),t.headers["Content-Type"]="application/json;charset=UTF-8",t.method="POST",t.url=this.requestCaptchaDataUrl,this._preRequest("requestCaptchaData",t);return this.doSendRequest(t).then((e=>(this._postRequest("requestCaptchaData",t,e),e)))}doSendRequest(t){if(t.headers)for(const e in t.headers)if(t.headers[e].indexOf("application/json")>-1){"string"!=typeof t.data&&(t.data=JSON.stringify(t.data));break}return(e=t,new Promise((function(t,a){var i=new XMLHttpRequest;if(i.open(e.method||"GET",e.url),e.headers)for(const t in e.headers)e.headers.hasOwnProperty(t)&&i.setRequestHeader(t,e.headers[t]);i.onreadystatechange=function(){if(i.readyState===XMLHttpRequest.DONE)if(i.status>=200&&i.status<=500){const e=i.getResponseHeader("Content-Type");e&&-1!==e.indexOf("application/json")?t(JSON.parse(i.responseText)):t(i.responseText)}else a(new Error("Request failed with status: "+i.status))},i.onerror=function(){a(new Error("Network Error"))},i.send(e.data)}))).then((t=>{try{return JSON.parse(t)}catch(e){return t}}));var e}_preRequest(t,e,a,i){for(let n=0;n(this._postRequest("validCaptcha",r,t,a,i),t))).then((t=>{if(200==t.code){const n=(e.stopTime-e.startTime)/1e3;a.showTips(`验证成功,耗时${n}秒`,1,(()=>this.validSuccess(t,a,i)))}else{let e="验证失败,请重新尝试!";t.code&&4001!=t.code&&(e="验证码被黑洞吸走了!"),a.showTips(e,0),i.reloadCaptcha()}})).catch((t=>{const e=a.styleConfig.i18n.tips_error;a.showTips(e,0),i.reloadCaptcha()}))}validSuccess(t,e,a){window.currentCaptchaRes=t,a.destroyWindow()}validFail(t,e,a){a.reloadCaptcha()}}window.TAC=class{constructor(t,e){this.config=function(t){return t instanceof k?t:new k(t)}(t),this.style=function(t){let e={...E,...t};return e.i18n={...E.i18n,...t?.i18n},e}(e)}init(){return this.destroyWindow(),this.config.domBindEl.append('\n
\n
\n
\n
\n
\n
\n '),this.domTemplate=this.config.domBindEl.find("#tianai-captcha-parent"),n(this.domTemplate),this.loadStyle(),this.reloadCaptcha(),this}reloadCaptcha(){this.showLoading(),this.destroyCaptcha((()=>{this.createCaptcha()}))}showLoading(){this.config.domBindEl.find("#tianai-captcha-loading").css("display","block")}closeLoading(){this.config.domBindEl.find("#tianai-captcha-loading").css("display","none")}loadStyle(){const t=this.style.bgUrl;t&&this.config.domBindEl.find("#tianai-captcha-bg-img").css("background-image","url("+t+")")}destroyWindow(){this.C&&(this.C.destroy(),this.C=void 0),this.domTemplate&&this.domTemplate.remove()}openCaptcha(){setTimeout((()=>{this.C.el.css("transform","translateX(0)")}),10)}createCaptcha(){this.config.requestCaptchaData().then((t=>{if(this.closeLoading(),!t.code)throw new Error("[TAC] 后台验证码接口数据错误!!!");let e=200===t.code?t.data?.type:"DISABLED";const a=function(t,e){const a=e.config.domBindEl.find("#tianai-captcha-box"),i=e.style;switch(t){case"SLIDER":return new v(a,i);case"ROTATE":return new g(a,i);case"CONCAT":return new C(a,i);case"WORD_IMAGE_CLICK":return new w(a,i);case"DISABLED":return new b(a,i);default:return null}}(e,this);if(null==a)throw new Error("[TAC] 未知的验证码类型["+e+"]");a.init(t,((t,e)=>{const a=e.currentCaptchaData,i={bgImageWidth:a.bgImageWidth,bgImageHeight:a.bgImageHeight,templateImageWidth:a.templateImageWidth,templateImageHeight:a.templateImageHeight,startTime:a.startTime.getTime(),stopTime:a.stopTime.getTime(),trackList:a.trackList,startX:a.startX,startY:a.startY,endX:a.endX,endY:a.endY};"ROTATE_DEGREE"!==e.type&&"ROTATE"!==e.type||(i.bgImageWidth=e.currentCaptchaData.end),a.data&&(i.data=a.data);const n=e.currentCaptchaData.currentCaptchaId;e.currentCaptchaData=void 0;const r=this.config.validCaptcha(n,i,e,this);r&&"function"==typeof r.then&&r.catch((t=>{setTimeout((()=>{this.reloadCaptcha()}),500)}))})),this.C=a,this.openCaptcha()}))}destroyCaptcha(t){this.C?(this.C.el.css("transform","translateX(300px)"),setTimeout((()=>{this.C.destroy(),t&&t()}),500)):t()}},window.CaptchaConfig=k}},i={};function n(t){var e=i[t];if(void 0!==e){if(void 0!==e.error)throw e.error;return e.exports}var r=i[t]={id:t,exports:{}};try{var s={id:t,module:r,factory:a[t],require:n};n.i.forEach((function(t){t(s)})),r=s.module,s.factory.call(r.exports,r,r.exports,s.require)}catch(t){throw r.error=t,t}return r.exports}n.m=a,n.c=i,n.i=[],n.hu=t=>t+"."+n.h()+".hot-update.js",n.miniCssF=t=>{},n.hmrF=()=>"main."+n.h()+".hot-update.json",n.h=()=>"6aac32209b04c3e7224a",n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),t={},e="webpack-demo:",n.l=(a,i,r,s)=>{if(t[a])t[a].push(i);else{var c,o;if(void 0!==r)for(var d=document.getElementsByTagName("script"),h=0;h{c.onerror=c.onload=null,clearTimeout(u);var n=t[a];if(delete t[a],c.parentNode&&c.parentNode.removeChild(c),n&&n.forEach((t=>t(i))),e)return e(i)},u=setTimeout(p.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=p.bind(null,c.onerror),c.onload=p.bind(null,c.onload),o&&document.head.appendChild(c)}},(()=>{var t,e,a,i={},r=n.c,s=[],c=[],o="idle",d=0,h=[];function l(t){o=t;for(var e=[],a=0;a0)return l("abort").then((function(){throw n[0]}));var r=l("dispose");i.forEach((function(t){t.dispose&&t.dispose()}));var s,c=l("apply"),o=function(t){s||(s=t)},d=[];return i.forEach((function(t){if(t.apply){var e=t.apply(o);if(e)for(var a=0;a=0&&C._disposeHandlers.splice(e,1)},invalidate:function(){switch(this._selfInvalidated=!0,o){case"idle":e=[],Object.keys(n.hmrI).forEach((function(t){n.hmrI[t](m,e)})),l("ready");break;case"ready":Object.keys(n.hmrI).forEach((function(t){n.hmrI[t](m,e)}));break;case"prepare":case"check":case"dispose":case"apply":(a=a||[]).push(m)}},check:u,apply:f,status:function(t){if(!t)return o;c.push(t)},addStatusHandler:function(t){c.push(t)},removeStatusHandler:function(t){var e=c.indexOf(t);e>=0&&c.splice(e,1)},data:i[m]},t=void 0,C),b.parents=s,b.children=[],s=[],h.require=y})),n.hmrC={},n.hmrI={}})(),(()=>{var t;n.g.importScripts&&(t=n.g.location+"");var e=n.g.document;if(!t&&e&&(e.currentScript&&(t=e.currentScript.src),!t)){var a=e.getElementsByTagName("script");if(a.length)for(var i=a.length-1;i>-1&&!t;)t=a[i--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),n.p=t+"../../"})(),(()=>{if("undefined"!=typeof document){var t=(t,e,a,i,n)=>{var r=document.createElement("link");r.rel="stylesheet",r.type="text/css";return r.onerror=r.onload=a=>{if(r.onerror=r.onload=null,"load"===a.type)i();else{var s=a&&("load"===a.type?"missing":a.type),c=a&&a.target&&a.target.href||e,o=new Error("Loading CSS chunk "+t+" failed.\n("+c+")");o.code="CSS_CHUNK_LOAD_FAILED",o.type=s,o.request=c,r.parentNode&&r.parentNode.removeChild(r),n(o)}},r.href=e,a?a.parentNode.insertBefore(r,a.nextSibling):document.head.appendChild(r),r},e=(t,e)=>{for(var a=document.getElementsByTagName("link"),i=0;i({dispose:()=>{for(var t=0;t{for(var t=0;t{h.push(r),s.forEach((r=>{var s=n.miniCssF(r),c=n.p+s,o=e(s,c);o&&d.push(new Promise(((e,n)=>{var s=t(r,c,o,(()=>{s.as="style",s.rel="preload",e()}),n);a.push(o),i.push(s)})))}))}}})(),(()=>{var t,e,a,i,r,s=n.hmrS_jsonp=n.hmrS_jsonp||{179:0},c={};function o(e,a){return t=a,new Promise(((t,a)=>{c[e]=t;var i=n.p+n.hu(e),r=new Error;n.l(i,(t=>{if(c[e]){c[e]=void 0;var i=t&&("load"===t.type?"missing":t.type),n=t&&t.target&&t.target.src;r.message="Loading hot update chunk "+e+" failed.\n("+i+": "+n+")",r.name="ChunkLoadError",r.type=i,r.request=n,a(r)}}))}))}function d(t){function c(t){for(var e=[t],a={},i=e.map((function(t){return{chain:[t],id:t}}));i.length>0;){var r=i.pop(),s=r.id,c=r.chain,d=n.c[s];if(d&&(!d.hot._selfAccepted||d.hot._selfInvalidated)){if(d.hot._selfDeclined)return{type:"self-declined",chain:c,moduleId:s};if(d.hot._main)return{type:"unaccepted",chain:c,moduleId:s};for(var h=0;h ")),f.type){case"self-declined":t.onDeclined&&t.onDeclined(f),t.ignoreDeclined||(v=new Error("Aborted because of self decline: "+f.moduleId+b));break;case"declined":t.onDeclined&&t.onDeclined(f),t.ignoreDeclined||(v=new Error("Aborted because of declined dependency: "+f.moduleId+" in "+f.parentId+b));break;case"unaccepted":t.onUnaccepted&&t.onUnaccepted(f),t.ignoreUnaccepted||(v=new Error("Aborted because "+u+" is not accepted"+b));break;case"accepted":t.onAccepted&&t.onAccepted(f),g=!0;break;case"disposed":t.onDisposed&&t.onDisposed(f),C=!0;break;default:throw new Error("Unexception type "+f.type)}if(v)return{error:v};if(g)for(u in l[u]=m,o(h,f.outdatedModules),f.outdatedDependencies)n.o(f.outdatedDependencies,u)&&(d[u]||(d[u]=[]),o(d[u],f.outdatedDependencies[u]));C&&(o(h,[f.moduleId]),l[u]=p)}a=void 0;for(var y,w=[],E=0;E0;){var r=a.pop(),c=n.c[r];if(c){var o={},l=c.hot._disposeHandlers;for(E=0;E=0&&p.parents.splice(t,1))}}}for(var u in d)if(n.o(d,u)&&(c=n.c[u]))for(y=d[u],E=0;E=0&&c.children.splice(t,1)},apply:function(e){for(var a in l)n.o(l,a)&&(n.m[a]=l[a]);for(var i=0;i{for(var o in i)n.o(i,o)&&(a[o]=i[o],t&&t.push(o));s&&r.push(s),c[e]&&(c[e](),c[e]=void 0)},n.hmrI.jsonp=function(t,e){a||(a={},r=[],i=[],e.push(d)),n.o(a,t)||(a[t]=n.m[t])},n.hmrC.jsonp=function(t,c,h,l,p,u){p.push(d),e={},i=c,a=h.reduce((function(t,e){return t[e]=!1,t}),{}),r=[],t.forEach((function(t){n.o(s,t)&&void 0!==s[t]?(l.push(o(t,u)),e[t]=!0):e[t]=!1})),n.f&&(n.f.jsonpHmr=function(t,a){e&&n.o(e,t)&&!e[t]&&(a.push(o(t)),e[t]=!0)})},n.hmrM=()=>{if("undefined"==typeof fetch)throw new Error("No browser support: need fetch API");return fetch(n.p+n.hmrF()).then((t=>{if(404!==t.status){if(!t.ok)throw new Error("Failed to fetch update manifest "+t.statusText);return t.json()}}))}})();n(687)})(); \ No newline at end of file diff --git a/promo-ui2/scripts/icon.ts b/promo-ui2/scripts/icon.ts new file mode 100644 index 0000000..724342e --- /dev/null +++ b/promo-ui2/scripts/icon.ts @@ -0,0 +1,73 @@ +import path from 'path' +import fs from 'fs-extra' +import inquirer from 'inquirer' +import chalk from 'chalk' +import pkg from '../package.json' +import { ICON_PREFIX } from '../src/constants' + +interface Icon { + name: string + prefix: string + icons: string[] +} + +async function generateIcon() { + const dir = path.resolve(process.cwd(), 'node_modules/@iconify/json') + + const raw = await fs.readJSON(path.join(dir, 'collections.json')) + + const collections = Object.entries(raw).map(([id, v]) => ({ + ...(v as any), + id + })) + + const choices = collections.map((item) => ({ key: item.id, value: item.id, name: item.name })) + + inquirer + .prompt([ + // { + // type: 'list', + // name: 'useType', + // choices: [ + // { key: 'local', value: 'local', name: 'Local' }, + // { key: 'onLine', value: 'onLine', name: 'OnLine' } + // ], + // message: 'How to use icons?' + // }, + { + type: 'list', + name: 'iconSet', + choices: choices, + message: 'Select the icon set that needs to be generated?' + } + ]) + // ↓命令行问答的答案 + .then(async (answers) => { + const { iconSet } = answers + // const isOnLine = useType === 'onLine' + const outputDir = path.resolve(process.cwd(), 'src/components/IconPicker/src/data') + fs.ensureDir(outputDir) + const genCollections = collections.filter((item) => [iconSet].includes(item.id)) + const prefixSet: string[] = [] + for (const info of genCollections) { + const data = await fs.readJSON(path.join(dir, 'json', `${info.id}.json`)) + if (data) { + const { prefix } = data + const prefixName = `${ICON_PREFIX}${prefix}` + const icons = Object.keys(data.icons).map((item) => `${prefixName}:${item}`) + + await fs.writeFileSync( + path.join('src/components/IconPicker/src/data', `icons.${prefix}.ts`), + `export default ${JSON.stringify({ name: info.name, prefix: prefixName, icons })}` + ) + // ↓分类处理完成,push类型名称 + prefixSet.push(prefix) + } + } + console.log( + `✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - Icon generated successfully:' + `[${prefixSet}]` + ) + }) +} + +generateIcon() diff --git a/promo-ui2/src/App.vue b/promo-ui2/src/App.vue new file mode 100644 index 0000000..9ea48b6 --- /dev/null +++ b/promo-ui2/src/App.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/promo-ui2/src/api/adminAccount/index.ts b/promo-ui2/src/api/adminAccount/index.ts new file mode 100644 index 0000000..5e29707 --- /dev/null +++ b/promo-ui2/src/api/adminAccount/index.ts @@ -0,0 +1,24 @@ +import request from '@/axios' + +export interface PromoAdminAccount { + id?: number + username: string + password?: string + nickName: string + phoneNumber?: string + email?: string + status?: string + createTime?: string +} + +export function getAdminListApi() { + return request.get({ url: '/promo/adminManage/adminList' }) +} + +export function saveAdminApi(data: PromoAdminAccount) { + return request.post({ url: '/promo/adminManage/saveAdmin', data }) +} + +export function deleteAdminApi(id: number) { + return request.post({ url: `/promo/adminManage/deleteAdmin/${id}` }) +} diff --git a/promo-ui2/src/api/adminManage/index.ts b/promo-ui2/src/api/adminManage/index.ts new file mode 100644 index 0000000..a49f115 --- /dev/null +++ b/promo-ui2/src/api/adminManage/index.ts @@ -0,0 +1,16 @@ +import request from '@/axios' + +// 招商管理列表 +export const getBusinessList = () => { + return request.get({ url: '/promo/adminManage/businessList' }) +} + +// 主播管理列表 +export const getAnchorList = () => { + return request.get({ url: '/promo/adminManage/anchorList' }) +} + +// 卡商管理列表 +export const getCardMerchantList = () => { + return request.get({ url: '/promo/adminManage/cardMerchantList' }) +} diff --git a/promo-ui2/src/api/cardMerchant/index.ts b/promo-ui2/src/api/cardMerchant/index.ts new file mode 100644 index 0000000..d58dddb --- /dev/null +++ b/promo-ui2/src/api/cardMerchant/index.ts @@ -0,0 +1,41 @@ +import request from '@/axios' + +/** + * 获取卡商信息(名称、余额) + */ +export function getMerchantInfo() { + return request.get({ url: '/promo/cardKey/merchantInfo' }) +} + +/** + * 获取充値金额列表 + */ +export function getRechargeList() { + return request.get({ url: '/promo/cardKey/rechargeList' }) +} + +/** + * 生成卡密 + */ +export function generateCardKeys(data: { amount: number; num: number }) { + return request.post({ url: '/promo/cardKey/generate', data }) +} + +/** + * 销毁未使用卡密(软删除并退回卡商余额) + */ +export function destroyCardKey(data: { id: number }) { + return request.post({ url: '/promo/cardKey/destroy', data }) +} + +/** + * 获取卡密列表(分页) + */ +export function getCardKeyList(params?: { + usedByName?: string + creatorName?: string + pageNum?: number + pageSize?: number +}) { + return request.get({ url: '/promo/cardKey/list', params }) +} diff --git a/promo-ui2/src/api/cdkey/index.ts b/promo-ui2/src/api/cdkey/index.ts new file mode 100644 index 0000000..54124b7 --- /dev/null +++ b/promo-ui2/src/api/cdkey/index.ts @@ -0,0 +1,51 @@ +import request from '@/axios' + +/** + * 生成CDKEY(仅主播账号可操作) + */ +export function generateCdkeys(data) { + return request.post({ + url: '/promo/cdkey/generate', + data + }) +} + +/** + * 获取创建CDKEY记录列表 + */ +export function getCdkeyCreateList(params) { + return request.get({ + url: '/promo/cdkey/createList', + params + }) +} + +/** + * 获取兑换CDKEY记录列表 + */ +export function getCdkeyRedeemList(params) { + return request.get({ + url: '/promo/cdkey/redeemList', + params + }) +} + +/** + * 创建CDKEY日报 + */ +export function getCdkeyCreateDaily(params) { + return request.get({ + url: '/promo/cdkey/createDaily', + params + }) +} + +/** + * 兑换CDKEY日报 + */ +export function getCdkeyRedeemDaily(params) { + return request.get({ + url: '/promo/cdkey/redeemDaily', + params + }) +} diff --git a/promo-ui2/src/api/common/index.ts b/promo-ui2/src/api/common/index.ts new file mode 100644 index 0000000..b8c986c --- /dev/null +++ b/promo-ui2/src/api/common/index.ts @@ -0,0 +1,11 @@ +import request from '@/axios' + +// 获取所有字典 +export const getDictApi = () => { + return request.get({ url: '/mock/dict/list' }) +} + +// 模拟获取某个字典 +export const getDictOneApi = async () => { + return request.get({ url: '/mock/dict/one' }) +} diff --git a/promo-ui2/src/api/dashboard/analysis/index.ts b/promo-ui2/src/api/dashboard/analysis/index.ts new file mode 100644 index 0000000..f559dc9 --- /dev/null +++ b/promo-ui2/src/api/dashboard/analysis/index.ts @@ -0,0 +1,23 @@ +import request from '@/axios' +import type { + AnalysisTotalTypes, + UserAccessSource, + WeeklyUserActivity, + MonthlySales +} from './types' + +export const getCountApi = (): Promise> => { + return request.get({ url: '/mock/analysis/total' }) +} + +export const getUserAccessSourceApi = (): Promise> => { + return request.get({ url: '/mock/analysis/userAccessSource' }) +} + +export const getWeeklyUserActivityApi = (): Promise> => { + return request.get({ url: '/mock/analysis/weeklyUserActivity' }) +} + +export const getMonthlySalesApi = (): Promise> => { + return request.get({ url: '/mock/analysis/monthlySales' }) +} diff --git a/promo-ui2/src/api/dashboard/analysis/types.ts b/promo-ui2/src/api/dashboard/analysis/types.ts new file mode 100644 index 0000000..4a239f5 --- /dev/null +++ b/promo-ui2/src/api/dashboard/analysis/types.ts @@ -0,0 +1,22 @@ +export type AnalysisTotalTypes = { + users: number + messages: number + moneys: number + shoppings: number +} + +export type UserAccessSource = { + value: number + name: string +} + +export type WeeklyUserActivity = { + value: number + name: string +} + +export type MonthlySales = { + name: string + estimate: number + actual: number +} diff --git a/promo-ui2/src/api/dashboard/workplace/index.ts b/promo-ui2/src/api/dashboard/workplace/index.ts new file mode 100644 index 0000000..062f351 --- /dev/null +++ b/promo-ui2/src/api/dashboard/workplace/index.ts @@ -0,0 +1,22 @@ +import request from '@/axios' +import type { WorkplaceTotal, Project, Dynamic, Team, RadarData } from './types' + +export const getCountApi = (): Promise> => { + return request.get({ url: '/mock/workplace/total' }) +} + +export const getProjectApi = (): Promise> => { + return request.get({ url: '/mock/workplace/project' }) +} + +export const getDynamicApi = (): Promise> => { + return request.get({ url: '/mock/workplace/dynamic' }) +} + +export const getTeamApi = (): Promise> => { + return request.get({ url: '/mock/workplace/team' }) +} + +export const getRadarApi = (): Promise> => { + return request.get({ url: '/mock/workplace/radar' }) +} diff --git a/promo-ui2/src/api/dashboard/workplace/types.ts b/promo-ui2/src/api/dashboard/workplace/types.ts new file mode 100644 index 0000000..da23e74 --- /dev/null +++ b/promo-ui2/src/api/dashboard/workplace/types.ts @@ -0,0 +1,30 @@ +export type WorkplaceTotal = { + project: number + access: number + todo: number +} + +export type Project = { + name: string + icon: string + message: string + personal: string + time: Date | number | string +} + +export type Dynamic = { + keys: string[] + time: Date | number | string +} + +export type Team = { + name: string + icon: string +} + +export type RadarData = { + personal: number + team: number + max: number + name: string +} diff --git a/promo-ui2/src/api/department/index.ts b/promo-ui2/src/api/department/index.ts new file mode 100644 index 0000000..eb583ac --- /dev/null +++ b/promo-ui2/src/api/department/index.ts @@ -0,0 +1,30 @@ +import request from '@/axios' +import { DepartmentListResponse, DepartmentUserParams, DepartmentUserResponse } from './types' + +export const getDepartmentApi = () => { + return request.get({ url: '/mock/department/list' }) +} + +export const getUserByIdApi = (params: DepartmentUserParams) => { + return request.get({ url: '/mock/department/users', params }) +} + +export const deleteUserByIdApi = (ids: string[] | number[]) => { + return request.post({ url: '/mock/department/user/delete', data: { ids } }) +} + +export const saveUserApi = (data: any) => { + return request.post({ url: '/mock/department/user/save', data }) +} + +export const saveDepartmentApi = (data: any) => { + return request.post({ url: '/mock/department/save', data }) +} + +export const deleteDepartmentApi = (ids: string[] | number[]) => { + return request.post({ url: '/mock/department/delete', data: { ids } }) +} + +export const getDepartmentTableApi = (params: any) => { + return request.get({ url: '/mock/department/table/list', params }) +} diff --git a/promo-ui2/src/api/department/types.ts b/promo-ui2/src/api/department/types.ts new file mode 100644 index 0000000..027a628 --- /dev/null +++ b/promo-ui2/src/api/department/types.ts @@ -0,0 +1,32 @@ +export interface DepartmentItem { + id: string + departmentName: string + children?: DepartmentItem[] +} + +export interface DepartmentListResponse { + list: DepartmentItem[] +} + +export interface DepartmentUserParams { + pageSize: number + pageIndex: number + id: string + username?: string + account?: string +} + +export interface DepartmentUserItem { + id: string + username: string + account: string + email: string + createTime: string + role: string + department: DepartmentItem +} + +export interface DepartmentUserResponse { + list: DepartmentUserItem[] + total: number +} diff --git a/promo-ui2/src/api/login/index.ts b/promo-ui2/src/api/login/index.ts new file mode 100644 index 0000000..ccc43f6 --- /dev/null +++ b/promo-ui2/src/api/login/index.ts @@ -0,0 +1,59 @@ +import request from '@/axios' +import type { RegisterUserType, SmsLoginType, UserLoginType, UserType } from './types' + +interface RoleParams { + roleName: string +} + +export const loginApi = (data: UserLoginType): Promise> => { + return request.post({ url: '/promo/login', data }) +} + +export const smsLoginApi = (data: SmsLoginType): Promise> => { + return request.post({ url: '/promo/smsLogin', data }) +} + +export const registerApi = (data: RegisterUserType): Promise> => { + return request.post({ url: '/promo/register', data }) +} + +export const getCodeApi = ( + phoneNumber: string, + captchaId: string, + type: '1' | '2' = '1', + parentInvitationCode?: string +): Promise> => { + return request.post({ + url: '/promo/sms/getVerifyCode', + data: { + phoneNumber: phoneNumber, + type, + captchaId: captchaId, + parentInvitationCode + } + }) +} + +export const loginOutApi = (): Promise => { + return request.get({ url: '/mock/user/loginOut' }) +} + +export const getUserListApi = ({ params }: AxiosConfig) => { + return request.get<{ + code: string + data: { + list: UserType[] + total: number + } + }>({ url: '/mock/user/list', params }) +} + +export const getAdminRoleApi = ( + params: RoleParams +): Promise> => { + return request.get({ url: '/mock/role/list', params }) +} + +export const getTestRoleApi = (params: RoleParams): Promise> => { + return request.get({ url: '/mock/role/list2', params }) +} diff --git a/promo-ui2/src/api/login/types.ts b/promo-ui2/src/api/login/types.ts new file mode 100644 index 0000000..a8a9830 --- /dev/null +++ b/promo-ui2/src/api/login/types.ts @@ -0,0 +1,36 @@ +export interface UserLoginType { + username: string + password: string +} + +export interface SmsLoginType { + username: string + code: string + /** 上级邀请码:手机号未注册时用于自动注册为主播 */ + parentInvitationCode?: string +} + +export interface UserType { + uid: number + username: string + password: string + role: string + roleId: string + token: string + shareLink: string + shareDomain: string +} + +export interface RegisterUserType { + username: string + password: string + check_password: string + contactInformation: string + parentInvitationCode: string + userType: string +} + +export interface ApiSmsBodyType { + phoneNumber: string + type: string +} diff --git a/promo-ui2/src/api/menu/index.ts b/promo-ui2/src/api/menu/index.ts new file mode 100644 index 0000000..a2b934c --- /dev/null +++ b/promo-ui2/src/api/menu/index.ts @@ -0,0 +1,5 @@ +import request from '@/axios' + +export const getMenuListApi = () => { + return request.get({ url: '/mock/menu/list' }) +} diff --git a/promo-ui2/src/api/permission/index.ts b/promo-ui2/src/api/permission/index.ts new file mode 100644 index 0000000..1c667b5 --- /dev/null +++ b/promo-ui2/src/api/permission/index.ts @@ -0,0 +1,29 @@ +import request from '@/axios' + +/** + * 获取当前用户的菜单权限(根据用户类型) + */ +export function getMyMenus() { + return request.get({ url: '/promo/menu/getMenus' }) +} + +/** + * 超管:获取某角色类型已配置的菜单 + */ +export function getRoleMenus(roleType: string) { + return request.get({ url: `/promo/menu/getRoleMenus/${roleType}` }) +} + +/** + * 超管:保存角色类型的菜单权限 + */ +export function saveRoleMenus(data: { roleType: string; menuPaths: string[] }) { + return request.post({ url: '/promo/menu/saveRoleMenus', data }) +} + +/** + * 超管:获取所有可分配的菜单配置(从数据库读) + */ +export function getAllMenuConfigs() { + return request.get({ url: '/promo/menu/allMenuConfigs' }) +} diff --git a/promo-ui2/src/api/request/index.ts b/promo-ui2/src/api/request/index.ts new file mode 100644 index 0000000..207f913 --- /dev/null +++ b/promo-ui2/src/api/request/index.ts @@ -0,0 +1,38 @@ +import request from '@/axios' +import { RequestResponse } from './types' + +export const request1 = () => { + return request.get>({ + url: '/mock/request/1' + }) +} + +export const request2 = () => { + return request.get>({ + url: '/mock/request/2' + }) +} + +export const request3 = () => { + return request.get>({ + url: '/mock/request/3' + }) +} + +export const request4 = () => { + return request.get>({ + url: '/mock/request/4' + }) +} + +export const request5 = () => { + return request.get>({ + url: '/mock/request/5' + }) +} + +export const expired = () => { + return request.get>({ + url: '/mock/request/expired' + }) +} diff --git a/promo-ui2/src/api/request/types.ts b/promo-ui2/src/api/request/types.ts new file mode 100644 index 0000000..9a2063f --- /dev/null +++ b/promo-ui2/src/api/request/types.ts @@ -0,0 +1,3 @@ +export interface RequestResponse { + data: string +} diff --git a/promo-ui2/src/api/role/index.ts b/promo-ui2/src/api/role/index.ts new file mode 100644 index 0000000..ecbf9d9 --- /dev/null +++ b/promo-ui2/src/api/role/index.ts @@ -0,0 +1,5 @@ +import request from '@/axios' + +export const getRoleListApi = () => { + return request.get({ url: '/mock/role/table' }) +} diff --git a/promo-ui2/src/api/roll/index.ts b/promo-ui2/src/api/roll/index.ts new file mode 100644 index 0000000..26b23dc --- /dev/null +++ b/promo-ui2/src/api/roll/index.ts @@ -0,0 +1,203 @@ +import request from '@/axios' + +/** + * 获取奖池列表(下拉选择用) + */ +export function getJackpotList(params?) { + return request.get({ + url: '/promo/roll/jackpotList', + params + }) +} + +/** + * 搜索可用饰品 + */ +export function searchOrnaments(params?) { + return request.get({ + url: '/promo/roll/ornaments', + params + }) +} + +/** + * 获取奖池饰品列表 + */ +export function getJackpotOrnaments(jackpotId: number | string) { + return request.get({ + url: `/promo/roll/jackpotOrnaments/${jackpotId}` + }) +} + +/** + * 向奖池添加饰品 + */ +export function addOrnaments(data: { jackpotId: number; ornamentIds: number[] }) { + return request.post({ + url: '/promo/roll/addOrnaments', + data + }) +} + +/** + * 从奖池移除饰品 + */ +export function removeOrnament(id: number | string) { + return request.delete({ + url: `/promo/roll/removeOrnament/${id}` + }) +} + +/** + * 更新奖池饰品数量 + */ +export function updateOrnamentNum(data: { id: number; ornamentsNum: number }) { + return request.put({ + url: '/promo/roll/updateOrnamentNum', + data + }) +} + +/** + * Roll房列表 + */ +export function getRollList(params?) { + return request.get({ + url: '/promo/roll/list', + params + }) +} + +/** + * Roll房详情 + */ +export function getRollInfo(rollId: number | string) { + return request.get({ + url: `/promo/roll/${rollId}` + }) +} + +/** + * 新增Roll房 + */ +export function createRoll(data) { + return request.post({ + url: '/promo/roll', + data + }) +} + +/** + * 修改Roll房 + */ +export function updateRoll(data) { + return request.put({ + url: '/promo/roll', + data + }) +} + +/** + * 删除Roll房 + */ +export function deleteRoll(rollId: number | string) { + return request.delete({ + url: `/promo/roll/${rollId}` + }) +} + +/** + * Roll房用户列表 + */ +export function getRollUsers(rollId: number | string) { + return request.get({ + url: `/promo/roll/users/${rollId}` + }) +} + +/** + * Roll房中奖名单(参与用户中了什么饰品) + */ +export function getRollWinners(rollId: number | string) { + return request.get({ + url: `/promo/roll/winners/${rollId}` + }) +} + +/** + * 指定奖池饰品获奖用户 + */ +export function designateOrnamentUser(data: { id: number; designatedUserId: string | null }) { + return request.put({ + url: '/promo/roll/designateOrnamentUser', + data + }) +} + +// ==================== 奖项模板 ==================== + +/** + * 获取奖项模板列表 + */ +export function getPrizeTemplateList() { + return request.get({ + url: '/promo/roll/prizeTemplates' + }) +} + +/** + * 获取奖项模板详情 + */ +export function getPrizeTemplateDetail(templateId: number | string) { + return request.get({ + url: `/promo/roll/prizeTemplate/${templateId}` + }) +} + +/** + * 创建奖项模板 + */ +export function createPrizeTemplate(data: { templateName: string; ornaments: any[] }) { + return request.post({ + url: '/promo/roll/prizeTemplate', + data + }) +} + +/** + * 修改奖项模板 + */ +export function updatePrizeTemplate(data: { id: number; templateName: string; ornaments: any[] }) { + return request.put({ + url: '/promo/roll/prizeTemplate', + data + }) +} + +/** + * 删除奖项模板 + */ +export function deletePrizeTemplate(templateId: number | string) { + return request.delete({ + url: `/promo/roll/prizeTemplate/${templateId}` + }) +} + +/** + * 按价格区间搜索饰品 + */ +export function searchOrnamentsByPrice(params: { minPrice?: number; maxPrice: number; name?: string; pageNum?: number; pageSize?: number }) { + return request.get({ + url: '/promo/roll/ornamentsByPrice', + params + }) +} + +/** + * 获取固定金币饰品列表(唯一Hash名以 jinbi- 开头) + */ +export function getCoinOrnaments() { + return request.get({ + url: '/promo/roll/coinOrnaments' + }) +} diff --git a/promo-ui2/src/api/subflow/index.ts b/promo-ui2/src/api/subflow/index.ts new file mode 100644 index 0000000..526faf5 --- /dev/null +++ b/promo-ui2/src/api/subflow/index.ts @@ -0,0 +1,36 @@ +import request from '@/axios' +import { getPurchaseByUserIdRequest, getSubBranchesRequest } from './types' + +export function getSubBranches(req: getSubBranchesRequest) { + return request.get({ + url: '/promo/turnover/getSubBranches', + params: req + }) +} + +export function getPurchaseByUserId(req: getPurchaseByUserIdRequest) { + return request.get({ + url: '/promo/turnover/getPurchaseByUserId/' + req.userId, + params: req + }) +} + +export function getCommissionRateByUserId(userId) { + return request.get({ + url: '/promo/turnover/getCommissionRateByUserId/' + userId + }) +} + +export function updateCommissionRate(data) { + return request.put({ + url: '/promo/turnover/updateCommissionRate', + data: data + }) +} + +export function getFlowRecord(params?: { userId?: number | string }) { + return request.get({ + url: '/promo/turnover/flowRecord', + params + }) +} diff --git a/promo-ui2/src/api/subflow/types.ts b/promo-ui2/src/api/subflow/types.ts new file mode 100644 index 0000000..8412662 --- /dev/null +++ b/promo-ui2/src/api/subflow/types.ts @@ -0,0 +1,14 @@ +export interface getPurchaseByUserIdRequest { + pageNum: number + pageSize: number + beginTime: undefined | string + endTime: string | undefined + userId: undefined | string | number +} + +export interface getSubBranchesRequest { + pageNum: number + pageSize: number + userId: undefined | string | number + playerId: undefined | string | number +} diff --git a/promo-ui2/src/api/table/index.ts b/promo-ui2/src/api/table/index.ts new file mode 100644 index 0000000..c870f25 --- /dev/null +++ b/promo-ui2/src/api/table/index.ts @@ -0,0 +1,26 @@ +import request from '@/axios' +import type { TableData } from './types' + +export const getTableListApi = (params: any) => { + return request.get({ url: '/mock/example/list', params }) +} + +export const getCardTableListApi = (params: any) => { + return request.get({ url: '/mock/card/list', params }) +} + +export const getTreeTableListApi = (params: any) => { + return request.get({ url: '/mock/example/treeList', params }) +} + +export const saveTableApi = (data: Partial): Promise => { + return request.post({ url: '/mock/example/save', data }) +} + +export const getTableDetApi = (id: string): Promise> => { + return request.get({ url: '/mock/example/detail', params: { id } }) +} + +export const delTableListApi = (ids: string[] | number[]): Promise => { + return request.post({ url: '/mock/example/delete', data: { ids } }) +} diff --git a/promo-ui2/src/api/table/types.ts b/promo-ui2/src/api/table/types.ts new file mode 100644 index 0000000..876c111 --- /dev/null +++ b/promo-ui2/src/api/table/types.ts @@ -0,0 +1,9 @@ +export type TableData = { + id: string + author: string + title: string + content: string + importance: number + display_time: string + pageviews: number +} diff --git a/promo-ui2/src/api/turnover/index.ts b/promo-ui2/src/api/turnover/index.ts new file mode 100644 index 0000000..a3e1c1d --- /dev/null +++ b/promo-ui2/src/api/turnover/index.ts @@ -0,0 +1,96 @@ +import request from '@/axios' + +export function getRealTimeData() { + return request.get({ + url: '/promo/turnover/getRealTimeData' + }) +} + +export function getGameCommissionData() { + return request.get({ + url: '/promo/turnover/getGameCommissionData' + }) +} + +export function getLast10DaysPromotionData() { + return request.get({ + url: '/promo/turnover/getLast10DaysPromotionData' + }) +} + +export function getAnchorPromotionData(query) { + return request.get({ + url: '/promo/turnover/getAnchorPromotionData', + params: query + }) +} + +export function queryPromoteWelfare(query) { + return request.get({ + url: '/promo/turnover/queryPromoteWelfare', + params: query + }) +} + +export function getMySubordinates(params?: { account?: string }) { + return request.get({ + url: '/promo/turnover/mySubordinates', + params + }) +} + +export function removeSubordinate(params?: { subUserId?: number }) { + return request.post({ + url: '/promo/turnover/removeSubordinate', + params + }) +} + +export function getFlowRecord(params?: { userId?: number; startDate?: string; endDate?: string }) { + return request.get({ + url: '/promo/turnover/flowRecord', + params + }) +} + +export function getSettledTotalCommission(params?: { startDate?: string; endDate?: string }) { + return request.get({ + url: '/promo/turnover/settledTotalCommission', + params + }) +} + +export function getWeeklyDailyCommission(params?: { startDate?: string; endDate?: string }) { + return request.get({ + url: '/promo/turnover/weeklyDailyCommission', + params + }) +} + +export function getCommissionDetail(params?: { date?: string; subUserId?: number }) { + return request.get({ + url: '/promo/turnover/commissionDetail', + params + }) +} + +export function getCommissionDaily(params?: { date?: string; subUserId?: number }) { + return request.get({ + url: '/promo/turnover/commissionDaily', + params + }) +} + +export function getCommissionSummary(params?: { startDate?: string; endDate?: string; subUserId?: number }) { + return request.get({ + url: '/promo/turnover/commissionSummary', + params + }) +} + +export function getSubordinateRecharge(params?: { month?: string; subUserId?: number; phone?: string }) { + return request.get({ + url: '/promo/turnover/subordinateRecharge', + params + }) +} diff --git a/promo-ui2/src/api/user/index.ts b/promo-ui2/src/api/user/index.ts new file mode 100644 index 0000000..a3d7213 --- /dev/null +++ b/promo-ui2/src/api/user/index.ts @@ -0,0 +1,61 @@ +import request from '@/axios' + +export function getInfo() { + return request.post({ + url: '/promo/getInfo' + }) +} + +export function updateProfile(data: { nickName: string; phoneNumber?: string; email?: string }) { + return request.post({ + url: '/promo/updateProfile', + data + }) +} + +export function updatePassword(data: { oldPassword: string; newPassword: string }) { + return request.post({ + url: '/promo/updatePassword', + data + }) +} + +export function updateAvatar(data: { avatar: string }) { + return request.post({ + url: '/promo/updateAvatar', + data + }) +} + +// 招商/主播/卡商的头像走 App 那套上传接口(存 URL,不是 base64) +export function uploadProfilePicture(data: FormData) { + return request.post({ + url: '/api/user/profilePictureUpload', + data + }) +} + +export function updateInvitationCode(data: any) { + return request.post({ + url: '/promo/updateInvitationCode', + data + }) +} + +export function getShareDomain() { + return request.get({ + url: '/promo/getsharedomain' + }) +} + +export function getRechargeListApi() { + return request.get({ + url: '/promo/card/list' + }) +} + +export function generateCardListApi(id: number, num: number) { + return request.post({ + url: `/promo/generateCard/${id}/${num}` + }) +} diff --git a/promo-ui2/src/assets/imgs/avatar.jpg b/promo-ui2/src/assets/imgs/avatar.jpg new file mode 100644 index 0000000..d46a70a Binary files /dev/null and b/promo-ui2/src/assets/imgs/avatar.jpg differ diff --git a/promo-ui2/src/assets/imgs/logo.png b/promo-ui2/src/assets/imgs/logo.png new file mode 100644 index 0000000..b76c71b Binary files /dev/null and b/promo-ui2/src/assets/imgs/logo.png differ diff --git a/promo-ui2/src/assets/imgs/personal-center-bg.jpg b/promo-ui2/src/assets/imgs/personal-center-bg.jpg new file mode 100644 index 0000000..b8ce80b Binary files /dev/null and b/promo-ui2/src/assets/imgs/personal-center-bg.jpg differ diff --git a/promo-ui2/src/assets/svgs/403.svg b/promo-ui2/src/assets/svgs/403.svg new file mode 100644 index 0000000..4500596 --- /dev/null +++ b/promo-ui2/src/assets/svgs/403.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/assets/svgs/404.svg b/promo-ui2/src/assets/svgs/404.svg new file mode 100644 index 0000000..5244d8d --- /dev/null +++ b/promo-ui2/src/assets/svgs/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/assets/svgs/500.svg b/promo-ui2/src/assets/svgs/500.svg new file mode 100644 index 0000000..9c02092 --- /dev/null +++ b/promo-ui2/src/assets/svgs/500.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/assets/svgs/icon.svg b/promo-ui2/src/assets/svgs/icon.svg new file mode 100644 index 0000000..7024bec --- /dev/null +++ b/promo-ui2/src/assets/svgs/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/assets/svgs/login-bg.svg b/promo-ui2/src/assets/svgs/login-bg.svg new file mode 100644 index 0000000..bbe06c1 --- /dev/null +++ b/promo-ui2/src/assets/svgs/login-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/assets/svgs/login-box-bg.svg b/promo-ui2/src/assets/svgs/login-box-bg.svg new file mode 100644 index 0000000..ab10040 --- /dev/null +++ b/promo-ui2/src/assets/svgs/login-box-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/assets/svgs/message.svg b/promo-ui2/src/assets/svgs/message.svg new file mode 100644 index 0000000..14ca817 --- /dev/null +++ b/promo-ui2/src/assets/svgs/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/assets/svgs/money.svg b/promo-ui2/src/assets/svgs/money.svg new file mode 100644 index 0000000..c1580de --- /dev/null +++ b/promo-ui2/src/assets/svgs/money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/assets/svgs/peoples.svg b/promo-ui2/src/assets/svgs/peoples.svg new file mode 100644 index 0000000..aab852e --- /dev/null +++ b/promo-ui2/src/assets/svgs/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/assets/svgs/shopping.svg b/promo-ui2/src/assets/svgs/shopping.svg new file mode 100644 index 0000000..f395bc7 --- /dev/null +++ b/promo-ui2/src/assets/svgs/shopping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/promo-ui2/src/axios/config.ts b/promo-ui2/src/axios/config.ts new file mode 100644 index 0000000..ef9faad --- /dev/null +++ b/promo-ui2/src/axios/config.ts @@ -0,0 +1,105 @@ +import { AxiosResponse, InternalAxiosRequestConfig } from './types' +import { ElMessage } from 'element-plus' +import qs from 'qs' +import { SUCCESS_CODE, TRANSFORM_REQUEST_DATA } from '@/constants' +import { useUserStoreWithOut } from '@/store/modules/user' +import { objToFormData } from '@/utils' +import { aesEncrypt, aesDecrypt, AES_ENABLED } from '@/utils/aes' + +const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => { + // POST/PUT + application/json -> AES 加密请求体(需 AES 开关开启) + if ( + AES_ENABLED && + (config.method === 'post' || config.method === 'put') && + config.headers['Content-Type'] === 'application/json' + ) { + const url = config.url as string + // 对/promo/*和/admin/*路径的请求体进行 AES 加密 + if (url.startsWith('/promo/') || url.startsWith('/admin/')) { + console.log('[AES] 检测到管理后台接口,准备加密:', url) + try { + const encrypted = aesEncrypt(config.data) // 返回纯 Base64 字符串 + config.data = encrypted + // ❗关键:保持 Content-Type=application/json(后端解密后按 JSON 解析成对象), + // 但用自定义 transformRequest 阻止 axios 对 Base64 字符串再次 JSON.stringify(避免加上引号) + config.transformRequest = [(data) => data] + console.log('[AES] 请求体已加密,密文前 50 位:', (encrypted as string).substring(0, 50) + '...') + } catch (e) { + console.error('[AES] 加密失败:', e) + return config + } + } + } + + // 非 JSON 请求的处理 + if ( + config.method === 'post' && + config.headers['Content-Type'] === 'application/x-www-form-urlencoded' + ) { + config.data = qs.stringify(config.data) + } else if ( + TRANSFORM_REQUEST_DATA && + config.method === 'post' && + config.headers['Content-Type'] === 'multipart/form-data' && + !(config.data instanceof FormData) + ) { + config.data = objToFormData(config.data) + } + + // GET 请求的参数处理 + if (config.method === 'get' && config.params) { + let url = config.url as string + url += '?' + const keys = Object.keys(config.params) + for (const key of keys) { + if (config.params[key] !== void 0 && config.params[key] !== null) { + url += `${key}=${encodeURIComponent(config.params[key])}&` + } + } + url = url.substring(0, url.length - 1) + config.params = {} + config.url = url + } + + return config +} + +const defaultResponseInterceptors = (response: AxiosResponse) => { + if (response?.config?.responseType === 'blob') { + // 如果是文件流,直接过 + return response + } + + const url = response.config.url || '' + // 对/promo/*和/admin/*路径的加密响应进行 AES 解密(响应体为 Base64 字符串,非对象) + if ( + AES_ENABLED && + (url.startsWith('/promo/') || url.startsWith('/admin/')) && + typeof response.data === 'string' + ) { + try { + const plainText = aesDecrypt(response.data) + response.data = JSON.parse(plainText) + console.log('[AES] 响应体已解密,原始数据:', response.data) + } catch (e) { + console.error('[AES] 响应解密失败,回退明文:', e) + } + } + + if (response.data.code === SUCCESS_CODE || response.data.code === 200) { + return response.data + } else { + if (response?.data?.msg) { + ElMessage.error(response?.data?.msg) + } else { + ElMessage.error(response?.data?.message) + } + + if (response?.data?.code === 401) { + const userStore = useUserStoreWithOut() + userStore.logout() + } + } +} + +export { defaultResponseInterceptors, defaultRequestInterceptors } diff --git a/promo-ui2/src/axios/index.ts b/promo-ui2/src/axios/index.ts new file mode 100644 index 0000000..60b46c0 --- /dev/null +++ b/promo-ui2/src/axios/index.ts @@ -0,0 +1,42 @@ +import service from './service' +import { CONTENT_TYPE } from '@/constants' +import { useUserStoreWithOut } from '@/store/modules/user' + +const request = (option: AxiosConfig) => { + const { url, method, params, data, headers, responseType } = option + + const userStore = useUserStoreWithOut() + return service.request({ + url: url, + method, + params, + data: data, + responseType: responseType, + headers: { + 'Content-Type': CONTENT_TYPE, + [userStore.getTokenKey ?? 'Authorization']: userStore.getToken ?? '', + ...headers + } + }) +} + +export default { + get: (option: AxiosConfig) => { + return request({ method: 'get', ...option }) as Promise> + }, + post: (option: AxiosConfig) => { + return request({ method: 'post', ...option }) as Promise> + }, + delete: (option: AxiosConfig) => { + return request({ method: 'delete', ...option }) as Promise> + }, + put: (option: AxiosConfig) => { + return request({ method: 'put', ...option }) as Promise> + }, + cancelRequest: (url: string | string[]) => { + return service.cancelRequest(url) + }, + cancelAllRequest: () => { + return service.cancelAllRequest() + } +} diff --git a/promo-ui2/src/axios/service.ts b/promo-ui2/src/axios/service.ts new file mode 100644 index 0000000..7b0edc5 --- /dev/null +++ b/promo-ui2/src/axios/service.ts @@ -0,0 +1,76 @@ +import axios, { AxiosError } from 'axios' +import { defaultRequestInterceptors, defaultResponseInterceptors } from './config' +import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './types' +import { ElMessage } from 'element-plus' +import { REQUEST_TIMEOUT } from '@/constants' + +export const PATH_URL = import.meta.env.VITE_API_BASE_PATH + +const abortControllerMap: Map = new Map() + +const axiosInstance: AxiosInstance = axios.create({ + timeout: REQUEST_TIMEOUT, + baseURL: PATH_URL +}) + +axiosInstance.interceptors.request.use((res: InternalAxiosRequestConfig) => { + const controller = new AbortController() + const url = res.url || '' + res.signal = controller.signal + abortControllerMap.set( + import.meta.env.VITE_USE_MOCK === 'true' ? url.replace('/mock', '') : url, + controller + ) + return res +}) + +axiosInstance.interceptors.response.use( + (res: AxiosResponse) => { + const url = res.config.url || '' + abortControllerMap.delete(url) + // 这里不能做任何处理,否则后面的 interceptors 拿不到完整的上下文了 + return res + }, + (error: AxiosError) => { + console.log('err: ' + error) // for debug + ElMessage.error(error.message) + return Promise.reject(error) + } +) + +axiosInstance.interceptors.request.use(defaultRequestInterceptors) +axiosInstance.interceptors.response.use(defaultResponseInterceptors) + +const service = { + request: (config: RequestConfig) => { + return new Promise((resolve, reject) => { + if (config.interceptors?.requestInterceptors) { + config = config.interceptors.requestInterceptors(config as any) + } + + axiosInstance + .request(config) + .then((res) => { + resolve(res) + }) + .catch((err: any) => { + reject(err) + }) + }) + }, + cancelRequest: (url: string | string[]) => { + const urlList = Array.isArray(url) ? url : [url] + for (const _url of urlList) { + abortControllerMap.get(_url)?.abort() + abortControllerMap.delete(_url) + } + }, + cancelAllRequest() { + for (const [_, controller] of abortControllerMap) { + controller.abort() + } + abortControllerMap.clear() + } +} + +export default service diff --git a/promo-ui2/src/axios/types/index.ts b/promo-ui2/src/axios/types/index.ts new file mode 100644 index 0000000..b70eb26 --- /dev/null +++ b/promo-ui2/src/axios/types/index.ts @@ -0,0 +1,31 @@ +import type { + InternalAxiosRequestConfig, + AxiosResponse, + AxiosRequestConfig, + AxiosInstance, + AxiosRequestHeaders, + AxiosError +} from 'axios' + +interface RequestInterceptors { + // 请求拦截 + requestInterceptors?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig + requestInterceptorsCatch?: (err: any) => any + // 响应拦截 + responseInterceptors?: (config: T) => T + responseInterceptorsCatch?: (err: any) => any +} + +interface RequestConfig extends AxiosRequestConfig { + interceptors?: RequestInterceptors +} + +export { + AxiosResponse, + RequestInterceptors, + RequestConfig, + AxiosInstance, + InternalAxiosRequestConfig, + AxiosRequestHeaders, + AxiosError +} diff --git a/promo-ui2/src/components/Avatars/index.ts b/promo-ui2/src/components/Avatars/index.ts new file mode 100644 index 0000000..5eaa329 --- /dev/null +++ b/promo-ui2/src/components/Avatars/index.ts @@ -0,0 +1,4 @@ +import Avatars from './src/Avatars.vue' + +export type { AvatarItem } from './src/types' +export { Avatars } diff --git a/promo-ui2/src/components/Avatars/src/Avatars.vue b/promo-ui2/src/components/Avatars/src/Avatars.vue new file mode 100644 index 0000000..1f3619c --- /dev/null +++ b/promo-ui2/src/components/Avatars/src/Avatars.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/promo-ui2/src/components/Avatars/src/types/index.ts b/promo-ui2/src/components/Avatars/src/types/index.ts new file mode 100644 index 0000000..c2554ee --- /dev/null +++ b/promo-ui2/src/components/Avatars/src/types/index.ts @@ -0,0 +1,4 @@ +export interface AvatarItem { + url: string + name?: string +} diff --git a/promo-ui2/src/components/Backtop/index.ts b/promo-ui2/src/components/Backtop/index.ts new file mode 100644 index 0000000..96de88d --- /dev/null +++ b/promo-ui2/src/components/Backtop/index.ts @@ -0,0 +1,3 @@ +import Backtop from './src/Backtop.vue' + +export { Backtop } diff --git a/promo-ui2/src/components/Backtop/src/Backtop.vue b/promo-ui2/src/components/Backtop/src/Backtop.vue new file mode 100644 index 0000000..4619017 --- /dev/null +++ b/promo-ui2/src/components/Backtop/src/Backtop.vue @@ -0,0 +1,15 @@ + + + diff --git a/promo-ui2/src/components/Breadcrumb/index.ts b/promo-ui2/src/components/Breadcrumb/index.ts new file mode 100644 index 0000000..93ffe70 --- /dev/null +++ b/promo-ui2/src/components/Breadcrumb/index.ts @@ -0,0 +1,3 @@ +import Breadcrumb from './src/Breadcrumb.vue' + +export { Breadcrumb } diff --git a/promo-ui2/src/components/Breadcrumb/src/Breadcrumb.vue b/promo-ui2/src/components/Breadcrumb/src/Breadcrumb.vue new file mode 100644 index 0000000..cc79f1b --- /dev/null +++ b/promo-ui2/src/components/Breadcrumb/src/Breadcrumb.vue @@ -0,0 +1,126 @@ + + + diff --git a/promo-ui2/src/components/Breadcrumb/src/helper.ts b/promo-ui2/src/components/Breadcrumb/src/helper.ts new file mode 100644 index 0000000..690cff9 --- /dev/null +++ b/promo-ui2/src/components/Breadcrumb/src/helper.ts @@ -0,0 +1,30 @@ +import { pathResolve } from '@/utils/routerHelper' + +export const filterBreadcrumb = ( + routes: AppRouteRecordRaw[], + parentPath = '' +): AppRouteRecordRaw[] => { + const res: AppRouteRecordRaw[] = [] + + for (const route of routes) { + const meta = route?.meta + if (meta.hidden && !meta.canTo) { + continue + } + + const data: AppRouteRecordRaw = + !meta.alwaysShow && route.children?.length === 1 + ? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) } + : { ...route } + + data.path = pathResolve(parentPath, data.path) + + if (data.children) { + data.children = filterBreadcrumb(data.children, data.path) + } + if (data) { + res.push(data) + } + } + return res +} diff --git a/promo-ui2/src/components/Button/index.ts b/promo-ui2/src/components/Button/index.ts new file mode 100644 index 0000000..6ae43b1 --- /dev/null +++ b/promo-ui2/src/components/Button/index.ts @@ -0,0 +1,3 @@ +import BaseButton from './src/Button.vue' + +export { BaseButton } diff --git a/promo-ui2/src/components/Button/src/Button.vue b/promo-ui2/src/components/Button/src/Button.vue new file mode 100644 index 0000000..709f4a7 --- /dev/null +++ b/promo-ui2/src/components/Button/src/Button.vue @@ -0,0 +1,121 @@ + + + diff --git a/promo-ui2/src/components/CodeEditor/index.ts b/promo-ui2/src/components/CodeEditor/index.ts new file mode 100644 index 0000000..5bf3e9e --- /dev/null +++ b/promo-ui2/src/components/CodeEditor/index.ts @@ -0,0 +1,3 @@ +import CodeEditor from './src/CodeEditor.vue' + +export { CodeEditor } diff --git a/promo-ui2/src/components/CodeEditor/src/CodeEditor.vue b/promo-ui2/src/components/CodeEditor/src/CodeEditor.vue new file mode 100644 index 0000000..3ec2495 --- /dev/null +++ b/promo-ui2/src/components/CodeEditor/src/CodeEditor.vue @@ -0,0 +1,119 @@ + + + diff --git a/promo-ui2/src/components/CodeEditor/src/config/config.ts b/promo-ui2/src/components/CodeEditor/src/config/config.ts new file mode 100644 index 0000000..4fa7b30 --- /dev/null +++ b/promo-ui2/src/components/CodeEditor/src/config/config.ts @@ -0,0 +1,129 @@ +export const languageOptions = [ + { label: 'plaintext', value: 'plaintext' }, + { label: 'abap', value: 'abap' }, + { label: 'apex', value: 'apex' }, + { label: 'azcli', value: 'azcli' }, + { label: 'bat', value: 'bat' }, + { label: 'bicep', value: 'bicep' }, + { label: 'cameligo', value: 'cameligo' }, + { label: 'clojure', value: 'clojure' }, + { label: 'coffeescript', value: 'coffeescript' }, + { label: 'c', value: 'c' }, + { label: 'cpp', value: 'cpp' }, + { label: 'csharp', value: 'csharp' }, + { label: 'csp', value: 'csp' }, + { label: 'css', value: 'css' }, + { label: 'cypher', value: 'cypher' }, + { label: 'dart', value: 'dart' }, + { label: 'dockerfile', value: 'dockerfile' }, + { label: 'ecl', value: 'ecl' }, + { label: 'elixir', value: 'elixir' }, + { label: 'flow9', value: 'flow9' }, + { label: 'fsharp', value: 'fsharp' }, + { label: 'freemarker2', value: 'freemarker2' }, + { + label: 'freemarker2.tag-angle.interpolation-dollar', + value: 'freemarker2.tag-angle.interpolation-dollar' + }, + { + label: 'freemarker2.tag-bracket.interpolation-dollar', + value: 'freemarker2.tag-bracket.interpolation-dollar' + }, + { + label: 'freemarker2.tag-angle.interpolation-bracket', + value: 'freemarker2.tag-angle.interpolation-bracket' + }, + { + label: 'freemarker2.tag-bracket.interpolation-bracket', + value: 'freemarker2.tag-bracket.interpolation-bracket' + }, + { + label: 'freemarker2.tag-auto.interpolation-dollar', + value: 'freemarker2.tag-auto.interpolation-dollar' + }, + { + label: 'freemarker2.tag-auto.interpolation-bracket', + value: 'freemarker2.tag-auto.interpolation-bracket' + }, + { label: 'go', value: 'go' }, + { label: 'graphql', value: 'graphql' }, + { label: 'handlebars', value: 'handlebars' }, + { label: 'hcl', value: 'hcl' }, + { label: 'html', value: 'html' }, + { label: 'ini', value: 'ini' }, + { label: 'java', value: 'java' }, + { label: 'javascript', value: 'javascript' }, + { label: 'julia', value: 'julia' }, + { label: 'kotlin', value: 'kotlin' }, + { label: 'less', value: 'less' }, + { label: 'lexon', value: 'lexon' }, + { label: 'lua', value: 'lua' }, + { label: 'liquid', value: 'liquid' }, + { label: 'm3', value: 'm3' }, + { label: 'markdown', value: 'markdown' }, + { label: 'mdx', value: 'mdx' }, + { label: 'mips', value: 'mips' }, + { label: 'msdax', value: 'msdax' }, + { label: 'mysql', value: 'mysql' }, + { label: 'objective-c', value: 'objective-c' }, + { label: 'pascal', value: 'pascal' }, + { label: 'pascaligo', value: 'pascaligo' }, + { label: 'perl', value: 'perl' }, + { label: 'pgsql', value: 'pgsql' }, + { label: 'php', value: 'php' }, + { label: 'pla', value: 'pla' }, + { label: 'postiats', value: 'postiats' }, + { label: 'powerquery', value: 'powerquery' }, + { label: 'powershell', value: 'powershell' }, + { label: 'proto', value: 'proto' }, + { label: 'pug', value: 'pug' }, + { label: 'python', value: 'python' }, + { label: 'qsharp', value: 'qsharp' }, + { label: 'r', value: 'r' }, + { label: 'razor', value: 'razor' }, + { label: 'redis', value: 'redis' }, + { label: 'redshift', value: 'redshift' }, + { label: 'restructuredtext', value: 'restructuredtext' }, + { label: 'ruby', value: 'ruby' }, + { label: 'rust', value: 'rust' }, + { label: 'sb', value: 'sb' }, + { label: 'scala', value: 'scala' }, + { label: 'scheme', value: 'scheme' }, + { label: 'scss', value: 'scss' }, + { label: 'shell', value: 'shell' }, + { label: 'sol', value: 'sol' }, + { label: 'aes', value: 'aes' }, + { label: 'sparql', value: 'sparql' }, + { label: 'sql', value: 'sql' }, + { label: 'st', value: 'st' }, + { label: 'swift', value: 'swift' }, + { label: 'systemverilog', value: 'systemverilog' }, + { label: 'verilog', value: 'verilog' }, + { label: 'tcl', value: 'tcl' }, + { label: 'twig', value: 'twig' }, + { label: 'typescript', value: 'typescript' }, + { label: 'vb', value: 'vb' }, + { label: 'wgsl', value: 'wgsl' }, + { label: 'xml', value: 'xml' }, + { label: 'yaml', value: 'yaml' }, + { label: 'json', value: 'json' } +] + +export const themeOptions = [ + { + label: 'vs', + value: 'vs' + }, + { + label: 'vs-dark', + value: 'vs-dark' + }, + { + label: 'hc-black', + value: 'hc-black' + }, + { + label: 'hc-light', + value: 'hc-light' + } +] diff --git a/promo-ui2/src/components/Collapse/index.ts b/promo-ui2/src/components/Collapse/index.ts new file mode 100644 index 0000000..73f65a3 --- /dev/null +++ b/promo-ui2/src/components/Collapse/index.ts @@ -0,0 +1,3 @@ +import Collapse from './src/Collapse.vue' + +export { Collapse } diff --git a/promo-ui2/src/components/Collapse/src/Collapse.vue b/promo-ui2/src/components/Collapse/src/Collapse.vue new file mode 100644 index 0000000..542d9d3 --- /dev/null +++ b/promo-ui2/src/components/Collapse/src/Collapse.vue @@ -0,0 +1,34 @@ + + + diff --git a/promo-ui2/src/components/ConfigGlobal/index.ts b/promo-ui2/src/components/ConfigGlobal/index.ts new file mode 100644 index 0000000..eaeb7d0 --- /dev/null +++ b/promo-ui2/src/components/ConfigGlobal/index.ts @@ -0,0 +1,5 @@ +import ConfigGlobal from './src/ConfigGlobal.vue' + +export type { ConfigGlobalTypes } from './src/types' + +export { ConfigGlobal } diff --git a/promo-ui2/src/components/ConfigGlobal/src/ConfigGlobal.vue b/promo-ui2/src/components/ConfigGlobal/src/ConfigGlobal.vue new file mode 100644 index 0000000..5bd90cf --- /dev/null +++ b/promo-ui2/src/components/ConfigGlobal/src/ConfigGlobal.vue @@ -0,0 +1,62 @@ + + + diff --git a/promo-ui2/src/components/ConfigGlobal/src/types/index.ts b/promo-ui2/src/components/ConfigGlobal/src/types/index.ts new file mode 100644 index 0000000..d41e4d3 --- /dev/null +++ b/promo-ui2/src/components/ConfigGlobal/src/types/index.ts @@ -0,0 +1,5 @@ +import { ComponentSize } from 'element-plus' + +export interface ConfigGlobalTypes { + size?: ComponentSize +} diff --git a/promo-ui2/src/components/ContentDetailWrap/index.ts b/promo-ui2/src/components/ContentDetailWrap/index.ts new file mode 100644 index 0000000..1871cac --- /dev/null +++ b/promo-ui2/src/components/ContentDetailWrap/index.ts @@ -0,0 +1,3 @@ +import ContentDetailWrap from './src/ContentDetailWrap.vue' + +export { ContentDetailWrap } diff --git a/promo-ui2/src/components/ContentDetailWrap/src/ContentDetailWrap.vue b/promo-ui2/src/components/ContentDetailWrap/src/ContentDetailWrap.vue new file mode 100644 index 0000000..29b4ff7 --- /dev/null +++ b/promo-ui2/src/components/ContentDetailWrap/src/ContentDetailWrap.vue @@ -0,0 +1,25 @@ + + + diff --git a/promo-ui2/src/components/ContentWrap/index.ts b/promo-ui2/src/components/ContentWrap/index.ts new file mode 100644 index 0000000..8c22cc8 --- /dev/null +++ b/promo-ui2/src/components/ContentWrap/index.ts @@ -0,0 +1,3 @@ +import ContentWrap from './src/ContentWrap.vue' + +export { ContentWrap } diff --git a/promo-ui2/src/components/ContentWrap/src/ContentWrap.vue b/promo-ui2/src/components/ContentWrap/src/ContentWrap.vue new file mode 100644 index 0000000..8557e81 --- /dev/null +++ b/promo-ui2/src/components/ContentWrap/src/ContentWrap.vue @@ -0,0 +1,36 @@ + + + diff --git a/promo-ui2/src/components/ContextMenu/index.ts b/promo-ui2/src/components/ContextMenu/index.ts new file mode 100644 index 0000000..1b5442d --- /dev/null +++ b/promo-ui2/src/components/ContextMenu/index.ts @@ -0,0 +1,12 @@ +import ContextMenu from './src/ContextMenu.vue' +import { ElDropdown } from 'element-plus' +import type { RouteLocationNormalizedLoaded } from 'vue-router' + +export type { ContextMenuSchema } from './src/types' + +export interface ContextMenuExpose { + elDropdownMenuRef: ComponentRef + tagItem: RouteLocationNormalizedLoaded +} + +export { ContextMenu } diff --git a/promo-ui2/src/components/ContextMenu/src/ContextMenu.vue b/promo-ui2/src/components/ContextMenu/src/ContextMenu.vue new file mode 100644 index 0000000..9583037 --- /dev/null +++ b/promo-ui2/src/components/ContextMenu/src/ContextMenu.vue @@ -0,0 +1,72 @@ + + + diff --git a/promo-ui2/src/components/ContextMenu/src/types/index.ts b/promo-ui2/src/components/ContextMenu/src/types/index.ts new file mode 100644 index 0000000..b2be72b --- /dev/null +++ b/promo-ui2/src/components/ContextMenu/src/types/index.ts @@ -0,0 +1,7 @@ +export interface ContextMenuSchema { + disabled?: boolean + divided?: boolean + icon?: string + label: string + command?: (item: ContextMenuSchema) => void +} diff --git a/promo-ui2/src/components/CountTo/index.ts b/promo-ui2/src/components/CountTo/index.ts new file mode 100644 index 0000000..2119f02 --- /dev/null +++ b/promo-ui2/src/components/CountTo/index.ts @@ -0,0 +1,3 @@ +import CountTo from './src/CountTo.vue' + +export { CountTo } diff --git a/promo-ui2/src/components/CountTo/src/CountTo.vue b/promo-ui2/src/components/CountTo/src/CountTo.vue new file mode 100644 index 0000000..cab8ea9 --- /dev/null +++ b/promo-ui2/src/components/CountTo/src/CountTo.vue @@ -0,0 +1,180 @@ + + + diff --git a/promo-ui2/src/components/Descriptions/index.ts b/promo-ui2/src/components/Descriptions/index.ts new file mode 100644 index 0000000..8f9a0f0 --- /dev/null +++ b/promo-ui2/src/components/Descriptions/index.ts @@ -0,0 +1,5 @@ +import Descriptions from './src/Descriptions.vue' + +export type { DescriptionsSchema } from './src/types' + +export { Descriptions } diff --git a/promo-ui2/src/components/Descriptions/src/Descriptions.vue b/promo-ui2/src/components/Descriptions/src/Descriptions.vue new file mode 100644 index 0000000..a4b222b --- /dev/null +++ b/promo-ui2/src/components/Descriptions/src/Descriptions.vue @@ -0,0 +1,197 @@ + + + diff --git a/promo-ui2/src/components/Descriptions/src/types/index.ts b/promo-ui2/src/components/Descriptions/src/types/index.ts new file mode 100644 index 0000000..be55b1c --- /dev/null +++ b/promo-ui2/src/components/Descriptions/src/types/index.ts @@ -0,0 +1,15 @@ +export interface DescriptionsSchema { + span?: number // 占多少分 + field: string // 字段名 + label?: string // label名 + width?: string | number + minWidth?: string | number + align?: 'left' | 'center' | 'right' + labelAlign?: 'left' | 'center' | 'right' + className?: string + labelClassName?: string + slots?: { + default?: (...args: any[]) => JSX.Element | null + label?: (...args: any[]) => JSX.Element | null + } +} diff --git a/promo-ui2/src/components/Dialog/hooks/useResize.ts b/promo-ui2/src/components/Dialog/hooks/useResize.ts new file mode 100644 index 0000000..d52b279 --- /dev/null +++ b/promo-ui2/src/components/Dialog/hooks/useResize.ts @@ -0,0 +1,178 @@ +import { ref } from 'vue' + +export const useResize = (props?: { + minHeightPx?: number + minWidthPx?: number + initHeight?: number + initWidth?: number +}) => { + const { + minHeightPx = 400, + minWidthPx = window.innerWidth / 2, + initHeight = 400, + initWidth = window.innerWidth / 2 + } = props || {} + // 屏幕宽度的 50% 作为最小宽度 + // const minWidthPx = window.innerWidth / 2 + // 固定的最小高度 400px + // const minHeightPx = 400 + // 初始高度限制为 400px + const maxHeight = ref(initHeight + 'px') + // 初始宽度限制为 50% + const minWidth = ref(initWidth + 'px') + const setupDrag = (elDialog: any, el: any) => { + // 获取对话框元素 + // 是否正在调整大小的标志 + let isResizing = false + // 当前调整的方向 + let currentResizeDirection = '' + + // 鼠标移动时的事件处理器,用于检测鼠标位置并设置相应的光标样式 + const handleMouseMove = (e: any) => { + const rect = elDialog.getBoundingClientRect() + // 鼠标相对于对话框左侧的偏移量 + const offsetX = e.clientX - rect.left + // 鼠标相对于对话框顶部的偏移量 + const offsetY = e.clientY - rect.top + const width = elDialog.clientWidth + const height = elDialog.clientHeight + + // 获取对话框的内边距 + const computedStyle = window.getComputedStyle(elDialog) + const paddingLeft = parseFloat(computedStyle.paddingLeft) + const paddingRight = parseFloat(computedStyle.paddingRight) + const paddingBottom = parseFloat(computedStyle.paddingBottom) + const paddingTop = parseFloat(computedStyle.paddingTop) + + // 根据鼠标位置设置相应的光标样式和调整方向 + if (!isResizing) { + if (offsetX < paddingLeft && offsetY > paddingTop && offsetY < height - paddingBottom) { + elDialog.style.cursor = 'ew-resize' // 左右箭头 + currentResizeDirection = 'left' + } else if ( + offsetX > width - paddingRight && + offsetY > paddingTop && + offsetY < height - paddingBottom + ) { + elDialog.style.cursor = 'ew-resize' // 左右箭头 + currentResizeDirection = 'right' + } else if ( + offsetY < paddingTop && + offsetX > paddingLeft && + offsetX < width - paddingRight + ) { + elDialog.style.cursor = 'ns-resize' // 上下箭头 + currentResizeDirection = 'top' + } else if ( + offsetY > height - paddingBottom && + offsetX > paddingLeft && + offsetX < width - paddingRight + ) { + elDialog.style.cursor = 'ns-resize' // 上下箭头 + currentResizeDirection = 'bottom' + } else if (offsetX < paddingLeft && offsetY < paddingTop) { + elDialog.style.cursor = 'nwse-resize' // 左上右下箭头 + currentResizeDirection = 'top-left' + } else if (offsetX > width - paddingRight && offsetY < paddingTop) { + elDialog.style.cursor = 'nesw-resize' // 右上左下箭头 + currentResizeDirection = 'top-right' + } else if (offsetX < paddingLeft && offsetY > height - paddingBottom) { + elDialog.style.cursor = 'nesw-resize' // 右上左下箭头 + currentResizeDirection = 'bottom-left' + } else if (offsetX > width - paddingRight && offsetY > height - paddingBottom) { + elDialog.style.cursor = 'nwse-resize' // 左上右下箭头 + currentResizeDirection = 'bottom-right' + } else { + elDialog.style.cursor = 'default' + currentResizeDirection = '' + } + } + } + + // 鼠标按下时的事件处理器,开始调整对话框大小 + const handleMouseDown = (e) => { + if (currentResizeDirection) { + isResizing = true + + const initialX = e.clientX + const initialY = e.clientY + const initialWidth = elDialog.clientWidth + const initialHeight = el.querySelector('.el-dialog__body').clientHeight + + // 调整大小的事件处理器 + const handleResizing = (e: any) => { + if (!isResizing) return + + let newWidth = initialWidth + let newHeight = initialHeight + + // 根据当前调整方向计算新的宽度和高度 + if (currentResizeDirection.includes('right')) { + newWidth = Math.max(minWidthPx, initialWidth + (e.clientX - initialX) * 2) + minWidth.value = `${newWidth}px` + } + + if (currentResizeDirection.includes('left')) { + newWidth = Math.max(minWidthPx, initialWidth - (e.clientX - initialX) * 2) + minWidth.value = `${newWidth}px` + } + + if (currentResizeDirection.includes('bottom')) { + newHeight = Math.max(minHeightPx, initialHeight + (e.clientY - initialY) * 2 - 20) + maxHeight.value = `${Math.min(newHeight, window.innerHeight - 165)}px` + } + + if (currentResizeDirection.includes('top')) { + newHeight = Math.max(minHeightPx, initialHeight - (e.clientY - initialY) * 2 - 20) + maxHeight.value = `${Math.min(newHeight, window.innerHeight - 165)}px` + } + + if (currentResizeDirection === 'top-left') { + newWidth = Math.max(minWidthPx, initialWidth - (e.clientX - initialX) * 2) + minWidth.value = `${newWidth}px` + newHeight = Math.max(minHeightPx, initialHeight - (e.clientY - initialY) * 2 - 20) + maxHeight.value = `${Math.min(newHeight, window.innerHeight - 165)}px` + } + + if (currentResizeDirection === 'top-right') { + newWidth = Math.max(minWidthPx, initialWidth + (e.clientX - initialX) * 2) + minWidth.value = `${newWidth}px` + newHeight = Math.max(minHeightPx, initialHeight - (e.clientY - initialY) * 2 - 20) + maxHeight.value = `${Math.min(newHeight, window.innerHeight - 165)}px` + } + + if (currentResizeDirection === 'bottom-left') { + newWidth = Math.max(minWidthPx, initialWidth - (e.clientX - initialX) * 2) + minWidth.value = `${newWidth}px` + newHeight = Math.max(minHeightPx, initialHeight + (e.clientY - initialY) * 2 - 20) + maxHeight.value = `${Math.min(newHeight, window.innerHeight - 165)}px` + } + + if (currentResizeDirection === 'bottom-right') { + newWidth = Math.max(minWidthPx, initialWidth + (e.clientX - initialX) * 2) + minWidth.value = `${newWidth}px` + newHeight = Math.max(minHeightPx, initialHeight + (e.clientY - initialY) * 2 - 20) + maxHeight.value = `${Math.min(newHeight, window.innerHeight - 165)}px` + } + } + // 停止调整大小的事件处理器 + const stopResizing = () => { + isResizing = false + document.removeEventListener('mousemove', handleResizing) + document.removeEventListener('mouseup', stopResizing) + } + + document.addEventListener('mousemove', handleResizing) + document.addEventListener('mouseup', stopResizing) + } + } + elDialog.addEventListener('mousemove', handleMouseMove) + elDialog.addEventListener('mousedown', handleMouseDown) + } + + return { + setupDrag, + maxHeight, + minWidth + } +} diff --git a/promo-ui2/src/components/Dialog/index.ts b/promo-ui2/src/components/Dialog/index.ts new file mode 100644 index 0000000..1655dad --- /dev/null +++ b/promo-ui2/src/components/Dialog/index.ts @@ -0,0 +1,3 @@ +import Dialog from './src/Dialog.vue' + +export { Dialog } diff --git a/promo-ui2/src/components/Dialog/src/Dialog.vue b/promo-ui2/src/components/Dialog/src/Dialog.vue new file mode 100644 index 0000000..611d702 --- /dev/null +++ b/promo-ui2/src/components/Dialog/src/Dialog.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/promo-ui2/src/components/Dialog/src/ResizeDialog.vue b/promo-ui2/src/components/Dialog/src/ResizeDialog.vue new file mode 100644 index 0000000..d604e12 --- /dev/null +++ b/promo-ui2/src/components/Dialog/src/ResizeDialog.vue @@ -0,0 +1,73 @@ + + diff --git a/promo-ui2/src/components/Echart/index.ts b/promo-ui2/src/components/Echart/index.ts new file mode 100644 index 0000000..4822092 --- /dev/null +++ b/promo-ui2/src/components/Echart/index.ts @@ -0,0 +1,3 @@ +import Echart from './src/Echart.vue' + +export { Echart } diff --git a/promo-ui2/src/components/Echart/src/Echart.vue b/promo-ui2/src/components/Echart/src/Echart.vue new file mode 100644 index 0000000..58cae29 --- /dev/null +++ b/promo-ui2/src/components/Echart/src/Echart.vue @@ -0,0 +1,115 @@ + + + diff --git a/promo-ui2/src/components/Editor/index.ts b/promo-ui2/src/components/Editor/index.ts new file mode 100644 index 0000000..3fbf0a9 --- /dev/null +++ b/promo-ui2/src/components/Editor/index.ts @@ -0,0 +1,8 @@ +import Editor from './src/Editor.vue' +import { IDomEditor } from '@wangeditor/editor' + +export interface EditorExpose { + getEditorRef: () => Promise +} + +export { Editor } diff --git a/promo-ui2/src/components/Editor/src/Editor.vue b/promo-ui2/src/components/Editor/src/Editor.vue new file mode 100644 index 0000000..0bb3452 --- /dev/null +++ b/promo-ui2/src/components/Editor/src/Editor.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/promo-ui2/src/components/Error/index.ts b/promo-ui2/src/components/Error/index.ts new file mode 100644 index 0000000..a52c6f9 --- /dev/null +++ b/promo-ui2/src/components/Error/index.ts @@ -0,0 +1,3 @@ +import Error from './src/Error.vue' + +export { Error } diff --git a/promo-ui2/src/components/Error/src/Error.vue b/promo-ui2/src/components/Error/src/Error.vue new file mode 100644 index 0000000..b18af60 --- /dev/null +++ b/promo-ui2/src/components/Error/src/Error.vue @@ -0,0 +1,57 @@ + + + diff --git a/promo-ui2/src/components/Footer/index.ts b/promo-ui2/src/components/Footer/index.ts new file mode 100644 index 0000000..bd052e0 --- /dev/null +++ b/promo-ui2/src/components/Footer/index.ts @@ -0,0 +1,3 @@ +import Footer from './src/Footer.vue' + +export { Footer } diff --git a/promo-ui2/src/components/Footer/src/Footer.vue b/promo-ui2/src/components/Footer/src/Footer.vue new file mode 100644 index 0000000..50b7ce7 --- /dev/null +++ b/promo-ui2/src/components/Footer/src/Footer.vue @@ -0,0 +1,22 @@ + + + diff --git a/promo-ui2/src/components/Form/index.ts b/promo-ui2/src/components/Form/index.ts new file mode 100644 index 0000000..6688425 --- /dev/null +++ b/promo-ui2/src/components/Form/index.ts @@ -0,0 +1,48 @@ +import Form from './src/Form.vue' +import type { FormSchema, FormSetProps } from './src/types' +export type { + ComponentNameEnum, + ComponentName, + InputComponentProps, + AutocompleteComponentProps, + InputNumberComponentProps, + SelectOption, + SelectComponentProps, + SelectV2ComponentProps, + CascaderComponentProps, + SwitchComponentProps, + RateComponentProps, + ColorPickerComponentProps, + TransferComponentProps, + RadioOption, + RadioGroupComponentProps, + RadioButtonComponentProps, + CheckboxOption, + CheckboxGroupComponentProps, + DividerComponentProps, + DatePickerComponentProps, + DateTimePickerComponentProps, + TimePickerComponentProps, + TimeSelectComponentProps, + ColProps, + FormSetProps, + FormItemProps, + FormSchema, + FormProps, + PlaceholderModel, + InputPasswordComponentProps, + TreeSelectComponentProps +} from './src/types' + +export interface FormExpose { + setValues: (data: Recordable) => void + setProps: (props: Recordable) => void + delSchema: (field: string) => void + addSchema: (formSchema: FormSchema, index?: number) => void + setSchema: (schemaProps: FormSetProps[]) => void + formModel: Recordable + getComponentExpose: (field: string) => any + getFormItemExpose: (field: string) => any +} + +export { Form } diff --git a/promo-ui2/src/components/Form/src/Form.vue b/promo-ui2/src/components/Form/src/Form.vue new file mode 100644 index 0000000..22de784 --- /dev/null +++ b/promo-ui2/src/components/Form/src/Form.vue @@ -0,0 +1,443 @@ + + + diff --git a/promo-ui2/src/components/Form/src/components/useRenderCheckbox.tsx b/promo-ui2/src/components/Form/src/components/useRenderCheckbox.tsx new file mode 100644 index 0000000..63afb10 --- /dev/null +++ b/promo-ui2/src/components/Form/src/components/useRenderCheckbox.tsx @@ -0,0 +1,31 @@ +import { FormSchema, ComponentNameEnum, CheckboxGroupComponentProps } from '../types' +import { ElCheckbox, ElCheckboxButton } from 'element-plus' +import { defineComponent } from 'vue' + +export const useRenderCheckbox = () => { + const renderCheckboxOptions = (item: FormSchema) => { + // 如果有别名,就取别名 + const componentProps = item?.componentProps as CheckboxGroupComponentProps + const valueAlias = componentProps?.props?.value || 'value' + const labelAlias = componentProps?.props?.label || 'label' + const disabledAlias = componentProps?.props?.disabled || 'disabled' + const Com = ( + item.component === ComponentNameEnum.CHECKBOX_GROUP ? ElCheckbox : ElCheckboxButton + ) as ReturnType + return componentProps?.options?.map((option) => { + const { ...other } = option + return ( + + ) + }) + } + + return { + renderCheckboxOptions + } +} diff --git a/promo-ui2/src/components/Form/src/components/useRenderRadio.tsx b/promo-ui2/src/components/Form/src/components/useRenderRadio.tsx new file mode 100644 index 0000000..fc9842b --- /dev/null +++ b/promo-ui2/src/components/Form/src/components/useRenderRadio.tsx @@ -0,0 +1,31 @@ +import { FormSchema, ComponentNameEnum, RadioGroupComponentProps } from '../types' +import { ElRadio, ElRadioButton } from 'element-plus' +import { defineComponent } from 'vue' + +export const useRenderRadio = () => { + const renderRadioOptions = (item: FormSchema) => { + // 如果有别名,就取别名 + const componentProps = item?.componentProps as RadioGroupComponentProps + const valueAlias = componentProps?.props?.value || 'value' + const labelAlias = componentProps?.props?.label || 'label' + const disabledAlias = componentProps?.props?.disabled || 'disabled' + const Com = ( + item.component === ComponentNameEnum.RADIO_GROUP ? ElRadio : ElRadioButton + ) as ReturnType + return componentProps?.options?.map((option) => { + const { ...other } = option + return ( + + ) + }) + } + + return { + renderRadioOptions + } +} diff --git a/promo-ui2/src/components/Form/src/components/useRenderSelect.tsx b/promo-ui2/src/components/Form/src/components/useRenderSelect.tsx new file mode 100644 index 0000000..46389af --- /dev/null +++ b/promo-ui2/src/components/Form/src/components/useRenderSelect.tsx @@ -0,0 +1,58 @@ +import { ElOption, ElOptionGroup } from 'element-plus' +import { FormSchema, SelectComponentProps, SelectOption } from '../types' + +export const useRenderSelect = () => { + // 渲染 select options + const renderSelectOptions = (item: FormSchema) => { + const componentsProps = item?.componentProps as SelectComponentProps + const optionGroupDefaultSlot = componentsProps?.slots?.optionGroupDefault + // 如果有别名,就取别名 + const labelAlias = componentsProps?.props?.label + const keyAlias = componentsProps?.props?.key + return componentsProps?.options?.map((option) => { + if (option?.options?.length) { + return optionGroupDefaultSlot ? ( + optionGroupDefaultSlot(option) + ) : ( + + {{ + default: () => + option?.options?.map((v) => { + return renderSelectOptionItem(item, v) + }) + }} + + ) + } else { + return renderSelectOptionItem(item, option) + } + }) + } + + // 渲染 select option item + const renderSelectOptionItem = (item: FormSchema, option: SelectOption) => { + // 如果有别名,就取别名 + const componentsProps = item.componentProps as SelectComponentProps + const labelAlias = componentsProps?.props?.label + const valueAlias = componentsProps?.props?.value + const keyAlias = componentsProps?.props?.key + const optionDefaultSlot = componentsProps.slots?.optionDefault + + return ( + + {{ + default: () => (optionDefaultSlot ? optionDefaultSlot(option) : undefined) + }} + + ) + } + + return { + renderSelectOptions + } +} diff --git a/promo-ui2/src/components/Form/src/helper/componentMap.ts b/promo-ui2/src/components/Form/src/helper/componentMap.ts new file mode 100644 index 0000000..df2714f --- /dev/null +++ b/promo-ui2/src/components/Form/src/helper/componentMap.ts @@ -0,0 +1,59 @@ +import type { Component } from 'vue' +import { + ElCascader, + ElCheckboxGroup, + ElColorPicker, + ElDatePicker, + ElInput, + ElInputNumber, + ElRadioGroup, + ElRate, + ElSelect, + ElSelectV2, + ElSlider, + ElSwitch, + ElTimePicker, + ElTimeSelect, + ElTransfer, + ElAutocomplete, + ElDivider, + ElTreeSelect, + ElUpload +} from 'element-plus' +import { InputPassword } from '@/components/InputPassword' +import { Editor } from '@/components/Editor' +import { JsonEditor } from '@/components/JsonEditor' +import { IconPicker } from '@/components/IconPicker' +import { IAgree } from '@/components/IAgree' +import { ComponentName } from '../types' + +const componentMap: Recordable = { + RadioGroup: ElRadioGroup, + RadioButton: ElRadioGroup, + CheckboxGroup: ElCheckboxGroup, + CheckboxButton: ElCheckboxGroup, + Input: ElInput, + Autocomplete: ElAutocomplete, + InputNumber: ElInputNumber, + Select: ElSelect, + Cascader: ElCascader, + Switch: ElSwitch, + Slider: ElSlider, + TimePicker: ElTimePicker, + DatePicker: ElDatePicker, + Rate: ElRate, + ColorPicker: ElColorPicker, + Transfer: ElTransfer, + Divider: ElDivider, + TimeSelect: ElTimeSelect, + SelectV2: ElSelectV2, + InputPassword: InputPassword, + Editor: Editor, + TreeSelect: ElTreeSelect, + Upload: ElUpload, + JsonEditor: JsonEditor, + IconPicker: IconPicker, + IAgree: IAgree +} + +export { componentMap } diff --git a/promo-ui2/src/components/Form/src/helper/index.ts b/promo-ui2/src/components/Form/src/helper/index.ts new file mode 100644 index 0000000..2ba1dc2 --- /dev/null +++ b/promo-ui2/src/components/Form/src/helper/index.ts @@ -0,0 +1,169 @@ +import { useI18n } from '@/hooks/web/useI18n' +import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from '../types' +import { isFunction } from '@/utils/is' +import { firstUpperCase, humpToDash } from '@/utils' +import { set, get } from 'lodash-es' + +const { t } = useI18n() + +/** + * + * @param schema 对应组件数据 + * @returns 返回提示信息对象 + * @description 用于自动设置placeholder + */ +export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => { + const textMap = [ + ComponentNameEnum.INPUT, + ComponentNameEnum.AUTOCOMPLETE, + ComponentNameEnum.INPUT_NUMBER, + ComponentNameEnum.INPUT_PASSWORD + ] + const selectMap = [ + ComponentNameEnum.SELECT, + ComponentNameEnum.TIME_PICKER, + ComponentNameEnum.DATE_PICKER, + ComponentNameEnum.TIME_SELECT, + ComponentNameEnum.SELECT_V2 + ] + if (textMap.includes(schema?.component as ComponentNameEnum)) { + return { + placeholder: t('common.inputText') + } + } + if (selectMap.includes(schema?.component as ComponentNameEnum)) { + // 一些范围选择器 + const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange'] + if ( + twoTextMap.includes( + ((schema?.componentProps as any)?.type || + (schema?.componentProps as any)?.isRange) as string + ) + ) { + return { + startPlaceholder: t('common.startTimeText'), + endPlaceholder: t('common.endTimeText'), + rangeSeparator: '-' + } + } else { + return { + placeholder: t('common.selectText') + } + } + } + return {} +} + +/** + * + * @param col 内置栅格 + * @returns 返回栅格属性 + * @description 合并传入进来的栅格属性 + */ +export const setGridProp = (col: ColProps = {}): ColProps => { + const colProps: ColProps = { + // 如果有span,代表用户优先级更高,所以不需要默认栅格 + ...(col.span + ? {} + : { + xs: 24, + sm: 12, + md: 12, + lg: 12, + xl: 12 + }), + ...col + } + return colProps +} + +/** + * + * @param item 传入的组件属性 + * @returns 默认添加 clearable 属性 + */ +export const setComponentProps = (item: FormSchema): Recordable => { + // const notNeedClearable = ['ColorPicker'] + // 拆分事件并组合 + const onEvents = (item?.componentProps as any)?.on || {} + const newOnEvents: Recordable = {} + + for (const key in onEvents) { + if (onEvents[key]) { + newOnEvents[`on${firstUpperCase(key)}`] = (...args: any[]) => { + onEvents[key](...args) + } + } + } + + const componentProps: Recordable = { + clearable: true, + ...item.componentProps, + ...newOnEvents + } + // 需要删除额外的属性 + if (componentProps.slots) { + delete componentProps.slots + } + if (componentProps.on) { + delete componentProps.on + } + return componentProps +} + +/** + * + * @param formModel 表单数据 + * @param slotsProps 插槽属性 + */ +export const setItemComponentSlots = (slotsProps: Recordable = {}): Recordable => { + const slotObj: Recordable = {} + for (const key in slotsProps) { + if (slotsProps[key]) { + if (isFunction(slotsProps[key])) { + slotObj[humpToDash(key)] = (...args: any[]) => { + return slotsProps[key]?.(...args) + } + } else { + slotObj[humpToDash(key)] = () => { + return slotsProps[key] + } + } + } + } + return slotObj +} + +/** + * + * @param schema Form表单结构化数组 + * @param formModel FormMoel + * @returns FormMoel + * @description 生成对应的formModel + */ +export const initModel = (schema: FormSchema[], formModel: Recordable) => { + const model: Recordable = { ...formModel } + schema.map((v) => { + if (v.remove) { + delete model[v.field] + } else if (v.component !== 'Divider') { + // const hasField = Reflect.has(model, v.field) + const hasField = get(model, v.field) + // 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值 + set( + model, + v.field, + hasField !== void 0 ? get(model, v.field) : v.value !== void 0 ? v.value : undefined + ) + // model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : undefined + } + }) + // 如果 schema 对应的 field 不存在,则删除 model 中的对应的 field + for (let i = 0; i < schema.length; i++) { + const key = schema[i].field + if (!Object.prototype.hasOwnProperty.call(model, key)) { + delete model[key] + } + } + return model +} diff --git a/promo-ui2/src/components/Form/src/types/index.ts b/promo-ui2/src/components/Form/src/types/index.ts new file mode 100644 index 0000000..ac80193 --- /dev/null +++ b/promo-ui2/src/components/Form/src/types/index.ts @@ -0,0 +1,670 @@ +import { + AutocompleteProps, + InputNumberProps, + CascaderProps, + CascaderNode, + CascaderValue, + SwitchProps, + ComponentSize, + InputProps, + RateProps, + ColorPickerProps, + TransferProps, + RadioGroupProps, + RadioButtonProps, + CheckboxGroupProps, + DividerProps, + DatePickerProps, + FormItemProps as ElFormItemProps, + FormProps as ElFormProps, + ISelectProps, + UploadProps +} from 'element-plus' +import { IEditorConfig } from '@wangeditor/editor' +import { JsonEditorProps } from '@/components/JsonEditor' +import { IAgreeProps } from '@/components/IAgree' +import { CSSProperties } from 'vue' + +export interface PlaceholderModel { + placeholder?: string + startPlaceholder?: string + endPlaceholder?: string + rangeSeparator?: string +} + +export enum ComponentNameEnum { + RADIO_GROUP = 'RadioGroup', + RADIO_BUTTON = 'RadioButton', + CHECKBOX_GROUP = 'CheckboxGroup', + CHECKBOX_BUTTON = 'CheckboxButton', + INPUT = 'Input', + AUTOCOMPLETE = 'Autocomplete', + INPUT_NUMBER = 'InputNumber', + SELECT = 'Select', + CASCADER = 'Cascader', + SWITCH = 'Switch', + SLIDER = 'Slider', + TIME_PICKER = 'TimePicker', + DATE_PICKER = 'DatePicker', + RATE = 'Rate', + COLOR_PICKER = 'ColorPicker', + TRANSFER = 'Transfer', + DIVIDER = 'Divider', + TIME_SELECT = 'TimeSelect', + SELECT_V2 = 'SelectV2', + INPUT_PASSWORD = 'InputPassword', + EDITOR = 'Editor', + TREE_SELECT = 'TreeSelect', + UPLOAD = 'Upload', + JSON_EDITOR = 'JsonEditor', + ICON_PICKER = 'IconPicker', + I_AGREE = 'IAgree' +} + +type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K + ? K extends string + ? K extends `${infer A}_${infer B}` + ? `${Capitalize>}${Capitalize>}` + : Capitalize> + : never + : never + +export type ComponentName = CamelCaseComponentName + +export interface InputPasswordComponentProps { + strength?: boolean + style?: CSSProperties +} + +export interface InputComponentProps extends Partial { + rows?: number + on?: { + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + change?: (value: string | number) => void + clear?: () => void + input?: (value: string | number) => void + } + slots?: { + prefix?: (...args: any[]) => JSX.Element | null + suffix?: (...args: any[]) => JSX.Element | null + prepend?: (...args: any[]) => JSX.Element | null + append?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface AutocompleteComponentProps extends Partial { + on?: { + select?: (item: any) => void + change?: (value: string | number) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + prefix?: (...args: any[]) => JSX.Element | null + suffix?: (...args: any[]) => JSX.Element | null + prepend?: (...args: any[]) => JSX.Element | null + append?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface InputNumberComponentProps extends Partial { + on?: { + change?: (currentValue: number | undefined, oldValue: number | undefined) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + } + style?: CSSProperties +} + +export interface SelectOption { + label?: string + disabled?: boolean + value?: any + key?: string | number + options?: SelectOption[] + [key: string]: any +} + +export interface SelectComponentProps extends Omit, 'options'> { + /** + * 数据源的字段别名 + */ + props?: { + key?: string + value?: string + label?: string + children?: string + } + on?: { + change?: (value: string | number | boolean | object) => void + visibleChange?: (visible: boolean) => void + removeTag?: (tag: any) => void + clear?: () => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + } + slots?: { + default?: (options: SelectOption[]) => JSX.Element[] | null + optionGroupDefault?: (item: SelectOption) => JSX.Element + optionDefault?: (option: SelectOption) => JSX.Element | null + prefix?: (...args: any[]) => JSX.Element | null + empty?: (...args: any[]) => JSX.Element | null + } + options?: SelectOption[] + style?: CSSProperties +} + +export interface SelectV2ComponentProps { + multiple?: boolean + disabled?: boolean + valueKey?: string + size?: ComponentSize + clearable?: boolean + clearIcon?: string | JSX.Element | null + collapseTags?: boolean + multipleLimit?: number + name?: string + effect?: string + autocomplete?: string + placeholder?: string + filterable?: boolean + allowCreate?: boolean + reserveKeyword?: boolean + noDataText?: string + popperClass?: string + teleported?: boolean + persistent?: boolean + popperOptions?: any + automaticDropdown?: boolean + height?: number + scrollbarAlwaysOn?: boolean + remote?: boolean + remoteMethod?: (query: string) => void + validateEvent?: boolean + placement?: AutocompleteProps['placement'] + collapseTagsTooltip?: boolean + on?: { + change?: (value: string | number | boolean | object) => void + visibleChange?: (visible: boolean) => void + removeTag?: (tag: any) => void + clear?: () => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + } + options?: SelectOption[] + slots?: { + default?: (option: SelectOption) => JSX.Element | null + } + style?: CSSProperties +} + +export interface CascaderComponentProps { + options?: Record[] + props?: CascaderProps + size?: ComponentSize + placeholder?: string + disabled?: boolean + clearable?: boolean + showAllLevels?: boolean + collapseTags?: boolean + collapseTagsTooltip?: boolean + separator?: string + filterable?: boolean + filterMethod?: (node: CascaderNode, keyword: string) => boolean + debounce?: number + beforeFilter?: (value: string) => boolean + popperClass?: string + teleported?: boolean + tagType?: ElementPlusInfoType + validateEvent?: boolean + on?: { + change?: (value: CascaderValue) => void + expandChange?: (value: CascaderValue) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + visibleChange?: (value: boolean) => void + removeTag?: (value: CascaderNode['valueByOption']) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + empty?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface SwitchComponentProps extends Partial { + on?: { + change?: (value: boolean | string | number) => void + } + style?: CSSProperties +} + +export interface RateComponentProps extends Partial { + on?: { + change?: (value: number) => void + } + style?: CSSProperties +} + +export interface ColorPickerComponentProps extends Partial { + on?: { + change?: (value: string) => void + activeChange?: (value: string) => void + } + style?: CSSProperties +} + +export interface TransferComponentProps extends Partial { + on?: { + change?: ( + value: number | string, + direction: 'left' | 'right', + movedKeys: string[] | number[] + ) => void + leftCheckChange?: (value: any[]) => void + rightCheckChange?: (value: any[]) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + leftFooter?: (...args: any[]) => JSX.Element | null + rightFooter?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface RadioOption { + label?: string + value?: string | number | boolean + disabled?: boolean + border?: boolean + size?: ComponentSize + name?: string + [key: string]: any +} +export interface RadioGroupComponentProps extends Partial { + options?: RadioOption[] + /** + * 数据源的字段别名 + */ + props?: { + label?: string + value?: string + disabled?: string + } + on?: { + change?: (value: string | number | boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element[] | null + } + style?: CSSProperties +} + +export interface RadioButtonComponentProps extends Partial { + options?: RadioOption[] + /** + * 数据源的字段别名 + */ + props?: { + label?: string + value?: string + disabled?: string + } + on?: { + change?: (value: string | number | boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element[] | null + } + style?: CSSProperties +} + +export interface CheckboxOption { + label?: string + value?: string | number | boolean + disabled?: boolean + trueLabel?: string | number + falseLabel?: string | number + border?: boolean + size?: ComponentSize + name?: string + checked?: boolean + indeterminate?: boolean + validateEvent?: boolean + tabindex?: number | string + id?: string + controls?: boolean + [key: string]: any +} + +export interface CheckboxGroupComponentProps extends Partial { + options?: CheckboxOption[] + /** + * 数据源的字段别名 + */ + props?: { + label?: string + value?: string + disabled?: string + } + on?: { + change?: (value: string | number | boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element[] | null + } + style?: CSSProperties +} + +export interface DividerComponentProps extends Partial { + on?: { + change?: (value: number) => void + input?: (value: number) => void + } + style?: CSSProperties +} + +export interface DatePickerComponentProps extends Partial { + on?: { + change?: (value: string | Date | number | string[]) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + calendarChange?: (val: [Date, Date]) => void + panelChange?: (date, mode, view) => void + visibleChange?: (visibility: boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + rangeSeparator?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface DateTimePickerComponentProps { + readonly?: boolean + disabled?: boolean + editable?: boolean + clearable?: boolean + size?: ComponentSize + placeholder?: string + startPlaceholder?: string + endPlaceholder?: string + timeArrowControl?: boolean + type?: 'year' | 'month' | 'date' | 'datetime' | 'datetimerange' | 'daterange' | 'week' + format?: string + popperClass?: string + rangeSeparator?: string + defaultValue?: Date | [Date, Date] + defaultTime?: Date | [Date, Date] + valueFormat?: string + id?: string + name?: string + unlinkPanels?: boolean + prefixIcon?: string | JSX.Element + clearIcon?: string | JSX.Element + shortcuts?: Array<{ text: string; value: Date | Function }> + disabledDate?: (date: Date) => boolean + cellClassName?: string | ((date: Date) => string | undefined) + teleported?: boolean + on?: { + change?: (value: string | Date | number | string[]) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + calendarChange?: (val: [Date, Date]) => void + visibleChange?: (visibility: boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + rangeSeparator?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface TimePickerComponentProps { + readonly?: boolean + disabled?: boolean + editable?: boolean + clearable?: boolean + size?: ComponentSize + placeholder?: string + startPlaceholder?: string + endPlaceholder?: string + isRange?: boolean + arrowControl?: boolean + popperClass?: string + rangeSeparator?: string + format?: string + defaultValue?: Date | [Date, Date] + id?: string + name?: string + label?: string + prefixIcon?: string | JSX.Element + clearIcon?: string | JSX.Element + disabledHours?: (role: string, comparingDate?: any) => number[] + disabledMinutes?: (hour: number, role: string, comparingDate?: any) => number[] + disabledSeconds?: (hour: number, minute: number, role: string, comparingDate?: any) => number[] + teleported?: boolean + tabindex?: number | string + on?: { + change: ( + val: number | string | Date | [number, number] | [string, string] | [Date, Date] + ) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + visibleChange?: (visibility: boolean) => void + } + style?: CSSProperties +} + +export interface TimeSelectComponentProps { + disabled?: boolean + editable?: boolean + clearable?: boolean + size?: ComponentSize + placeholder?: string + name?: string + effect?: string + prefixIcon?: string | JSX.Element + clearIcon?: string | JSX.Element + start?: string + end?: string + step?: string + minTime?: string + maxTime?: string + format?: string + on?: { + change?: (val: string) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + } + style?: CSSProperties +} + +export interface EditorComponentProps { + editorConfig?: IEditorConfig + style?: CSSProperties +} + +export interface ColProps { + span?: number + xs?: number + sm?: number + md?: number + lg?: number + xl?: number + tag?: string +} + +export interface FormSetProps { + field: string + path: string + value: any +} + +export interface FormItemProps extends Partial { + style?: CSSProperties + slots?: { + default?: (...args: any[]) => JSX.Element | null + label?: (...args: any[]) => JSX.Element | null + error?: (...args: any[]) => JSX.Element | null + } +} + +export interface UploadComponentProps extends Partial { + slots?: { + default?: (...args: any[]) => JSX.Element | null + trigger?: (...args: any[]) => JSX.Element | null + tip?: (...args: any[]) => JSX.Element | null + file?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface TreeSelectComponentProps + extends Omit, 'props' | 'on' | 'slots'> { + data?: any[] + emptyText?: string + nodeKey?: string + props?: { + children?: string + label?: string | ((...args: any[]) => string) + disabled?: string | ((...args: any[]) => string) + isLeaf?: string | ((...args: any[]) => string) + class?: string | ((...args: any[]) => string) + } + renderAfterExpand?: boolean + load?: (...args: any[]) => Promise + renderContent?: (...args: any[]) => JSX.Element | null + highlightCurrent?: boolean + defaultExpandAll?: boolean + expandOnClickNode?: boolean + checkOnClickNode?: boolean + autoExpandParent?: boolean + defaultExpandedKeys?: any[] + showCheckbox?: boolean + checkStrictly?: boolean + defaultCheckedKeys?: any[] + currentNodeKey?: string | number + filterNodeMethod?: (...args: any[]) => boolean + accordion?: boolean + indent?: number + icon?: string | ((...args: any[]) => JSX.Element | null) + lazy?: boolean + draggable?: boolean + allowDrag?: (...args: any[]) => boolean + allowDrop?: (...args: any[]) => boolean + on?: { + change?: (value: string | number | boolean | object) => void + visibleChange?: (visible: boolean) => void + removeTag?: (tag: any) => void + clear?: () => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + nodeClick?: (...args: any[]) => void + nodeContextMenu?: (...args: any[]) => void + checkChange?: (...args: any[]) => void + check?: (...args: any[]) => void + currentChange?: (...args: any[]) => void + nodeExpand?: (...args: any[]) => void + nodeCollapse?: (...args: any[]) => void + nodeDragStart?: (...args: any[]) => void + nodeDragEnter?: (...args: any[]) => void + nodeDragLeave?: (...args: any[]) => void + nodeDragOver?: (...args: any[]) => void + nodeDragEnd?: (...args: any[]) => void + nodeDrop?: (...args: any[]) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + optionGroupDefault?: (item: SelectOption) => JSX.Element + optionDefault?: (option: SelectOption) => JSX.Element | null + prefix?: (...args: any[]) => JSX.Element | null + empty?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface FormSchema { + /** + * 唯一标识 + */ + field: string + + /** + * 标题 + */ + label?: string + + /** + * col组件属性 + */ + colProps?: ColProps + + /** + * 表单组件属性,具体可以查看element-plus文档 + */ + componentProps?: + | InputComponentProps + | AutocompleteComponentProps + | InputNumberComponentProps + | SelectComponentProps + | SelectV2ComponentProps + | CascaderComponentProps + | SwitchComponentProps + | RateComponentProps + | ColorPickerComponentProps + | TransferComponentProps + | RadioGroupComponentProps + | RadioButtonComponentProps + | DividerComponentProps + | DatePickerComponentProps + | DateTimePickerComponentProps + | TimePickerComponentProps + | InputPasswordComponentProps + | TreeSelectComponentProps + | UploadComponentProps + | JsonEditorProps + | IAgreeProps + | any + + /** + * formItem组件属性,具体可以查看element-plus文档 + */ + formItemProps?: FormItemProps + + /** + * 渲染的组件名称 + */ + component?: ComponentName + + /** + * 初始值 + */ + value?: any + + /** + * 是否隐藏,如果为true,会连同值一同删除,类似v-if + */ + remove?: boolean + + /** + * 样式隐藏,不会把值一同删掉,类似v-show + */ + hidden?: boolean + + /** + * @returns 远程加载下拉项 + */ + optionApi?: any +} + +export interface FormProps extends Partial { + schema?: FormSchema[] + isCol?: boolean + model?: Recordable + autoSetPlaceholder?: boolean + isCustom?: boolean + [key: string]: any +} diff --git a/promo-ui2/src/components/Highlight/index.ts b/promo-ui2/src/components/Highlight/index.ts new file mode 100644 index 0000000..3e2d9ed --- /dev/null +++ b/promo-ui2/src/components/Highlight/index.ts @@ -0,0 +1,3 @@ +import Highlight from './src/Highlight.vue' + +export { Highlight } diff --git a/promo-ui2/src/components/Highlight/src/Highlight.vue b/promo-ui2/src/components/Highlight/src/Highlight.vue new file mode 100644 index 0000000..ef923a9 --- /dev/null +++ b/promo-ui2/src/components/Highlight/src/Highlight.vue @@ -0,0 +1,65 @@ + diff --git a/promo-ui2/src/components/IAgree/index.ts b/promo-ui2/src/components/IAgree/index.ts new file mode 100644 index 0000000..8c31125 --- /dev/null +++ b/promo-ui2/src/components/IAgree/index.ts @@ -0,0 +1,4 @@ +import IAgree from './src/IAgree.vue' + +export type { LinkItem, IAgreeProps } from './src/types' +export { IAgree } diff --git a/promo-ui2/src/components/IAgree/src/IAgree.vue b/promo-ui2/src/components/IAgree/src/IAgree.vue new file mode 100644 index 0000000..6a36e23 --- /dev/null +++ b/promo-ui2/src/components/IAgree/src/IAgree.vue @@ -0,0 +1,41 @@ + + + diff --git a/promo-ui2/src/components/IAgree/src/types/index.ts b/promo-ui2/src/components/IAgree/src/types/index.ts new file mode 100644 index 0000000..9ddef75 --- /dev/null +++ b/promo-ui2/src/components/IAgree/src/types/index.ts @@ -0,0 +1,10 @@ +export interface LinkItem { + text: string + url?: string + onClick?: () => void +} + +export interface IAgreeProps { + text: string + link: LinkItem[] +} diff --git a/promo-ui2/src/components/Icon/index.ts b/promo-ui2/src/components/Icon/index.ts new file mode 100644 index 0000000..83469d4 --- /dev/null +++ b/promo-ui2/src/components/Icon/index.ts @@ -0,0 +1,5 @@ +import Icon from './src/Icon.vue' + +export type { IconTypes } from './src/types' + +export { Icon } diff --git a/promo-ui2/src/components/Icon/src/Icon.vue b/promo-ui2/src/components/Icon/src/Icon.vue new file mode 100644 index 0000000..0fa4d73 --- /dev/null +++ b/promo-ui2/src/components/Icon/src/Icon.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/promo-ui2/src/components/Icon/src/types/index.ts b/promo-ui2/src/components/Icon/src/types/index.ts new file mode 100644 index 0000000..632e4e8 --- /dev/null +++ b/promo-ui2/src/components/Icon/src/types/index.ts @@ -0,0 +1,6 @@ +export interface IconTypes { + size?: number + color?: string + icon: string + hoverColor?: string +} diff --git a/promo-ui2/src/components/IconPicker/index.ts b/promo-ui2/src/components/IconPicker/index.ts new file mode 100644 index 0000000..b7b3022 --- /dev/null +++ b/promo-ui2/src/components/IconPicker/index.ts @@ -0,0 +1,3 @@ +import IconPicker from './src/IconPicker.vue' + +export { IconPicker } diff --git a/promo-ui2/src/components/IconPicker/src/IconPicker.vue b/promo-ui2/src/components/IconPicker/src/IconPicker.vue new file mode 100644 index 0000000..1b20c4c --- /dev/null +++ b/promo-ui2/src/components/IconPicker/src/IconPicker.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/promo-ui2/src/components/IconPicker/src/data/icons.ant-design.ts b/promo-ui2/src/components/IconPicker/src/data/icons.ant-design.ts new file mode 100644 index 0000000..eab2169 --- /dev/null +++ b/promo-ui2/src/components/IconPicker/src/data/icons.ant-design.ts @@ -0,0 +1,836 @@ +export default { + name: 'Ant Design Icons', + prefix: 'vi-ant-design', + icons: [ + 'vi-ant-design:account-book-filled', + 'vi-ant-design:account-book-outlined', + 'vi-ant-design:account-book-twotone', + 'vi-ant-design:aim-outlined', + 'vi-ant-design:alert-filled', + 'vi-ant-design:alert-outlined', + 'vi-ant-design:alert-twotone', + 'vi-ant-design:alibaba-outlined', + 'vi-ant-design:align-center-outlined', + 'vi-ant-design:align-left-outlined', + 'vi-ant-design:align-right-outlined', + 'vi-ant-design:alipay-circle-filled', + 'vi-ant-design:alipay-circle-outlined', + 'vi-ant-design:alipay-outlined', + 'vi-ant-design:alipay-square-filled', + 'vi-ant-design:aliwangwang-filled', + 'vi-ant-design:aliwangwang-outlined', + 'vi-ant-design:aliyun-outlined', + 'vi-ant-design:amazon-circle-filled', + 'vi-ant-design:amazon-outlined', + 'vi-ant-design:amazon-square-filled', + 'vi-ant-design:android-filled', + 'vi-ant-design:android-outlined', + 'vi-ant-design:ant-cloud-outlined', + 'vi-ant-design:ant-design-outlined', + 'vi-ant-design:apartment-outlined', + 'vi-ant-design:api-filled', + 'vi-ant-design:api-outlined', + 'vi-ant-design:api-twotone', + 'vi-ant-design:apple-filled', + 'vi-ant-design:apple-outlined', + 'vi-ant-design:appstore-add-outlined', + 'vi-ant-design:appstore-filled', + 'vi-ant-design:appstore-outlined', + 'vi-ant-design:appstore-twotone', + 'vi-ant-design:area-chart-outlined', + 'vi-ant-design:arrow-down-outlined', + 'vi-ant-design:arrow-left-outlined', + 'vi-ant-design:arrow-right-outlined', + 'vi-ant-design:arrow-up-outlined', + 'vi-ant-design:arrows-alt-outlined', + 'vi-ant-design:audio-filled', + 'vi-ant-design:audio-muted-outlined', + 'vi-ant-design:audio-outlined', + 'vi-ant-design:audio-twotone', + 'vi-ant-design:audit-outlined', + 'vi-ant-design:backward-filled', + 'vi-ant-design:backward-outlined', + 'vi-ant-design:baidu-outlined', + 'vi-ant-design:bank-filled', + 'vi-ant-design:bank-outlined', + 'vi-ant-design:bank-twotone', + 'vi-ant-design:bar-chart-outlined', + 'vi-ant-design:barcode-outlined', + 'vi-ant-design:bars-outlined', + 'vi-ant-design:behance-circle-filled', + 'vi-ant-design:behance-outlined', + 'vi-ant-design:behance-square-filled', + 'vi-ant-design:behance-square-outlined', + 'vi-ant-design:bell-filled', + 'vi-ant-design:bell-outlined', + 'vi-ant-design:bell-twotone', + 'vi-ant-design:bg-colors-outlined', + 'vi-ant-design:bilibili-filled', + 'vi-ant-design:bilibili-outlined', + 'vi-ant-design:block-outlined', + 'vi-ant-design:bold-outlined', + 'vi-ant-design:book-filled', + 'vi-ant-design:book-outlined', + 'vi-ant-design:book-twotone', + 'vi-ant-design:border-bottom-outlined', + 'vi-ant-design:border-horizontal-outlined', + 'vi-ant-design:border-inner-outlined', + 'vi-ant-design:border-left-outlined', + 'vi-ant-design:border-outer-outlined', + 'vi-ant-design:border-outlined', + 'vi-ant-design:border-right-outlined', + 'vi-ant-design:border-top-outlined', + 'vi-ant-design:border-verticle-outlined', + 'vi-ant-design:borderless-table-outlined', + 'vi-ant-design:box-plot-filled', + 'vi-ant-design:box-plot-outlined', + 'vi-ant-design:box-plot-twotone', + 'vi-ant-design:branches-outlined', + 'vi-ant-design:bug-filled', + 'vi-ant-design:bug-outlined', + 'vi-ant-design:bug-twotone', + 'vi-ant-design:build-filled', + 'vi-ant-design:build-outlined', + 'vi-ant-design:build-twotone', + 'vi-ant-design:bulb-filled', + 'vi-ant-design:bulb-outlined', + 'vi-ant-design:bulb-twotone', + 'vi-ant-design:calculator-filled', + 'vi-ant-design:calculator-outlined', + 'vi-ant-design:calculator-twotone', + 'vi-ant-design:calendar-filled', + 'vi-ant-design:calendar-outlined', + 'vi-ant-design:calendar-twotone', + 'vi-ant-design:camera-filled', + 'vi-ant-design:camera-outlined', + 'vi-ant-design:camera-twotone', + 'vi-ant-design:car-filled', + 'vi-ant-design:car-outlined', + 'vi-ant-design:car-twotone', + 'vi-ant-design:caret-down-filled', + 'vi-ant-design:caret-down-outlined', + 'vi-ant-design:caret-left-filled', + 'vi-ant-design:caret-left-outlined', + 'vi-ant-design:caret-right-filled', + 'vi-ant-design:caret-right-outlined', + 'vi-ant-design:caret-up-filled', + 'vi-ant-design:caret-up-outlined', + 'vi-ant-design:carry-out-filled', + 'vi-ant-design:carry-out-outlined', + 'vi-ant-design:carry-out-twotone', + 'vi-ant-design:check-circle-filled', + 'vi-ant-design:check-circle-outlined', + 'vi-ant-design:check-circle-twotone', + 'vi-ant-design:check-outlined', + 'vi-ant-design:check-square-filled', + 'vi-ant-design:check-square-outlined', + 'vi-ant-design:check-square-twotone', + 'vi-ant-design:chrome-filled', + 'vi-ant-design:chrome-outlined', + 'vi-ant-design:ci-circle-filled', + 'vi-ant-design:ci-circle-outlined', + 'vi-ant-design:ci-circle-twotone', + 'vi-ant-design:ci-outlined', + 'vi-ant-design:ci-twotone', + 'vi-ant-design:clear-outlined', + 'vi-ant-design:clock-circle-filled', + 'vi-ant-design:clock-circle-outlined', + 'vi-ant-design:clock-circle-twotone', + 'vi-ant-design:close-circle-filled', + 'vi-ant-design:close-circle-outlined', + 'vi-ant-design:close-circle-twotone', + 'vi-ant-design:close-outlined', + 'vi-ant-design:close-square-filled', + 'vi-ant-design:close-square-outlined', + 'vi-ant-design:close-square-twotone', + 'vi-ant-design:cloud-download-outlined', + 'vi-ant-design:cloud-filled', + 'vi-ant-design:cloud-outlined', + 'vi-ant-design:cloud-server-outlined', + 'vi-ant-design:cloud-sync-outlined', + 'vi-ant-design:cloud-twotone', + 'vi-ant-design:cloud-upload-outlined', + 'vi-ant-design:cluster-outlined', + 'vi-ant-design:code-filled', + 'vi-ant-design:code-outlined', + 'vi-ant-design:code-sandbox-circle-filled', + 'vi-ant-design:code-sandbox-outlined', + 'vi-ant-design:code-sandbox-square-filled', + 'vi-ant-design:code-twotone', + 'vi-ant-design:codepen-circle-filled', + 'vi-ant-design:codepen-circle-outlined', + 'vi-ant-design:codepen-outlined', + 'vi-ant-design:codepen-square-filled', + 'vi-ant-design:coffee-outlined', + 'vi-ant-design:column-height-outlined', + 'vi-ant-design:column-width-outlined', + 'vi-ant-design:comment-outlined', + 'vi-ant-design:compass-filled', + 'vi-ant-design:compass-outlined', + 'vi-ant-design:compass-twotone', + 'vi-ant-design:compress-outlined', + 'vi-ant-design:console-sql-outlined', + 'vi-ant-design:contacts-filled', + 'vi-ant-design:contacts-outlined', + 'vi-ant-design:contacts-twotone', + 'vi-ant-design:container-filled', + 'vi-ant-design:container-outlined', + 'vi-ant-design:container-twotone', + 'vi-ant-design:control-filled', + 'vi-ant-design:control-outlined', + 'vi-ant-design:control-twotone', + 'vi-ant-design:copy-filled', + 'vi-ant-design:copy-outlined', + 'vi-ant-design:copy-twotone', + 'vi-ant-design:copyright-circle-filled', + 'vi-ant-design:copyright-circle-outlined', + 'vi-ant-design:copyright-circle-twotone', + 'vi-ant-design:copyright-outlined', + 'vi-ant-design:copyright-twotone', + 'vi-ant-design:credit-card-filled', + 'vi-ant-design:credit-card-outlined', + 'vi-ant-design:credit-card-twotone', + 'vi-ant-design:crown-filled', + 'vi-ant-design:crown-outlined', + 'vi-ant-design:crown-twotone', + 'vi-ant-design:customer-service-filled', + 'vi-ant-design:customer-service-outlined', + 'vi-ant-design:customer-service-twotone', + 'vi-ant-design:dash-outlined', + 'vi-ant-design:dashboard-filled', + 'vi-ant-design:dashboard-outlined', + 'vi-ant-design:dashboard-twotone', + 'vi-ant-design:database-filled', + 'vi-ant-design:database-outlined', + 'vi-ant-design:database-twotone', + 'vi-ant-design:delete-column-outlined', + 'vi-ant-design:delete-filled', + 'vi-ant-design:delete-outlined', + 'vi-ant-design:delete-row-outlined', + 'vi-ant-design:delete-twotone', + 'vi-ant-design:delivered-procedure-outlined', + 'vi-ant-design:deployment-unit-outlined', + 'vi-ant-design:desktop-outlined', + 'vi-ant-design:diff-filled', + 'vi-ant-design:diff-outlined', + 'vi-ant-design:diff-twotone', + 'vi-ant-design:dingding-outlined', + 'vi-ant-design:dingtalk-circle-filled', + 'vi-ant-design:dingtalk-outlined', + 'vi-ant-design:dingtalk-square-filled', + 'vi-ant-design:disconnect-outlined', + 'vi-ant-design:discord-filled', + 'vi-ant-design:discord-outlined', + 'vi-ant-design:dislike-filled', + 'vi-ant-design:dislike-outlined', + 'vi-ant-design:dislike-twotone', + 'vi-ant-design:docker-outlined', + 'vi-ant-design:dollar-circle-filled', + 'vi-ant-design:dollar-circle-outlined', + 'vi-ant-design:dollar-circle-twotone', + 'vi-ant-design:dollar-outlined', + 'vi-ant-design:dollar-twotone', + 'vi-ant-design:dot-chart-outlined', + 'vi-ant-design:dot-net-outlined', + 'vi-ant-design:double-left-outlined', + 'vi-ant-design:double-right-outlined', + 'vi-ant-design:down-circle-filled', + 'vi-ant-design:down-circle-outlined', + 'vi-ant-design:down-circle-twotone', + 'vi-ant-design:down-outlined', + 'vi-ant-design:down-square-filled', + 'vi-ant-design:down-square-outlined', + 'vi-ant-design:down-square-twotone', + 'vi-ant-design:download-outlined', + 'vi-ant-design:drag-outlined', + 'vi-ant-design:dribbble-circle-filled', + 'vi-ant-design:dribbble-outlined', + 'vi-ant-design:dribbble-square-filled', + 'vi-ant-design:dribbble-square-outlined', + 'vi-ant-design:dropbox-circle-filled', + 'vi-ant-design:dropbox-outlined', + 'vi-ant-design:dropbox-square-filled', + 'vi-ant-design:edit-filled', + 'vi-ant-design:edit-outlined', + 'vi-ant-design:edit-twotone', + 'vi-ant-design:ellipsis-outlined', + 'vi-ant-design:enter-outlined', + 'vi-ant-design:environment-filled', + 'vi-ant-design:environment-outlined', + 'vi-ant-design:environment-twotone', + 'vi-ant-design:euro-circle-filled', + 'vi-ant-design:euro-circle-outlined', + 'vi-ant-design:euro-circle-twotone', + 'vi-ant-design:euro-outlined', + 'vi-ant-design:euro-twotone', + 'vi-ant-design:exception-outlined', + 'vi-ant-design:exclamation-circle-filled', + 'vi-ant-design:exclamation-circle-outlined', + 'vi-ant-design:exclamation-circle-twotone', + 'vi-ant-design:exclamation-outlined', + 'vi-ant-design:expand-alt-outlined', + 'vi-ant-design:expand-outlined', + 'vi-ant-design:experiment-filled', + 'vi-ant-design:experiment-outlined', + 'vi-ant-design:experiment-twotone', + 'vi-ant-design:export-outlined', + 'vi-ant-design:eye-filled', + 'vi-ant-design:eye-invisible-filled', + 'vi-ant-design:eye-invisible-outlined', + 'vi-ant-design:eye-invisible-twotone', + 'vi-ant-design:eye-outlined', + 'vi-ant-design:eye-twotone', + 'vi-ant-design:facebook-filled', + 'vi-ant-design:facebook-outlined', + 'vi-ant-design:fall-outlined', + 'vi-ant-design:fast-backward-filled', + 'vi-ant-design:fast-backward-outlined', + 'vi-ant-design:fast-forward-filled', + 'vi-ant-design:fast-forward-outlined', + 'vi-ant-design:field-binary-outlined', + 'vi-ant-design:field-number-outlined', + 'vi-ant-design:field-string-outlined', + 'vi-ant-design:field-time-outlined', + 'vi-ant-design:file-add-filled', + 'vi-ant-design:file-add-outlined', + 'vi-ant-design:file-add-twotone', + 'vi-ant-design:file-done-outlined', + 'vi-ant-design:file-excel-filled', + 'vi-ant-design:file-excel-outlined', + 'vi-ant-design:file-excel-twotone', + 'vi-ant-design:file-exclamation-filled', + 'vi-ant-design:file-exclamation-outlined', + 'vi-ant-design:file-exclamation-twotone', + 'vi-ant-design:file-filled', + 'vi-ant-design:file-gif-outlined', + 'vi-ant-design:file-image-filled', + 'vi-ant-design:file-image-outlined', + 'vi-ant-design:file-image-twotone', + 'vi-ant-design:file-jpg-outlined', + 'vi-ant-design:file-markdown-filled', + 'vi-ant-design:file-markdown-outlined', + 'vi-ant-design:file-markdown-twotone', + 'vi-ant-design:file-outlined', + 'vi-ant-design:file-pdf-filled', + 'vi-ant-design:file-pdf-outlined', + 'vi-ant-design:file-pdf-twotone', + 'vi-ant-design:file-ppt-filled', + 'vi-ant-design:file-ppt-outlined', + 'vi-ant-design:file-ppt-twotone', + 'vi-ant-design:file-protect-outlined', + 'vi-ant-design:file-search-outlined', + 'vi-ant-design:file-sync-outlined', + 'vi-ant-design:file-text-filled', + 'vi-ant-design:file-text-outlined', + 'vi-ant-design:file-text-twotone', + 'vi-ant-design:file-twotone', + 'vi-ant-design:file-unknown-filled', + 'vi-ant-design:file-unknown-outlined', + 'vi-ant-design:file-unknown-twotone', + 'vi-ant-design:file-word-filled', + 'vi-ant-design:file-word-outlined', + 'vi-ant-design:file-word-twotone', + 'vi-ant-design:file-zip-filled', + 'vi-ant-design:file-zip-outlined', + 'vi-ant-design:file-zip-twotone', + 'vi-ant-design:filter-filled', + 'vi-ant-design:filter-outlined', + 'vi-ant-design:filter-twotone', + 'vi-ant-design:fire-filled', + 'vi-ant-design:fire-outlined', + 'vi-ant-design:fire-twotone', + 'vi-ant-design:flag-filled', + 'vi-ant-design:flag-outlined', + 'vi-ant-design:flag-twotone', + 'vi-ant-design:folder-add-filled', + 'vi-ant-design:folder-add-outlined', + 'vi-ant-design:folder-add-twotone', + 'vi-ant-design:folder-filled', + 'vi-ant-design:folder-open-filled', + 'vi-ant-design:folder-open-outlined', + 'vi-ant-design:folder-open-twotone', + 'vi-ant-design:folder-outlined', + 'vi-ant-design:folder-twotone', + 'vi-ant-design:folder-view-outlined', + 'vi-ant-design:font-colors-outlined', + 'vi-ant-design:font-size-outlined', + 'vi-ant-design:fork-outlined', + 'vi-ant-design:form-outlined', + 'vi-ant-design:format-painter-filled', + 'vi-ant-design:format-painter-outlined', + 'vi-ant-design:forward-filled', + 'vi-ant-design:forward-outlined', + 'vi-ant-design:frown-filled', + 'vi-ant-design:frown-outlined', + 'vi-ant-design:frown-twotone', + 'vi-ant-design:fullscreen-exit-outlined', + 'vi-ant-design:fullscreen-outlined', + 'vi-ant-design:function-outlined', + 'vi-ant-design:fund-filled', + 'vi-ant-design:fund-outlined', + 'vi-ant-design:fund-projection-screen-outlined', + 'vi-ant-design:fund-twotone', + 'vi-ant-design:fund-view-outlined', + 'vi-ant-design:funnel-plot-filled', + 'vi-ant-design:funnel-plot-outlined', + 'vi-ant-design:funnel-plot-twotone', + 'vi-ant-design:gateway-outlined', + 'vi-ant-design:gif-outlined', + 'vi-ant-design:gift-filled', + 'vi-ant-design:gift-outlined', + 'vi-ant-design:gift-twotone', + 'vi-ant-design:github-filled', + 'vi-ant-design:github-outlined', + 'vi-ant-design:gitlab-filled', + 'vi-ant-design:gitlab-outlined', + 'vi-ant-design:global-outlined', + 'vi-ant-design:gold-filled', + 'vi-ant-design:gold-outlined', + 'vi-ant-design:gold-twotone', + 'vi-ant-design:golden-filled', + 'vi-ant-design:google-circle-filled', + 'vi-ant-design:google-outlined', + 'vi-ant-design:google-plus-circle-filled', + 'vi-ant-design:google-plus-outlined', + 'vi-ant-design:google-plus-square-filled', + 'vi-ant-design:google-square-filled', + 'vi-ant-design:group-outlined', + 'vi-ant-design:harmony-o-s-outlined', + 'vi-ant-design:hdd-filled', + 'vi-ant-design:hdd-outlined', + 'vi-ant-design:hdd-twotone', + 'vi-ant-design:heart-filled', + 'vi-ant-design:heart-outlined', + 'vi-ant-design:heart-twotone', + 'vi-ant-design:heat-map-outlined', + 'vi-ant-design:highlight-filled', + 'vi-ant-design:highlight-outlined', + 'vi-ant-design:highlight-twotone', + 'vi-ant-design:history-outlined', + 'vi-ant-design:holder-outlined', + 'vi-ant-design:home-filled', + 'vi-ant-design:home-outlined', + 'vi-ant-design:home-twotone', + 'vi-ant-design:hourglass-filled', + 'vi-ant-design:hourglass-outlined', + 'vi-ant-design:hourglass-twotone', + 'vi-ant-design:html5-filled', + 'vi-ant-design:html5-outlined', + 'vi-ant-design:html5-twotone', + 'vi-ant-design:idcard-filled', + 'vi-ant-design:idcard-outlined', + 'vi-ant-design:idcard-twotone', + 'vi-ant-design:ie-circle-filled', + 'vi-ant-design:ie-outlined', + 'vi-ant-design:ie-square-filled', + 'vi-ant-design:import-outlined', + 'vi-ant-design:inbox-outlined', + 'vi-ant-design:info-circle-filled', + 'vi-ant-design:info-circle-outlined', + 'vi-ant-design:info-circle-twotone', + 'vi-ant-design:info-outlined', + 'vi-ant-design:insert-row-above-outlined', + 'vi-ant-design:insert-row-below-outlined', + 'vi-ant-design:insert-row-left-outlined', + 'vi-ant-design:insert-row-right-outlined', + 'vi-ant-design:instagram-filled', + 'vi-ant-design:instagram-outlined', + 'vi-ant-design:insurance-filled', + 'vi-ant-design:insurance-outlined', + 'vi-ant-design:insurance-twotone', + 'vi-ant-design:interaction-filled', + 'vi-ant-design:interaction-outlined', + 'vi-ant-design:interaction-twotone', + 'vi-ant-design:issues-close-outlined', + 'vi-ant-design:italic-outlined', + 'vi-ant-design:java-outlined', + 'vi-ant-design:java-script-outlined', + 'vi-ant-design:key-outlined', + 'vi-ant-design:kubernetes-outlined', + 'vi-ant-design:laptop-outlined', + 'vi-ant-design:layout-filled', + 'vi-ant-design:layout-outlined', + 'vi-ant-design:layout-twotone', + 'vi-ant-design:left-circle-filled', + 'vi-ant-design:left-circle-outlined', + 'vi-ant-design:left-circle-twotone', + 'vi-ant-design:left-outlined', + 'vi-ant-design:left-square-filled', + 'vi-ant-design:left-square-outlined', + 'vi-ant-design:left-square-twotone', + 'vi-ant-design:like-filled', + 'vi-ant-design:like-outlined', + 'vi-ant-design:like-twotone', + 'vi-ant-design:line-chart-outlined', + 'vi-ant-design:line-height-outlined', + 'vi-ant-design:line-outlined', + 'vi-ant-design:link-outlined', + 'vi-ant-design:linkedin-filled', + 'vi-ant-design:linkedin-outlined', + 'vi-ant-design:linux-outlined', + 'vi-ant-design:loading-3-quarters-outlined', + 'vi-ant-design:loading-outlined', + 'vi-ant-design:lock-filled', + 'vi-ant-design:lock-outlined', + 'vi-ant-design:lock-twotone', + 'vi-ant-design:login-outlined', + 'vi-ant-design:logout-outlined', + 'vi-ant-design:mac-command-filled', + 'vi-ant-design:mac-command-outlined', + 'vi-ant-design:mail-filled', + 'vi-ant-design:mail-outlined', + 'vi-ant-design:mail-twotone', + 'vi-ant-design:man-outlined', + 'vi-ant-design:medicine-box-filled', + 'vi-ant-design:medicine-box-outlined', + 'vi-ant-design:medicine-box-twotone', + 'vi-ant-design:medium-circle-filled', + 'vi-ant-design:medium-outlined', + 'vi-ant-design:medium-square-filled', + 'vi-ant-design:medium-workmark-outlined', + 'vi-ant-design:meh-filled', + 'vi-ant-design:meh-outlined', + 'vi-ant-design:meh-twotone', + 'vi-ant-design:menu-fold-outlined', + 'vi-ant-design:menu-outlined', + 'vi-ant-design:menu-unfold-outlined', + 'vi-ant-design:merge-cells-outlined', + 'vi-ant-design:merge-filled', + 'vi-ant-design:merge-outlined', + 'vi-ant-design:message-filled', + 'vi-ant-design:message-outlined', + 'vi-ant-design:message-twotone', + 'vi-ant-design:minus-circle-filled', + 'vi-ant-design:minus-circle-outlined', + 'vi-ant-design:minus-circle-twotone', + 'vi-ant-design:minus-outlined', + 'vi-ant-design:minus-square-filled', + 'vi-ant-design:minus-square-outlined', + 'vi-ant-design:minus-square-twotone', + 'vi-ant-design:mobile-filled', + 'vi-ant-design:mobile-outlined', + 'vi-ant-design:mobile-twotone', + 'vi-ant-design:money-collect-filled', + 'vi-ant-design:money-collect-outlined', + 'vi-ant-design:money-collect-twotone', + 'vi-ant-design:monitor-outlined', + 'vi-ant-design:moon-filled', + 'vi-ant-design:moon-outlined', + 'vi-ant-design:more-outlined', + 'vi-ant-design:muted-filled', + 'vi-ant-design:muted-outlined', + 'vi-ant-design:node-collapse-outlined', + 'vi-ant-design:node-expand-outlined', + 'vi-ant-design:node-index-outlined', + 'vi-ant-design:notification-filled', + 'vi-ant-design:notification-outlined', + 'vi-ant-design:notification-twotone', + 'vi-ant-design:number-outlined', + 'vi-ant-design:one-to-one-outlined', + 'vi-ant-design:open-a-i-filled', + 'vi-ant-design:open-a-i-outlined', + 'vi-ant-design:ordered-list-outlined', + 'vi-ant-design:paper-clip-outlined', + 'vi-ant-design:partition-outlined', + 'vi-ant-design:pause-circle-filled', + 'vi-ant-design:pause-circle-outlined', + 'vi-ant-design:pause-circle-twotone', + 'vi-ant-design:pause-outlined', + 'vi-ant-design:pay-circle-filled', + 'vi-ant-design:pay-circle-outlined', + 'vi-ant-design:percentage-outlined', + 'vi-ant-design:phone-filled', + 'vi-ant-design:phone-outlined', + 'vi-ant-design:phone-twotone', + 'vi-ant-design:pic-center-outlined', + 'vi-ant-design:pic-left-outlined', + 'vi-ant-design:pic-right-outlined', + 'vi-ant-design:picture-filled', + 'vi-ant-design:picture-outlined', + 'vi-ant-design:picture-twotone', + 'vi-ant-design:pie-chart-filled', + 'vi-ant-design:pie-chart-outlined', + 'vi-ant-design:pie-chart-twotone', + 'vi-ant-design:pinterest-filled', + 'vi-ant-design:pinterest-outlined', + 'vi-ant-design:play-circle-filled', + 'vi-ant-design:play-circle-outlined', + 'vi-ant-design:play-circle-twotone', + 'vi-ant-design:play-square-filled', + 'vi-ant-design:play-square-outlined', + 'vi-ant-design:play-square-twotone', + 'vi-ant-design:plus-circle-filled', + 'vi-ant-design:plus-circle-outlined', + 'vi-ant-design:plus-circle-twotone', + 'vi-ant-design:plus-outlined', + 'vi-ant-design:plus-square-filled', + 'vi-ant-design:plus-square-outlined', + 'vi-ant-design:plus-square-twotone', + 'vi-ant-design:pound-circle-filled', + 'vi-ant-design:pound-circle-outlined', + 'vi-ant-design:pound-circle-twotone', + 'vi-ant-design:pound-outlined', + 'vi-ant-design:poweroff-outlined', + 'vi-ant-design:printer-filled', + 'vi-ant-design:printer-outlined', + 'vi-ant-design:printer-twotone', + 'vi-ant-design:product-filled', + 'vi-ant-design:product-outlined', + 'vi-ant-design:profile-filled', + 'vi-ant-design:profile-outlined', + 'vi-ant-design:profile-twotone', + 'vi-ant-design:project-filled', + 'vi-ant-design:project-outlined', + 'vi-ant-design:project-twotone', + 'vi-ant-design:property-safety-filled', + 'vi-ant-design:property-safety-outlined', + 'vi-ant-design:property-safety-twotone', + 'vi-ant-design:pull-request-outlined', + 'vi-ant-design:pushpin-filled', + 'vi-ant-design:pushpin-outlined', + 'vi-ant-design:pushpin-twotone', + 'vi-ant-design:python-outlined', + 'vi-ant-design:qq-circle-filled', + 'vi-ant-design:qq-outlined', + 'vi-ant-design:qq-square-filled', + 'vi-ant-design:qrcode-outlined', + 'vi-ant-design:question-circle-filled', + 'vi-ant-design:question-circle-outlined', + 'vi-ant-design:question-circle-twotone', + 'vi-ant-design:question-outlined', + 'vi-ant-design:radar-chart-outlined', + 'vi-ant-design:radius-bottomleft-outlined', + 'vi-ant-design:radius-bottomright-outlined', + 'vi-ant-design:radius-setting-outlined', + 'vi-ant-design:radius-upleft-outlined', + 'vi-ant-design:radius-upright-outlined', + 'vi-ant-design:read-filled', + 'vi-ant-design:read-outlined', + 'vi-ant-design:reconciliation-filled', + 'vi-ant-design:reconciliation-outlined', + 'vi-ant-design:reconciliation-twotone', + 'vi-ant-design:red-envelope-filled', + 'vi-ant-design:red-envelope-outlined', + 'vi-ant-design:red-envelope-twotone', + 'vi-ant-design:reddit-circle-filled', + 'vi-ant-design:reddit-outlined', + 'vi-ant-design:reddit-square-filled', + 'vi-ant-design:redo-outlined', + 'vi-ant-design:reload-outlined', + 'vi-ant-design:rest-filled', + 'vi-ant-design:rest-outlined', + 'vi-ant-design:rest-twotone', + 'vi-ant-design:retweet-outlined', + 'vi-ant-design:right-circle-filled', + 'vi-ant-design:right-circle-outlined', + 'vi-ant-design:right-circle-twotone', + 'vi-ant-design:right-outlined', + 'vi-ant-design:right-square-filled', + 'vi-ant-design:right-square-outlined', + 'vi-ant-design:right-square-twotone', + 'vi-ant-design:rise-outlined', + 'vi-ant-design:robot-filled', + 'vi-ant-design:robot-outlined', + 'vi-ant-design:rocket-filled', + 'vi-ant-design:rocket-outlined', + 'vi-ant-design:rocket-twotone', + 'vi-ant-design:rollback-outlined', + 'vi-ant-design:rotate-left-outlined', + 'vi-ant-design:rotate-right-outlined', + 'vi-ant-design:ruby-outlined', + 'vi-ant-design:safety-certificate-filled', + 'vi-ant-design:safety-certificate-outlined', + 'vi-ant-design:safety-certificate-twotone', + 'vi-ant-design:safety-outlined', + 'vi-ant-design:save-filled', + 'vi-ant-design:save-outlined', + 'vi-ant-design:save-twotone', + 'vi-ant-design:scan-outlined', + 'vi-ant-design:schedule-filled', + 'vi-ant-design:schedule-outlined', + 'vi-ant-design:schedule-twotone', + 'vi-ant-design:scissor-outlined', + 'vi-ant-design:search-outlined', + 'vi-ant-design:security-scan-filled', + 'vi-ant-design:security-scan-outlined', + 'vi-ant-design:security-scan-twotone', + 'vi-ant-design:select-outlined', + 'vi-ant-design:send-outlined', + 'vi-ant-design:setting-filled', + 'vi-ant-design:setting-outlined', + 'vi-ant-design:setting-twotone', + 'vi-ant-design:shake-outlined', + 'vi-ant-design:share-alt-outlined', + 'vi-ant-design:shop-filled', + 'vi-ant-design:shop-outlined', + 'vi-ant-design:shop-twotone', + 'vi-ant-design:shopping-cart-outlined', + 'vi-ant-design:shopping-filled', + 'vi-ant-design:shopping-outlined', + 'vi-ant-design:shopping-twotone', + 'vi-ant-design:shrink-outlined', + 'vi-ant-design:signal-filled', + 'vi-ant-design:signature-filled', + 'vi-ant-design:signature-outlined', + 'vi-ant-design:sisternode-outlined', + 'vi-ant-design:sketch-circle-filled', + 'vi-ant-design:sketch-outlined', + 'vi-ant-design:sketch-square-filled', + 'vi-ant-design:skin-filled', + 'vi-ant-design:skin-outlined', + 'vi-ant-design:skin-twotone', + 'vi-ant-design:skype-filled', + 'vi-ant-design:skype-outlined', + 'vi-ant-design:slack-circle-filled', + 'vi-ant-design:slack-outlined', + 'vi-ant-design:slack-square-filled', + 'vi-ant-design:slack-square-outlined', + 'vi-ant-design:sliders-filled', + 'vi-ant-design:sliders-outlined', + 'vi-ant-design:sliders-twotone', + 'vi-ant-design:small-dash-outlined', + 'vi-ant-design:smile-filled', + 'vi-ant-design:smile-outlined', + 'vi-ant-design:smile-twotone', + 'vi-ant-design:snippets-filled', + 'vi-ant-design:snippets-outlined', + 'vi-ant-design:snippets-twotone', + 'vi-ant-design:solution-outlined', + 'vi-ant-design:sort-ascending-outlined', + 'vi-ant-design:sort-descending-outlined', + 'vi-ant-design:sound-filled', + 'vi-ant-design:sound-outlined', + 'vi-ant-design:sound-twotone', + 'vi-ant-design:split-cells-outlined', + 'vi-ant-design:spotify-filled', + 'vi-ant-design:spotify-outlined', + 'vi-ant-design:star-filled', + 'vi-ant-design:star-outlined', + 'vi-ant-design:star-twotone', + 'vi-ant-design:step-backward-filled', + 'vi-ant-design:step-backward-outlined', + 'vi-ant-design:step-forward-filled', + 'vi-ant-design:step-forward-outlined', + 'vi-ant-design:stock-outlined', + 'vi-ant-design:stop-filled', + 'vi-ant-design:stop-outlined', + 'vi-ant-design:stop-twotone', + 'vi-ant-design:strikethrough-outlined', + 'vi-ant-design:subnode-outlined', + 'vi-ant-design:sun-filled', + 'vi-ant-design:sun-outlined', + 'vi-ant-design:swap-left-outlined', + 'vi-ant-design:swap-outlined', + 'vi-ant-design:swap-right-outlined', + 'vi-ant-design:switcher-filled', + 'vi-ant-design:switcher-outlined', + 'vi-ant-design:switcher-twotone', + 'vi-ant-design:sync-outlined', + 'vi-ant-design:table-outlined', + 'vi-ant-design:tablet-filled', + 'vi-ant-design:tablet-outlined', + 'vi-ant-design:tablet-twotone', + 'vi-ant-design:tag-filled', + 'vi-ant-design:tag-outlined', + 'vi-ant-design:tag-twotone', + 'vi-ant-design:tags-filled', + 'vi-ant-design:tags-outlined', + 'vi-ant-design:tags-twotone', + 'vi-ant-design:taobao-circle-filled', + 'vi-ant-design:taobao-circle-outlined', + 'vi-ant-design:taobao-outlined', + 'vi-ant-design:taobao-square-filled', + 'vi-ant-design:team-outlined', + 'vi-ant-design:thunderbolt-filled', + 'vi-ant-design:thunderbolt-outlined', + 'vi-ant-design:thunderbolt-twotone', + 'vi-ant-design:tik-tok-filled', + 'vi-ant-design:tik-tok-outlined', + 'vi-ant-design:to-top-outlined', + 'vi-ant-design:tool-filled', + 'vi-ant-design:tool-outlined', + 'vi-ant-design:tool-twotone', + 'vi-ant-design:trademark-circle-filled', + 'vi-ant-design:trademark-circle-outlined', + 'vi-ant-design:trademark-circle-twotone', + 'vi-ant-design:trademark-outlined', + 'vi-ant-design:transaction-outlined', + 'vi-ant-design:translation-outlined', + 'vi-ant-design:trophy-filled', + 'vi-ant-design:trophy-outlined', + 'vi-ant-design:trophy-twotone', + 'vi-ant-design:truck-filled', + 'vi-ant-design:truck-outlined', + 'vi-ant-design:twitch-outlined', + 'vi-ant-design:twitter-circle-filled', + 'vi-ant-design:twitter-outlined', + 'vi-ant-design:twitter-square-filled', + 'vi-ant-design:underline-outlined', + 'vi-ant-design:undo-outlined', + 'vi-ant-design:ungroup-outlined', + 'vi-ant-design:unlock-filled', + 'vi-ant-design:unlock-outlined', + 'vi-ant-design:unlock-twotone', + 'vi-ant-design:unordered-list-outlined', + 'vi-ant-design:up-circle-filled', + 'vi-ant-design:up-circle-outlined', + 'vi-ant-design:up-circle-twotone', + 'vi-ant-design:up-outlined', + 'vi-ant-design:up-square-filled', + 'vi-ant-design:up-square-outlined', + 'vi-ant-design:up-square-twotone', + 'vi-ant-design:upload-outlined', + 'vi-ant-design:usb-filled', + 'vi-ant-design:usb-outlined', + 'vi-ant-design:usb-twotone', + 'vi-ant-design:user-add-outlined', + 'vi-ant-design:user-delete-outlined', + 'vi-ant-design:user-outlined', + 'vi-ant-design:user-switch-outlined', + 'vi-ant-design:usergroup-add-outlined', + 'vi-ant-design:usergroup-delete-outlined', + 'vi-ant-design:verified-outlined', + 'vi-ant-design:vertical-align-bottom-outlined', + 'vi-ant-design:vertical-align-middle-outlined', + 'vi-ant-design:vertical-align-top-outlined', + 'vi-ant-design:vertical-left-outlined', + 'vi-ant-design:vertical-right-outlined', + 'vi-ant-design:video-camera-add-outlined', + 'vi-ant-design:video-camera-filled', + 'vi-ant-design:video-camera-outlined', + 'vi-ant-design:video-camera-twotone', + 'vi-ant-design:wallet-filled', + 'vi-ant-design:wallet-outlined', + 'vi-ant-design:wallet-twotone', + 'vi-ant-design:warning-filled', + 'vi-ant-design:warning-outlined', + 'vi-ant-design:warning-twotone', + 'vi-ant-design:wechat-filled', + 'vi-ant-design:wechat-outlined', + 'vi-ant-design:wechat-work-filled', + 'vi-ant-design:wechat-work-outlined', + 'vi-ant-design:weibo-circle-filled', + 'vi-ant-design:weibo-circle-outlined', + 'vi-ant-design:weibo-outlined', + 'vi-ant-design:weibo-square-filled', + 'vi-ant-design:weibo-square-outlined', + 'vi-ant-design:whats-app-outlined', + 'vi-ant-design:wifi-outlined', + 'vi-ant-design:windows-filled', + 'vi-ant-design:windows-outlined', + 'vi-ant-design:woman-outlined', + 'vi-ant-design:x-filled', + 'vi-ant-design:x-outlined', + 'vi-ant-design:yahoo-filled', + 'vi-ant-design:yahoo-outlined', + 'vi-ant-design:youtube-filled', + 'vi-ant-design:youtube-outlined', + 'vi-ant-design:yuque-filled', + 'vi-ant-design:yuque-outlined', + 'vi-ant-design:zhihu-circle-filled', + 'vi-ant-design:zhihu-outlined', + 'vi-ant-design:zhihu-square-filled', + 'vi-ant-design:zoom-in-outlined', + 'vi-ant-design:zoom-out-outlined' + ] +} diff --git a/promo-ui2/src/components/IconPicker/src/data/icons.ep.ts b/promo-ui2/src/components/IconPicker/src/data/icons.ep.ts new file mode 100644 index 0000000..b3918be --- /dev/null +++ b/promo-ui2/src/components/IconPicker/src/data/icons.ep.ts @@ -0,0 +1,299 @@ +export default { + name: 'Element Plus', + prefix: 'vi-ep', + icons: [ + 'vi-ep:add-location', + 'vi-ep:aim', + 'vi-ep:alarm-clock', + 'vi-ep:apple', + 'vi-ep:arrow-down', + 'vi-ep:arrow-down-bold', + 'vi-ep:arrow-left', + 'vi-ep:arrow-left-bold', + 'vi-ep:arrow-right', + 'vi-ep:arrow-right-bold', + 'vi-ep:arrow-up', + 'vi-ep:arrow-up-bold', + 'vi-ep:avatar', + 'vi-ep:back', + 'vi-ep:baseball', + 'vi-ep:basketball', + 'vi-ep:bell', + 'vi-ep:bell-filled', + 'vi-ep:bicycle', + 'vi-ep:bottom', + 'vi-ep:bottom-left', + 'vi-ep:bottom-right', + 'vi-ep:bowl', + 'vi-ep:box', + 'vi-ep:briefcase', + 'vi-ep:brush', + 'vi-ep:brush-filled', + 'vi-ep:burger', + 'vi-ep:calendar', + 'vi-ep:camera', + 'vi-ep:camera-filled', + 'vi-ep:caret-bottom', + 'vi-ep:caret-left', + 'vi-ep:caret-right', + 'vi-ep:caret-top', + 'vi-ep:cellphone', + 'vi-ep:chat-dot-round', + 'vi-ep:chat-dot-square', + 'vi-ep:chat-line-round', + 'vi-ep:chat-line-square', + 'vi-ep:chat-round', + 'vi-ep:chat-square', + 'vi-ep:check', + 'vi-ep:checked', + 'vi-ep:cherry', + 'vi-ep:chicken', + 'vi-ep:chrome-filled', + 'vi-ep:circle-check', + 'vi-ep:circle-check-filled', + 'vi-ep:circle-close', + 'vi-ep:circle-close-filled', + 'vi-ep:circle-plus', + 'vi-ep:circle-plus-filled', + 'vi-ep:clock', + 'vi-ep:close', + 'vi-ep:close-bold', + 'vi-ep:cloudy', + 'vi-ep:coffee', + 'vi-ep:coffee-cup', + 'vi-ep:coin', + 'vi-ep:cold-drink', + 'vi-ep:collection', + 'vi-ep:collection-tag', + 'vi-ep:comment', + 'vi-ep:compass', + 'vi-ep:connection', + 'vi-ep:coordinate', + 'vi-ep:copy-document', + 'vi-ep:cpu', + 'vi-ep:credit-card', + 'vi-ep:crop', + 'vi-ep:d-arrow-left', + 'vi-ep:d-arrow-right', + 'vi-ep:d-caret', + 'vi-ep:data-analysis', + 'vi-ep:data-board', + 'vi-ep:data-line', + 'vi-ep:delete', + 'vi-ep:delete-filled', + 'vi-ep:delete-location', + 'vi-ep:dessert', + 'vi-ep:discount', + 'vi-ep:dish', + 'vi-ep:dish-dot', + 'vi-ep:document', + 'vi-ep:document-add', + 'vi-ep:document-checked', + 'vi-ep:document-copy', + 'vi-ep:document-delete', + 'vi-ep:document-remove', + 'vi-ep:download', + 'vi-ep:drizzling', + 'vi-ep:edit', + 'vi-ep:edit-pen', + 'vi-ep:eleme', + 'vi-ep:eleme-filled', + 'vi-ep:element-plus', + 'vi-ep:expand', + 'vi-ep:failed', + 'vi-ep:female', + 'vi-ep:files', + 'vi-ep:film', + 'vi-ep:filter', + 'vi-ep:finished', + 'vi-ep:first-aid-kit', + 'vi-ep:flag', + 'vi-ep:fold', + 'vi-ep:folder', + 'vi-ep:folder-add', + 'vi-ep:folder-checked', + 'vi-ep:folder-delete', + 'vi-ep:folder-opened', + 'vi-ep:folder-remove', + 'vi-ep:food', + 'vi-ep:football', + 'vi-ep:fork-spoon', + 'vi-ep:fries', + 'vi-ep:full-screen', + 'vi-ep:goblet', + 'vi-ep:goblet-full', + 'vi-ep:goblet-square', + 'vi-ep:goblet-square-full', + 'vi-ep:gold-medal', + 'vi-ep:goods', + 'vi-ep:goods-filled', + 'vi-ep:grape', + 'vi-ep:grid', + 'vi-ep:guide', + 'vi-ep:handbag', + 'vi-ep:headset', + 'vi-ep:help', + 'vi-ep:help-filled', + 'vi-ep:hide', + 'vi-ep:histogram', + 'vi-ep:home-filled', + 'vi-ep:hot-water', + 'vi-ep:house', + 'vi-ep:ice-cream', + 'vi-ep:ice-cream-round', + 'vi-ep:ice-cream-square', + 'vi-ep:ice-drink', + 'vi-ep:ice-tea', + 'vi-ep:info-filled', + 'vi-ep:iphone', + 'vi-ep:key', + 'vi-ep:knife-fork', + 'vi-ep:lightning', + 'vi-ep:link', + 'vi-ep:list', + 'vi-ep:loading', + 'vi-ep:location', + 'vi-ep:location-filled', + 'vi-ep:location-information', + 'vi-ep:lock', + 'vi-ep:lollipop', + 'vi-ep:magic-stick', + 'vi-ep:magnet', + 'vi-ep:male', + 'vi-ep:management', + 'vi-ep:map-location', + 'vi-ep:medal', + 'vi-ep:memo', + 'vi-ep:menu', + 'vi-ep:message', + 'vi-ep:message-box', + 'vi-ep:mic', + 'vi-ep:microphone', + 'vi-ep:milk-tea', + 'vi-ep:minus', + 'vi-ep:money', + 'vi-ep:monitor', + 'vi-ep:moon', + 'vi-ep:moon-night', + 'vi-ep:more', + 'vi-ep:more-filled', + 'vi-ep:mostly-cloudy', + 'vi-ep:mouse', + 'vi-ep:mug', + 'vi-ep:mute', + 'vi-ep:mute-notification', + 'vi-ep:no-smoking', + 'vi-ep:notebook', + 'vi-ep:notification', + 'vi-ep:odometer', + 'vi-ep:office-building', + 'vi-ep:open', + 'vi-ep:operation', + 'vi-ep:opportunity', + 'vi-ep:orange', + 'vi-ep:paperclip', + 'vi-ep:partly-cloudy', + 'vi-ep:pear', + 'vi-ep:phone', + 'vi-ep:phone-filled', + 'vi-ep:picture', + 'vi-ep:picture-filled', + 'vi-ep:picture-rounded', + 'vi-ep:pie-chart', + 'vi-ep:place', + 'vi-ep:platform', + 'vi-ep:plus', + 'vi-ep:pointer', + 'vi-ep:position', + 'vi-ep:postcard', + 'vi-ep:pouring', + 'vi-ep:present', + 'vi-ep:price-tag', + 'vi-ep:printer', + 'vi-ep:promotion', + 'vi-ep:quartz-watch', + 'vi-ep:question-filled', + 'vi-ep:rank', + 'vi-ep:reading', + 'vi-ep:reading-lamp', + 'vi-ep:refresh', + 'vi-ep:refresh-left', + 'vi-ep:refresh-right', + 'vi-ep:refrigerator', + 'vi-ep:remove', + 'vi-ep:remove-filled', + 'vi-ep:right', + 'vi-ep:scale-to-original', + 'vi-ep:school', + 'vi-ep:scissor', + 'vi-ep:search', + 'vi-ep:select', + 'vi-ep:sell', + 'vi-ep:semi-select', + 'vi-ep:service', + 'vi-ep:set-up', + 'vi-ep:setting', + 'vi-ep:share', + 'vi-ep:ship', + 'vi-ep:shop', + 'vi-ep:shopping-bag', + 'vi-ep:shopping-cart', + 'vi-ep:shopping-cart-full', + 'vi-ep:shopping-trolley', + 'vi-ep:smoking', + 'vi-ep:soccer', + 'vi-ep:sold-out', + 'vi-ep:sort', + 'vi-ep:sort-down', + 'vi-ep:sort-up', + 'vi-ep:stamp', + 'vi-ep:star', + 'vi-ep:star-filled', + 'vi-ep:stopwatch', + 'vi-ep:success-filled', + 'vi-ep:sugar', + 'vi-ep:suitcase', + 'vi-ep:suitcase-line', + 'vi-ep:sunny', + 'vi-ep:sunrise', + 'vi-ep:sunset', + 'vi-ep:switch', + 'vi-ep:switch-button', + 'vi-ep:switch-filled', + 'vi-ep:takeaway-box', + 'vi-ep:ticket', + 'vi-ep:tickets', + 'vi-ep:timer', + 'vi-ep:toilet-paper', + 'vi-ep:tools', + 'vi-ep:top', + 'vi-ep:top-left', + 'vi-ep:top-right', + 'vi-ep:trend-charts', + 'vi-ep:trophy', + 'vi-ep:trophy-base', + 'vi-ep:turn-off', + 'vi-ep:umbrella', + 'vi-ep:unlock', + 'vi-ep:upload', + 'vi-ep:upload-filled', + 'vi-ep:user', + 'vi-ep:user-filled', + 'vi-ep:van', + 'vi-ep:video-camera', + 'vi-ep:video-camera-filled', + 'vi-ep:video-pause', + 'vi-ep:video-play', + 'vi-ep:view', + 'vi-ep:wallet', + 'vi-ep:wallet-filled', + 'vi-ep:warn-triangle-filled', + 'vi-ep:warning', + 'vi-ep:warning-filled', + 'vi-ep:watch', + 'vi-ep:watermelon', + 'vi-ep:wind-power', + 'vi-ep:zoom-in', + 'vi-ep:zoom-out' + ] +} diff --git a/promo-ui2/src/components/IconPicker/src/data/icons.tdesign.ts b/promo-ui2/src/components/IconPicker/src/data/icons.tdesign.ts new file mode 100644 index 0000000..77b298b --- /dev/null +++ b/promo-ui2/src/components/IconPicker/src/data/icons.tdesign.ts @@ -0,0 +1,1209 @@ +export default { + name: 'TDesign Icons', + prefix: 'vi-tdesign', + icons: [ + 'vi-tdesign:activity', + 'vi-tdesign:add', + 'vi-tdesign:add-and-subtract', + 'vi-tdesign:add-circle', + 'vi-tdesign:add-rectangle', + 'vi-tdesign:address-book', + 'vi-tdesign:adjustment', + 'vi-tdesign:airplay-wave', + 'vi-tdesign:alarm', + 'vi-tdesign:alarm-add', + 'vi-tdesign:alarm-off', + 'vi-tdesign:align-top', + 'vi-tdesign:align-vertical', + 'vi-tdesign:alpha', + 'vi-tdesign:analytics', + 'vi-tdesign:anchor', + 'vi-tdesign:angry', + 'vi-tdesign:animation', + 'vi-tdesign:animation-1', + 'vi-tdesign:anticlockwise', + 'vi-tdesign:api', + 'vi-tdesign:app', + 'vi-tdesign:apple', + 'vi-tdesign:application', + 'vi-tdesign:architecture-hui-style', + 'vi-tdesign:archway', + 'vi-tdesign:archway-1', + 'vi-tdesign:arrow-down', + 'vi-tdesign:arrow-down-circle', + 'vi-tdesign:arrow-down-rectangle', + 'vi-tdesign:arrow-left', + 'vi-tdesign:arrow-left-circle', + 'vi-tdesign:arrow-left-down', + 'vi-tdesign:arrow-left-down-circle', + 'vi-tdesign:arrow-left-right-1', + 'vi-tdesign:arrow-left-right-2', + 'vi-tdesign:arrow-left-right-3', + 'vi-tdesign:arrow-left-right-circle', + 'vi-tdesign:arrow-left-up', + 'vi-tdesign:arrow-left-up-circle', + 'vi-tdesign:arrow-right', + 'vi-tdesign:arrow-right-circle', + 'vi-tdesign:arrow-right-down', + 'vi-tdesign:arrow-right-down-circle', + 'vi-tdesign:arrow-right-up', + 'vi-tdesign:arrow-right-up-circle', + 'vi-tdesign:arrow-triangle-down', + 'vi-tdesign:arrow-triangle-down-filled', + 'vi-tdesign:arrow-triangle-up', + 'vi-tdesign:arrow-triangle-up-filled', + 'vi-tdesign:arrow-up', + 'vi-tdesign:arrow-up-circle', + 'vi-tdesign:arrow-up-down-1', + 'vi-tdesign:arrow-up-down-2', + 'vi-tdesign:arrow-up-down-3', + 'vi-tdesign:arrow-up-down-circle', + 'vi-tdesign:artboard', + 'vi-tdesign:article', + 'vi-tdesign:assignment', + 'vi-tdesign:assignment-checked', + 'vi-tdesign:assignment-code', + 'vi-tdesign:assignment-error', + 'vi-tdesign:assignment-user', + 'vi-tdesign:attach', + 'vi-tdesign:attic', + 'vi-tdesign:attic-1', + 'vi-tdesign:audio', + 'vi-tdesign:awkward', + 'vi-tdesign:backtop', + 'vi-tdesign:backtop-rectangle', + 'vi-tdesign:backup', + 'vi-tdesign:backward', + 'vi-tdesign:bad-laugh', + 'vi-tdesign:bamboo-shoot', + 'vi-tdesign:banana', + 'vi-tdesign:barbecue', + 'vi-tdesign:barcode', + 'vi-tdesign:barcode-1', + 'vi-tdesign:base-station', + 'vi-tdesign:battery', + 'vi-tdesign:battery-add', + 'vi-tdesign:battery-charging', + 'vi-tdesign:battery-low', + 'vi-tdesign:bean', + 'vi-tdesign:beer', + 'vi-tdesign:beta', + 'vi-tdesign:bifurcate', + 'vi-tdesign:bill', + 'vi-tdesign:blockchain', + 'vi-tdesign:bluetooth', + 'vi-tdesign:bone', + 'vi-tdesign:book', + 'vi-tdesign:book-open', + 'vi-tdesign:bookmark', + 'vi-tdesign:bookmark-add', + 'vi-tdesign:bookmark-checked', + 'vi-tdesign:bookmark-double', + 'vi-tdesign:bookmark-minus', + 'vi-tdesign:braces', + 'vi-tdesign:brackets', + 'vi-tdesign:bread', + 'vi-tdesign:bridge', + 'vi-tdesign:bridge-1', + 'vi-tdesign:bridge-2', + 'vi-tdesign:bridge-3', + 'vi-tdesign:bridge-4', + 'vi-tdesign:bridge-5', + 'vi-tdesign:bridge-6', + 'vi-tdesign:brightness', + 'vi-tdesign:brightness-1', + 'vi-tdesign:broccoli', + 'vi-tdesign:browse', + 'vi-tdesign:browse-gallery', + 'vi-tdesign:browse-off', + 'vi-tdesign:brush', + 'vi-tdesign:bug', + 'vi-tdesign:bug-report', + 'vi-tdesign:building', + 'vi-tdesign:building-1', + 'vi-tdesign:building-2', + 'vi-tdesign:building-3', + 'vi-tdesign:building-4', + 'vi-tdesign:building-5', + 'vi-tdesign:bulletpoint', + 'vi-tdesign:button', + 'vi-tdesign:cabbage', + 'vi-tdesign:cake', + 'vi-tdesign:calculation', + 'vi-tdesign:calculation-1', + 'vi-tdesign:calculator', + 'vi-tdesign:calculator-1', + 'vi-tdesign:calendar', + 'vi-tdesign:calendar-1', + 'vi-tdesign:calendar-2', + 'vi-tdesign:calendar-edit', + 'vi-tdesign:calendar-event', + 'vi-tdesign:call', + 'vi-tdesign:call-1', + 'vi-tdesign:call-cancel', + 'vi-tdesign:call-forwarded', + 'vi-tdesign:call-incoming', + 'vi-tdesign:call-off', + 'vi-tdesign:calm', + 'vi-tdesign:calm-1', + 'vi-tdesign:camera', + 'vi-tdesign:camera-1', + 'vi-tdesign:camera-2', + 'vi-tdesign:camera-off', + 'vi-tdesign:candy', + 'vi-tdesign:card', + 'vi-tdesign:cardmembership', + 'vi-tdesign:caret-down', + 'vi-tdesign:caret-down-small', + 'vi-tdesign:caret-left', + 'vi-tdesign:caret-left-small', + 'vi-tdesign:caret-right', + 'vi-tdesign:caret-right-small', + 'vi-tdesign:caret-up', + 'vi-tdesign:caret-up-small', + 'vi-tdesign:cart', + 'vi-tdesign:cart-add', + 'vi-tdesign:cast', + 'vi-tdesign:castle', + 'vi-tdesign:castle-1', + 'vi-tdesign:castle-2', + 'vi-tdesign:castle-3', + 'vi-tdesign:castle-4', + 'vi-tdesign:castle-5', + 'vi-tdesign:castle-6', + 'vi-tdesign:castle-7', + 'vi-tdesign:cat', + 'vi-tdesign:catalog', + 'vi-tdesign:cd', + 'vi-tdesign:celsius', + 'vi-tdesign:center-focus-strong', + 'vi-tdesign:centimeter', + 'vi-tdesign:certificate', + 'vi-tdesign:certificate-1', + 'vi-tdesign:chart', + 'vi-tdesign:chart-3d', + 'vi-tdesign:chart-add', + 'vi-tdesign:chart-analytics', + 'vi-tdesign:chart-area', + 'vi-tdesign:chart-area-multi', + 'vi-tdesign:chart-bar', + 'vi-tdesign:chart-bubble', + 'vi-tdesign:chart-colum', + 'vi-tdesign:chart-combo', + 'vi-tdesign:chart-line', + 'vi-tdesign:chart-line-data', + 'vi-tdesign:chart-line-data-1', + 'vi-tdesign:chart-line-multi', + 'vi-tdesign:chart-maximum', + 'vi-tdesign:chart-median', + 'vi-tdesign:chart-minimum', + 'vi-tdesign:chart-pie', + 'vi-tdesign:chart-radar', + 'vi-tdesign:chart-radial', + 'vi-tdesign:chart-ring', + 'vi-tdesign:chart-ring-1', + 'vi-tdesign:chart-scatter', + 'vi-tdesign:chart-stacked', + 'vi-tdesign:chat', + 'vi-tdesign:chat-add', + 'vi-tdesign:chat-bubble', + 'vi-tdesign:chat-bubble-1', + 'vi-tdesign:chat-bubble-add', + 'vi-tdesign:chat-bubble-error', + 'vi-tdesign:chat-bubble-help', + 'vi-tdesign:chat-bubble-history', + 'vi-tdesign:chat-bubble-locked', + 'vi-tdesign:chat-bubble-smile', + 'vi-tdesign:chat-checked', + 'vi-tdesign:chat-clear', + 'vi-tdesign:chat-double', + 'vi-tdesign:chat-error', + 'vi-tdesign:chat-heart', + 'vi-tdesign:chat-message', + 'vi-tdesign:chat-off', + 'vi-tdesign:chat-poll', + 'vi-tdesign:chat-setting', + 'vi-tdesign:check', + 'vi-tdesign:check-circle', + 'vi-tdesign:check-circle-filled', + 'vi-tdesign:check-double', + 'vi-tdesign:check-rectangle', + 'vi-tdesign:check-rectangle-filled', + 'vi-tdesign:cheese', + 'vi-tdesign:cherry', + 'vi-tdesign:chevron-down', + 'vi-tdesign:chevron-down-circle', + 'vi-tdesign:chevron-down-double', + 'vi-tdesign:chevron-down-double-s', + 'vi-tdesign:chevron-down-rectangle', + 'vi-tdesign:chevron-down-s', + 'vi-tdesign:chevron-left', + 'vi-tdesign:chevron-left-circle', + 'vi-tdesign:chevron-left-double', + 'vi-tdesign:chevron-left-double-s', + 'vi-tdesign:chevron-left-rectangle', + 'vi-tdesign:chevron-left-s', + 'vi-tdesign:chevron-right', + 'vi-tdesign:chevron-right-circle', + 'vi-tdesign:chevron-right-double', + 'vi-tdesign:chevron-right-double-s', + 'vi-tdesign:chevron-right-rectangle', + 'vi-tdesign:chevron-right-s', + 'vi-tdesign:chevron-up', + 'vi-tdesign:chevron-up-circle', + 'vi-tdesign:chevron-up-double', + 'vi-tdesign:chevron-up-double-s', + 'vi-tdesign:chevron-up-rectangle', + 'vi-tdesign:chevron-up-s', + 'vi-tdesign:chicken', + 'vi-tdesign:chili', + 'vi-tdesign:chimney', + 'vi-tdesign:chimney-1', + 'vi-tdesign:chimney-2', + 'vi-tdesign:chinese-cabbage', + 'vi-tdesign:church', + 'vi-tdesign:circle', + 'vi-tdesign:city', + 'vi-tdesign:city-1', + 'vi-tdesign:city-10', + 'vi-tdesign:city-11', + 'vi-tdesign:city-12', + 'vi-tdesign:city-13', + 'vi-tdesign:city-14', + 'vi-tdesign:city-15', + 'vi-tdesign:city-2', + 'vi-tdesign:city-3', + 'vi-tdesign:city-4', + 'vi-tdesign:city-5', + 'vi-tdesign:city-6', + 'vi-tdesign:city-7', + 'vi-tdesign:city-8', + 'vi-tdesign:city-9', + 'vi-tdesign:city-ancient', + 'vi-tdesign:city-ancient-1', + 'vi-tdesign:city-ancient-2', + 'vi-tdesign:clear', + 'vi-tdesign:clear-formatting', + 'vi-tdesign:clear-formatting-1', + 'vi-tdesign:close', + 'vi-tdesign:close-circle', + 'vi-tdesign:close-circle-filled', + 'vi-tdesign:close-octagon', + 'vi-tdesign:close-rectangle', + 'vi-tdesign:cloud', + 'vi-tdesign:cloud-download', + 'vi-tdesign:cloud-upload', + 'vi-tdesign:cloudy-day', + 'vi-tdesign:cloudy-night', + 'vi-tdesign:cloudy-night-rain', + 'vi-tdesign:cloudy-rain', + 'vi-tdesign:cloudy-sunny', + 'vi-tdesign:code', + 'vi-tdesign:code-1', + 'vi-tdesign:code-off', + 'vi-tdesign:cola', + 'vi-tdesign:collage', + 'vi-tdesign:collection', + 'vi-tdesign:color-invert', + 'vi-tdesign:combination', + 'vi-tdesign:command', + 'vi-tdesign:compass', + 'vi-tdesign:compass-1', + 'vi-tdesign:component-breadcrumb', + 'vi-tdesign:component-checkbox', + 'vi-tdesign:component-divider-horizontal', + 'vi-tdesign:component-divider-vertical', + 'vi-tdesign:component-dropdown', + 'vi-tdesign:component-grid', + 'vi-tdesign:component-input', + 'vi-tdesign:component-layout', + 'vi-tdesign:component-radio', + 'vi-tdesign:component-space', + 'vi-tdesign:component-steps', + 'vi-tdesign:component-switch', + 'vi-tdesign:constraint', + 'vi-tdesign:contrast', + 'vi-tdesign:contrast-1', + 'vi-tdesign:control-platform', + 'vi-tdesign:cooperate', + 'vi-tdesign:coordinate-system', + 'vi-tdesign:copy', + 'vi-tdesign:copyright', + 'vi-tdesign:corn', + 'vi-tdesign:coupon', + 'vi-tdesign:course', + 'vi-tdesign:cpu', + 'vi-tdesign:crack', + 'vi-tdesign:creditcard', + 'vi-tdesign:creditcard-add', + 'vi-tdesign:creditcard-off', + 'vi-tdesign:crooked-smile', + 'vi-tdesign:cry-and-laugh', + 'vi-tdesign:cry-loudly', + 'vi-tdesign:css3', + 'vi-tdesign:cucumber', + 'vi-tdesign:currency-exchange', + 'vi-tdesign:cursor', + 'vi-tdesign:curtain', + 'vi-tdesign:curve', + 'vi-tdesign:cut', + 'vi-tdesign:cut-1', + 'vi-tdesign:dam', + 'vi-tdesign:dam-1', + 'vi-tdesign:dam-2', + 'vi-tdesign:dam-3', + 'vi-tdesign:dam-4', + 'vi-tdesign:dam-5', + 'vi-tdesign:dam-6', + 'vi-tdesign:dam-7', + 'vi-tdesign:dart-board', + 'vi-tdesign:dashboard', + 'vi-tdesign:dashboard-1', + 'vi-tdesign:data', + 'vi-tdesign:data-base', + 'vi-tdesign:data-checked', + 'vi-tdesign:data-display', + 'vi-tdesign:data-error', + 'vi-tdesign:data-search', + 'vi-tdesign:delete', + 'vi-tdesign:delete-1', + 'vi-tdesign:delete-time', + 'vi-tdesign:delta', + 'vi-tdesign:depressed', + 'vi-tdesign:desktop', + 'vi-tdesign:desktop-1', + 'vi-tdesign:despise', + 'vi-tdesign:device', + 'vi-tdesign:discount', + 'vi-tdesign:discount-filled', + 'vi-tdesign:dissatisfaction', + 'vi-tdesign:divide', + 'vi-tdesign:dividers', + 'vi-tdesign:dividers-1', + 'vi-tdesign:doge', + 'vi-tdesign:double-storey', + 'vi-tdesign:download', + 'vi-tdesign:download-1', + 'vi-tdesign:downscale', + 'vi-tdesign:drag-drop', + 'vi-tdesign:drag-move', + 'vi-tdesign:drink', + 'vi-tdesign:drumstick', + 'vi-tdesign:dv', + 'vi-tdesign:dvd', + 'vi-tdesign:earphone', + 'vi-tdesign:earth', + 'vi-tdesign:edit', + 'vi-tdesign:edit-1', + 'vi-tdesign:edit-2', + 'vi-tdesign:edit-off', + 'vi-tdesign:education', + 'vi-tdesign:eggplant', + 'vi-tdesign:ellipsis', + 'vi-tdesign:emo-emotional', + 'vi-tdesign:enter', + 'vi-tdesign:equal', + 'vi-tdesign:error', + 'vi-tdesign:error-circle', + 'vi-tdesign:error-circle-filled', + 'vi-tdesign:error-triangle', + 'vi-tdesign:excited', + 'vi-tdesign:excited-1', + 'vi-tdesign:expand-horizontal', + 'vi-tdesign:expand-vertical', + 'vi-tdesign:explore', + 'vi-tdesign:explore-off', + 'vi-tdesign:exposure', + 'vi-tdesign:extension', + 'vi-tdesign:extension-off', + 'vi-tdesign:face-retouching', + 'vi-tdesign:fact-check', + 'vi-tdesign:fahrenheit-scale', + 'vi-tdesign:feel-at-ease', + 'vi-tdesign:ferocious', + 'vi-tdesign:ferris-wheel', + 'vi-tdesign:file', + 'vi-tdesign:file-1', + 'vi-tdesign:file-add', + 'vi-tdesign:file-add-1', + 'vi-tdesign:file-attachment', + 'vi-tdesign:file-blocked', + 'vi-tdesign:file-code', + 'vi-tdesign:file-code-1', + 'vi-tdesign:file-copy', + 'vi-tdesign:file-download', + 'vi-tdesign:file-excel', + 'vi-tdesign:file-export', + 'vi-tdesign:file-icon', + 'vi-tdesign:file-image', + 'vi-tdesign:file-import', + 'vi-tdesign:file-locked', + 'vi-tdesign:file-minus', + 'vi-tdesign:file-music', + 'vi-tdesign:file-onenote', + 'vi-tdesign:file-outlook', + 'vi-tdesign:file-paste', + 'vi-tdesign:file-pdf', + 'vi-tdesign:file-powerpoint', + 'vi-tdesign:file-restore', + 'vi-tdesign:file-safety', + 'vi-tdesign:file-search', + 'vi-tdesign:file-setting', + 'vi-tdesign:file-teams', + 'vi-tdesign:file-unknown', + 'vi-tdesign:file-unlocked', + 'vi-tdesign:file-word', + 'vi-tdesign:file-zip', + 'vi-tdesign:fill-color', + 'vi-tdesign:fill-color-1', + 'vi-tdesign:film', + 'vi-tdesign:film-1', + 'vi-tdesign:filter', + 'vi-tdesign:filter-1', + 'vi-tdesign:filter-2', + 'vi-tdesign:filter-3', + 'vi-tdesign:filter-clear', + 'vi-tdesign:filter-off', + 'vi-tdesign:fingerprint', + 'vi-tdesign:fingerprint-1', + 'vi-tdesign:fingerprint-2', + 'vi-tdesign:fingerprint-3', + 'vi-tdesign:fish', + 'vi-tdesign:flag', + 'vi-tdesign:flag-1', + 'vi-tdesign:flag-2', + 'vi-tdesign:flag-3', + 'vi-tdesign:flag-4', + 'vi-tdesign:flashlight', + 'vi-tdesign:flight-landing', + 'vi-tdesign:flight-takeoff', + 'vi-tdesign:flip-smiling-face', + 'vi-tdesign:flip-to-back', + 'vi-tdesign:flip-to-front', + 'vi-tdesign:focus', + 'vi-tdesign:fog', + 'vi-tdesign:fog-night', + 'vi-tdesign:fog-sunny', + 'vi-tdesign:folder', + 'vi-tdesign:folder-1', + 'vi-tdesign:folder-add', + 'vi-tdesign:folder-add-1', + 'vi-tdesign:folder-blocked', + 'vi-tdesign:folder-details', + 'vi-tdesign:folder-export', + 'vi-tdesign:folder-import', + 'vi-tdesign:folder-locked', + 'vi-tdesign:folder-minus', + 'vi-tdesign:folder-move', + 'vi-tdesign:folder-off', + 'vi-tdesign:folder-open', + 'vi-tdesign:folder-open-1', + 'vi-tdesign:folder-search', + 'vi-tdesign:folder-setting', + 'vi-tdesign:folder-shared', + 'vi-tdesign:folder-unlocked', + 'vi-tdesign:folder-zip', + 'vi-tdesign:forest', + 'vi-tdesign:fork', + 'vi-tdesign:form', + 'vi-tdesign:format-horizontal-align-bottom', + 'vi-tdesign:format-horizontal-align-center', + 'vi-tdesign:format-horizontal-align-top', + 'vi-tdesign:format-vertical-align-center', + 'vi-tdesign:format-vertical-align-left', + 'vi-tdesign:format-vertical-align-right', + 'vi-tdesign:forward', + 'vi-tdesign:frame', + 'vi-tdesign:frame-1', + 'vi-tdesign:fries', + 'vi-tdesign:fullscreen', + 'vi-tdesign:fullscreen-1', + 'vi-tdesign:fullscreen-2', + 'vi-tdesign:fullscreen-exit', + 'vi-tdesign:fullscreen-exit-1', + 'vi-tdesign:function-curve', + 'vi-tdesign:functions', + 'vi-tdesign:functions-1', + 'vi-tdesign:gamepad', + 'vi-tdesign:gamepad-1', + 'vi-tdesign:gamma', + 'vi-tdesign:garlic', + 'vi-tdesign:gender-female', + 'vi-tdesign:gender-male', + 'vi-tdesign:gesture-applause', + 'vi-tdesign:gesture-click', + 'vi-tdesign:gesture-down', + 'vi-tdesign:gesture-expansion', + 'vi-tdesign:gesture-left', + 'vi-tdesign:gesture-left-slip', + 'vi-tdesign:gesture-pray', + 'vi-tdesign:gesture-pray-1', + 'vi-tdesign:gesture-press', + 'vi-tdesign:gesture-ranslation', + 'vi-tdesign:gesture-ranslation-1', + 'vi-tdesign:gesture-right', + 'vi-tdesign:gesture-right-slip', + 'vi-tdesign:gesture-slide-up', + 'vi-tdesign:gesture-up', + 'vi-tdesign:gesture-up-1', + 'vi-tdesign:gesture-up-2', + 'vi-tdesign:gesture-up-and-down', + 'vi-tdesign:gesture-wipe-down', + 'vi-tdesign:gift', + 'vi-tdesign:giggle', + 'vi-tdesign:git-branch', + 'vi-tdesign:git-commit', + 'vi-tdesign:git-merge', + 'vi-tdesign:git-pull-request', + 'vi-tdesign:git-repository', + 'vi-tdesign:git-repository-commits', + 'vi-tdesign:git-repository-private', + 'vi-tdesign:gps', + 'vi-tdesign:grape', + 'vi-tdesign:greater-than', + 'vi-tdesign:greater-than-or-equal', + 'vi-tdesign:green-onion', + 'vi-tdesign:grid-add', + 'vi-tdesign:grid-view', + 'vi-tdesign:guitar', + 'vi-tdesign:hamburger', + 'vi-tdesign:happy', + 'vi-tdesign:hard-disk-storage', + 'vi-tdesign:hard-drive', + 'vi-tdesign:hashtag', + 'vi-tdesign:hd', + 'vi-tdesign:heart', + 'vi-tdesign:heart-filled', + 'vi-tdesign:help', + 'vi-tdesign:help-circle', + 'vi-tdesign:help-circle-filled', + 'vi-tdesign:help-rectangle', + 'vi-tdesign:highlight', + 'vi-tdesign:highlight-1', + 'vi-tdesign:history', + 'vi-tdesign:history-setting', + 'vi-tdesign:home', + 'vi-tdesign:hospital', + 'vi-tdesign:hospital-1', + 'vi-tdesign:hotspot-wave', + 'vi-tdesign:hourglass', + 'vi-tdesign:houses', + 'vi-tdesign:houses-1', + 'vi-tdesign:houses-2', + 'vi-tdesign:html5', + 'vi-tdesign:https', + 'vi-tdesign:ice-cream', + 'vi-tdesign:icon', + 'vi-tdesign:image', + 'vi-tdesign:image-1', + 'vi-tdesign:image-add', + 'vi-tdesign:image-edit', + 'vi-tdesign:image-error', + 'vi-tdesign:image-off', + 'vi-tdesign:image-search', + 'vi-tdesign:indent-left', + 'vi-tdesign:indent-right', + 'vi-tdesign:indicator', + 'vi-tdesign:info-circle', + 'vi-tdesign:info-circle-filled', + 'vi-tdesign:ink', + 'vi-tdesign:install', + 'vi-tdesign:install-desktop', + 'vi-tdesign:install-mobile', + 'vi-tdesign:institution', + 'vi-tdesign:institution-checked', + 'vi-tdesign:internet', + 'vi-tdesign:ipod', + 'vi-tdesign:joyful', + 'vi-tdesign:jump', + 'vi-tdesign:jump-off', + 'vi-tdesign:keyboard', + 'vi-tdesign:laptop', + 'vi-tdesign:layers', + 'vi-tdesign:layout', + 'vi-tdesign:leaderboard', + 'vi-tdesign:lemon', + 'vi-tdesign:lemon-slice', + 'vi-tdesign:less-than', + 'vi-tdesign:less-than-or-equal', + 'vi-tdesign:letters-a', + 'vi-tdesign:letters-b', + 'vi-tdesign:letters-c', + 'vi-tdesign:letters-d', + 'vi-tdesign:letters-e', + 'vi-tdesign:letters-f', + 'vi-tdesign:letters-g', + 'vi-tdesign:letters-h', + 'vi-tdesign:letters-i', + 'vi-tdesign:letters-j', + 'vi-tdesign:letters-k', + 'vi-tdesign:letters-l', + 'vi-tdesign:letters-m', + 'vi-tdesign:letters-n', + 'vi-tdesign:letters-o', + 'vi-tdesign:letters-p', + 'vi-tdesign:letters-q', + 'vi-tdesign:letters-r', + 'vi-tdesign:letters-s', + 'vi-tdesign:letters-t', + 'vi-tdesign:letters-u', + 'vi-tdesign:letters-v', + 'vi-tdesign:letters-w', + 'vi-tdesign:letters-x', + 'vi-tdesign:letters-y', + 'vi-tdesign:letters-z', + 'vi-tdesign:lightbulb', + 'vi-tdesign:lightbulb-circle', + 'vi-tdesign:lighthouse', + 'vi-tdesign:lighthouse-1', + 'vi-tdesign:lighthouse-2', + 'vi-tdesign:lighting-circle', + 'vi-tdesign:line-height', + 'vi-tdesign:link', + 'vi-tdesign:link-1', + 'vi-tdesign:link-unlink', + 'vi-tdesign:liquor', + 'vi-tdesign:list', + 'vi-tdesign:load', + 'vi-tdesign:loading', + 'vi-tdesign:location', + 'vi-tdesign:location-1', + 'vi-tdesign:location-enlargement', + 'vi-tdesign:location-error', + 'vi-tdesign:location-parking-place', + 'vi-tdesign:location-reduction', + 'vi-tdesign:location-setting', + 'vi-tdesign:lock-off', + 'vi-tdesign:lock-on', + 'vi-tdesign:lock-time', + 'vi-tdesign:login', + 'vi-tdesign:logo-adobe-illustrate', + 'vi-tdesign:logo-adobe-photoshop', + 'vi-tdesign:logo-adobe-photoshop-1', + 'vi-tdesign:logo-android', + 'vi-tdesign:logo-apple', + 'vi-tdesign:logo-apple-filled', + 'vi-tdesign:logo-behance', + 'vi-tdesign:logo-chrome', + 'vi-tdesign:logo-chrome-filled', + 'vi-tdesign:logo-cinema4d', + 'vi-tdesign:logo-codepen', + 'vi-tdesign:logo-codesandbox', + 'vi-tdesign:logo-dribbble', + 'vi-tdesign:logo-facebook', + 'vi-tdesign:logo-figma', + 'vi-tdesign:logo-framer', + 'vi-tdesign:logo-github', + 'vi-tdesign:logo-github-filled', + 'vi-tdesign:logo-gitlab', + 'vi-tdesign:logo-ie', + 'vi-tdesign:logo-ie-filled', + 'vi-tdesign:logo-instagram', + 'vi-tdesign:logo-qq', + 'vi-tdesign:logo-twitter', + 'vi-tdesign:logo-wechat', + 'vi-tdesign:logo-wechat-stroke', + 'vi-tdesign:logo-wecom', + 'vi-tdesign:logo-windows', + 'vi-tdesign:logo-windows-filled', + 'vi-tdesign:logo-youtube', + 'vi-tdesign:logout', + 'vi-tdesign:look-around', + 'vi-tdesign:loudspeaker', + 'vi-tdesign:mail', + 'vi-tdesign:map', + 'vi-tdesign:map-3d', + 'vi-tdesign:map-add', + 'vi-tdesign:map-aiming', + 'vi-tdesign:map-blocked', + 'vi-tdesign:map-bubble', + 'vi-tdesign:map-cancel', + 'vi-tdesign:map-chat', + 'vi-tdesign:map-checked', + 'vi-tdesign:map-collection', + 'vi-tdesign:map-connection', + 'vi-tdesign:map-distance', + 'vi-tdesign:map-double', + 'vi-tdesign:map-edit', + 'vi-tdesign:map-grid', + 'vi-tdesign:map-information', + 'vi-tdesign:map-information-1', + 'vi-tdesign:map-information-2', + 'vi-tdesign:map-location', + 'vi-tdesign:map-locked', + 'vi-tdesign:map-marked', + 'vi-tdesign:map-navigation', + 'vi-tdesign:map-outline', + 'vi-tdesign:map-route-planning', + 'vi-tdesign:map-ruler', + 'vi-tdesign:map-safety', + 'vi-tdesign:map-search', + 'vi-tdesign:map-search-1', + 'vi-tdesign:map-setting', + 'vi-tdesign:map-unlocked', + 'vi-tdesign:mark-as-unread', + 'vi-tdesign:markup', + 'vi-tdesign:mathematics', + 'vi-tdesign:measurement', + 'vi-tdesign:measurement-1', + 'vi-tdesign:measurement-2', + 'vi-tdesign:meat-pepper', + 'vi-tdesign:media-library', + 'vi-tdesign:member', + 'vi-tdesign:menu', + 'vi-tdesign:menu-application', + 'vi-tdesign:menu-fold', + 'vi-tdesign:menu-unfold', + 'vi-tdesign:merge-cells', + 'vi-tdesign:microphone', + 'vi-tdesign:microphone-1', + 'vi-tdesign:microphone-2', + 'vi-tdesign:milk', + 'vi-tdesign:minus', + 'vi-tdesign:minus-circle', + 'vi-tdesign:minus-circle-filled', + 'vi-tdesign:minus-rectangle', + 'vi-tdesign:minus-rectangle-filled', + 'vi-tdesign:mirror', + 'vi-tdesign:mobile', + 'vi-tdesign:mobile-blocked', + 'vi-tdesign:mobile-list', + 'vi-tdesign:mobile-navigation', + 'vi-tdesign:mobile-shortcut', + 'vi-tdesign:mobile-vibrate', + 'vi-tdesign:mode-dark', + 'vi-tdesign:mode-light', + 'vi-tdesign:module', + 'vi-tdesign:money', + 'vi-tdesign:monument', + 'vi-tdesign:moon', + 'vi-tdesign:moon-fall', + 'vi-tdesign:moon-rising', + 'vi-tdesign:more', + 'vi-tdesign:mosque', + 'vi-tdesign:mosque-1', + 'vi-tdesign:mouse', + 'vi-tdesign:move', + 'vi-tdesign:move-1', + 'vi-tdesign:movie-clapper', + 'vi-tdesign:multiply', + 'vi-tdesign:museum', + 'vi-tdesign:museum-1', + 'vi-tdesign:museum-2', + 'vi-tdesign:mushroom', + 'vi-tdesign:mushroom-1', + 'vi-tdesign:music', + 'vi-tdesign:music-1', + 'vi-tdesign:music-2', + 'vi-tdesign:music-rectangle-add', + 'vi-tdesign:navigation-arrow', + 'vi-tdesign:next', + 'vi-tdesign:no-expression', + 'vi-tdesign:noodle', + 'vi-tdesign:notification', + 'vi-tdesign:notification-add', + 'vi-tdesign:notification-circle', + 'vi-tdesign:notification-error', + 'vi-tdesign:notification-filled', + 'vi-tdesign:numbers-0', + 'vi-tdesign:numbers-0-1', + 'vi-tdesign:numbers-1', + 'vi-tdesign:numbers-1-1', + 'vi-tdesign:numbers-2', + 'vi-tdesign:numbers-2-1', + 'vi-tdesign:numbers-3', + 'vi-tdesign:numbers-3-1', + 'vi-tdesign:numbers-4', + 'vi-tdesign:numbers-4-1', + 'vi-tdesign:numbers-5', + 'vi-tdesign:numbers-5-1', + 'vi-tdesign:numbers-6', + 'vi-tdesign:numbers-6-1', + 'vi-tdesign:numbers-7', + 'vi-tdesign:numbers-7-1', + 'vi-tdesign:numbers-8', + 'vi-tdesign:numbers-8-1', + 'vi-tdesign:numbers-9', + 'vi-tdesign:numbers-9-1', + 'vi-tdesign:nut', + 'vi-tdesign:object-storage', + 'vi-tdesign:open-mouth', + 'vi-tdesign:opera', + 'vi-tdesign:order-adjustment-column', + 'vi-tdesign:order-ascending', + 'vi-tdesign:order-descending', + 'vi-tdesign:outbox', + 'vi-tdesign:page-first', + 'vi-tdesign:page-head', + 'vi-tdesign:page-last', + 'vi-tdesign:palace', + 'vi-tdesign:palace-1', + 'vi-tdesign:palace-2', + 'vi-tdesign:palace-3', + 'vi-tdesign:palace-4', + 'vi-tdesign:palette', + 'vi-tdesign:palette-1', + 'vi-tdesign:panorama-horizontal', + 'vi-tdesign:panorama-vertical', + 'vi-tdesign:pantone', + 'vi-tdesign:parabola', + 'vi-tdesign:parentheses', + 'vi-tdesign:paste', + 'vi-tdesign:patio', + 'vi-tdesign:pause', + 'vi-tdesign:pause-circle', + 'vi-tdesign:pause-circle-filled', + 'vi-tdesign:pause-circle-stroke', + 'vi-tdesign:pea', + 'vi-tdesign:peach', + 'vi-tdesign:pear', + 'vi-tdesign:pearl-of-the-orient', + 'vi-tdesign:pen', + 'vi-tdesign:pen-ball', + 'vi-tdesign:pen-brush', + 'vi-tdesign:pen-mark', + 'vi-tdesign:pen-quill', + 'vi-tdesign:pending', + 'vi-tdesign:percent', + 'vi-tdesign:personal-information', + 'vi-tdesign:phone-locked', + 'vi-tdesign:phone-search', + 'vi-tdesign:pi', + 'vi-tdesign:piano', + 'vi-tdesign:pin', + 'vi-tdesign:pin-filled', + 'vi-tdesign:play', + 'vi-tdesign:play-circle', + 'vi-tdesign:play-circle-filled', + 'vi-tdesign:play-circle-stroke', + 'vi-tdesign:play-circle-stroke-add', + 'vi-tdesign:play-demo', + 'vi-tdesign:play-rectangle', + 'vi-tdesign:plus', + 'vi-tdesign:popsicle', + 'vi-tdesign:portrait', + 'vi-tdesign:pout', + 'vi-tdesign:poweroff', + 'vi-tdesign:precise-monitor', + 'vi-tdesign:previous', + 'vi-tdesign:print', + 'vi-tdesign:pumpkin', + 'vi-tdesign:pyramid', + 'vi-tdesign:pyramid-maya', + 'vi-tdesign:qrcode', + 'vi-tdesign:quadratic', + 'vi-tdesign:questionnaire', + 'vi-tdesign:queue', + 'vi-tdesign:radar', + 'vi-tdesign:radio-1', + 'vi-tdesign:radio-2', + 'vi-tdesign:radish', + 'vi-tdesign:rain-heavy', + 'vi-tdesign:rain-light', + 'vi-tdesign:rain-medium', + 'vi-tdesign:rainbow', + 'vi-tdesign:rectangle', + 'vi-tdesign:refresh', + 'vi-tdesign:relation', + 'vi-tdesign:relativity', + 'vi-tdesign:remote-wave', + 'vi-tdesign:remove', + 'vi-tdesign:replay', + 'vi-tdesign:rice', + 'vi-tdesign:rice-ball', + 'vi-tdesign:roast', + 'vi-tdesign:rocket', + 'vi-tdesign:rollback', + 'vi-tdesign:rollfront', + 'vi-tdesign:root-list', + 'vi-tdesign:rotate', + 'vi-tdesign:rotate-locked', + 'vi-tdesign:rotation', + 'vi-tdesign:round', + 'vi-tdesign:router-wave', + 'vi-tdesign:rss', + 'vi-tdesign:ruler', + 'vi-tdesign:sailing-hotel', + 'vi-tdesign:sandwich', + 'vi-tdesign:saturation', + 'vi-tdesign:sausage', + 'vi-tdesign:save', + 'vi-tdesign:saving-pot', + 'vi-tdesign:scan', + 'vi-tdesign:screen-4k', + 'vi-tdesign:screencast', + 'vi-tdesign:screenshot', + 'vi-tdesign:scroll-bar', + 'vi-tdesign:sd-card', + 'vi-tdesign:sd-card-1', + 'vi-tdesign:search', + 'vi-tdesign:search-error', + 'vi-tdesign:secured', + 'vi-tdesign:send', + 'vi-tdesign:send-cancel', + 'vi-tdesign:sensors', + 'vi-tdesign:sensors-1', + 'vi-tdesign:sensors-2', + 'vi-tdesign:sensors-off', + 'vi-tdesign:serenity', + 'vi-tdesign:server', + 'vi-tdesign:service', + 'vi-tdesign:setting', + 'vi-tdesign:setting-1', + 'vi-tdesign:share', + 'vi-tdesign:share-1', + 'vi-tdesign:sharpness', + 'vi-tdesign:shield-error', + 'vi-tdesign:shimen', + 'vi-tdesign:shop', + 'vi-tdesign:shop-1', + 'vi-tdesign:shop-2', + 'vi-tdesign:shop-3', + 'vi-tdesign:shop-4', + 'vi-tdesign:shop-5', + 'vi-tdesign:shrimp', + 'vi-tdesign:shrink-horizontal', + 'vi-tdesign:shrink-vertical', + 'vi-tdesign:shutter', + 'vi-tdesign:shutup', + 'vi-tdesign:sim-card', + 'vi-tdesign:sim-card-1', + 'vi-tdesign:sim-card-2', + 'vi-tdesign:sinister-smile', + 'vi-tdesign:sip', + 'vi-tdesign:slash', + 'vi-tdesign:sleep', + 'vi-tdesign:slice', + 'vi-tdesign:slideshow', + 'vi-tdesign:smile', + 'vi-tdesign:sneer', + 'vi-tdesign:snowflake', + 'vi-tdesign:sonic', + 'vi-tdesign:sound', + 'vi-tdesign:sound-down', + 'vi-tdesign:sound-high', + 'vi-tdesign:sound-low', + 'vi-tdesign:sound-mute', + 'vi-tdesign:sound-mute-1', + 'vi-tdesign:sound-up', + 'vi-tdesign:space', + 'vi-tdesign:speechless-1', + 'vi-tdesign:star', + 'vi-tdesign:star-filled', + 'vi-tdesign:statue-of-jesus', + 'vi-tdesign:sticky-note', + 'vi-tdesign:stop', + 'vi-tdesign:stop-circle', + 'vi-tdesign:stop-circle-filled', + 'vi-tdesign:stop-circle-stroke', + 'vi-tdesign:store', + 'vi-tdesign:street-road', + 'vi-tdesign:street-road-1', + 'vi-tdesign:subtitle', + 'vi-tdesign:subway-line', + 'vi-tdesign:sum', + 'vi-tdesign:sun-fall', + 'vi-tdesign:sun-rising', + 'vi-tdesign:sunny', + 'vi-tdesign:support', + 'vi-tdesign:surprised', + 'vi-tdesign:surprised-1', + 'vi-tdesign:swap', + 'vi-tdesign:swap-left', + 'vi-tdesign:swap-right', + 'vi-tdesign:swear-1', + 'vi-tdesign:swear-2', + 'vi-tdesign:system-2', + 'vi-tdesign:system-3', + 'vi-tdesign:system-application', + 'vi-tdesign:system-blocked', + 'vi-tdesign:system-code', + 'vi-tdesign:system-components', + 'vi-tdesign:system-coordinate', + 'vi-tdesign:system-device', + 'vi-tdesign:system-interface', + 'vi-tdesign:system-location', + 'vi-tdesign:system-locked', + 'vi-tdesign:system-log', + 'vi-tdesign:system-marked', + 'vi-tdesign:system-messages', + 'vi-tdesign:system-regulation', + 'vi-tdesign:system-search', + 'vi-tdesign:system-setting', + 'vi-tdesign:system-storage', + 'vi-tdesign:system-sum', + 'vi-tdesign:system-unlocked', + 'vi-tdesign:tab', + 'vi-tdesign:table', + 'vi-tdesign:table-1', + 'vi-tdesign:table-2', + 'vi-tdesign:table-add', + 'vi-tdesign:table-split', + 'vi-tdesign:tag', + 'vi-tdesign:tangerinr', + 'vi-tdesign:tape', + 'vi-tdesign:task', + 'vi-tdesign:task-1', + 'vi-tdesign:task-add', + 'vi-tdesign:task-add-1', + 'vi-tdesign:task-checked', + 'vi-tdesign:task-error', + 'vi-tdesign:task-location', + 'vi-tdesign:task-marked', + 'vi-tdesign:task-setting', + 'vi-tdesign:task-visible', + 'vi-tdesign:tea', + 'vi-tdesign:teahouse', + 'vi-tdesign:template', + 'vi-tdesign:temple', + 'vi-tdesign:terminal', + 'vi-tdesign:terminal-rectangle', + 'vi-tdesign:terminal-rectangle-1', + 'vi-tdesign:terminal-window', + 'vi-tdesign:textbox', + 'vi-tdesign:textformat-bold', + 'vi-tdesign:textformat-color', + 'vi-tdesign:textformat-italic', + 'vi-tdesign:textformat-strikethrough', + 'vi-tdesign:textformat-underline', + 'vi-tdesign:textformat-wrap', + 'vi-tdesign:theaters', + 'vi-tdesign:thumb-down', + 'vi-tdesign:thumb-down-1', + 'vi-tdesign:thumb-down-2', + 'vi-tdesign:thumb-up', + 'vi-tdesign:thumb-up-1', + 'vi-tdesign:thumb-up-2', + 'vi-tdesign:thunder', + 'vi-tdesign:thunderstorm', + 'vi-tdesign:thunderstorm-night', + 'vi-tdesign:thunderstorm-sunny', + 'vi-tdesign:ticket', + 'vi-tdesign:time', + 'vi-tdesign:time-filled', + 'vi-tdesign:tips', + 'vi-tdesign:tips-double', + 'vi-tdesign:tomato', + 'vi-tdesign:tools', + 'vi-tdesign:tools-circle', + 'vi-tdesign:tornado', + 'vi-tdesign:tower', + 'vi-tdesign:tower-1', + 'vi-tdesign:tower-2', + 'vi-tdesign:tower-3', + 'vi-tdesign:tower-clock', + 'vi-tdesign:town', + 'vi-tdesign:traffic', + 'vi-tdesign:traffic-events', + 'vi-tdesign:transform', + 'vi-tdesign:transform-1', + 'vi-tdesign:transform-2', + 'vi-tdesign:transform-3', + 'vi-tdesign:translate', + 'vi-tdesign:translate-1', + 'vi-tdesign:tree-list', + 'vi-tdesign:tree-round-dot', + 'vi-tdesign:tree-round-dot-vertical', + 'vi-tdesign:tree-square-dot', + 'vi-tdesign:tree-square-dot-vertical', + 'vi-tdesign:trending-down', + 'vi-tdesign:trending-up', + 'vi-tdesign:tv', + 'vi-tdesign:tv-1', + 'vi-tdesign:tv-2', + 'vi-tdesign:typography', + 'vi-tdesign:uncomfortable', + 'vi-tdesign:uncomfortable-1', + 'vi-tdesign:uncomfortable-2', + 'vi-tdesign:undertake', + 'vi-tdesign:undertake-delivery', + 'vi-tdesign:undertake-environment-protection', + 'vi-tdesign:undertake-hold-up', + 'vi-tdesign:undertake-transaction', + 'vi-tdesign:unfold-less', + 'vi-tdesign:unfold-more', + 'vi-tdesign:unhappy', + 'vi-tdesign:unhappy-1', + 'vi-tdesign:uninstall', + 'vi-tdesign:upload', + 'vi-tdesign:upload-1', + 'vi-tdesign:upscale', + 'vi-tdesign:usb', + 'vi-tdesign:user', + 'vi-tdesign:user-1', + 'vi-tdesign:user-add', + 'vi-tdesign:user-arrow-down', + 'vi-tdesign:user-arrow-left', + 'vi-tdesign:user-arrow-right', + 'vi-tdesign:user-arrow-up', + 'vi-tdesign:user-avatar', + 'vi-tdesign:user-blocked', + 'vi-tdesign:user-business', + 'vi-tdesign:user-checked', + 'vi-tdesign:user-checked-1', + 'vi-tdesign:user-circle', + 'vi-tdesign:user-clear', + 'vi-tdesign:user-error-1', + 'vi-tdesign:user-invisible', + 'vi-tdesign:user-list', + 'vi-tdesign:user-locked', + 'vi-tdesign:user-marked', + 'vi-tdesign:user-password', + 'vi-tdesign:user-safety', + 'vi-tdesign:user-search', + 'vi-tdesign:user-setting', + 'vi-tdesign:user-talk', + 'vi-tdesign:user-talk-1', + 'vi-tdesign:user-talk-off-1', + 'vi-tdesign:user-time', + 'vi-tdesign:user-transmit', + 'vi-tdesign:user-unknown', + 'vi-tdesign:user-unlocked', + 'vi-tdesign:user-vip', + 'vi-tdesign:user-visible', + 'vi-tdesign:usergroup', + 'vi-tdesign:usergroup-add', + 'vi-tdesign:usergroup-clear', + 'vi-tdesign:vehicle', + 'vi-tdesign:verified', + 'vi-tdesign:verify', + 'vi-tdesign:video', + 'vi-tdesign:video-camera', + 'vi-tdesign:video-camera-1', + 'vi-tdesign:video-camera-2', + 'vi-tdesign:video-camera-dollar', + 'vi-tdesign:video-camera-minus', + 'vi-tdesign:video-camera-music', + 'vi-tdesign:video-camera-off', + 'vi-tdesign:video-library', + 'vi-tdesign:view-agenda', + 'vi-tdesign:view-column', + 'vi-tdesign:view-in-ar', + 'vi-tdesign:view-list', + 'vi-tdesign:view-module', + 'vi-tdesign:visual-recognition', + 'vi-tdesign:wallet', + 'vi-tdesign:watch', + 'vi-tdesign:watermelon', + 'vi-tdesign:wave-left', + 'vi-tdesign:wave-right', + 'vi-tdesign:wealth', + 'vi-tdesign:wealth-1', + 'vi-tdesign:widget', + 'vi-tdesign:wifi', + 'vi-tdesign:wifi-1', + 'vi-tdesign:wifi-off', + 'vi-tdesign:wifi-off-1', + 'vi-tdesign:window', + 'vi-tdesign:window-1', + 'vi-tdesign:windy', + 'vi-tdesign:windy-rain', + 'vi-tdesign:wink', + 'vi-tdesign:work', + 'vi-tdesign:work-history', + 'vi-tdesign:work-off', + 'vi-tdesign:wry-smile', + 'vi-tdesign:zoom-in', + 'vi-tdesign:zoom-out' + ] +} diff --git a/promo-ui2/src/components/ImageCropping/index.ts b/promo-ui2/src/components/ImageCropping/index.ts new file mode 100644 index 0000000..d6c0f25 --- /dev/null +++ b/promo-ui2/src/components/ImageCropping/index.ts @@ -0,0 +1,3 @@ +import ImageCropping from './src/ImageCropping.vue' + +export { ImageCropping } diff --git a/promo-ui2/src/components/ImageCropping/src/ImageCropping.vue b/promo-ui2/src/components/ImageCropping/src/ImageCropping.vue new file mode 100644 index 0000000..2f8c685 --- /dev/null +++ b/promo-ui2/src/components/ImageCropping/src/ImageCropping.vue @@ -0,0 +1,259 @@ + + + diff --git a/promo-ui2/src/components/ImageViewer/index.ts b/promo-ui2/src/components/ImageViewer/index.ts new file mode 100644 index 0000000..35764d6 --- /dev/null +++ b/promo-ui2/src/components/ImageViewer/index.ts @@ -0,0 +1,33 @@ +import ImageViewer from './src/ImageViewer.vue' +import { isClient } from '@/utils/is' +import { createVNode, render, VNode } from 'vue' +import { ImageViewerProps } from './src/types' + +let instance: Nullable = null + +export function createImageViewer(options: ImageViewerProps) { + if (!isClient) return + const { + urlList, + initialIndex = 0, + infinite = true, + hideOnClickModal = false, + teleported = false, + zIndex = 2000, + show = true + } = options + + const propsData: Partial = {} + const container = document.createElement('div') + propsData.urlList = urlList + propsData.initialIndex = initialIndex + propsData.infinite = infinite + propsData.hideOnClickModal = hideOnClickModal + propsData.teleported = teleported + propsData.zIndex = zIndex + propsData.show = show + + document.body.appendChild(container) + instance = createVNode(ImageViewer, propsData) + render(instance, container) +} diff --git a/promo-ui2/src/components/ImageViewer/src/ImageViewer.vue b/promo-ui2/src/components/ImageViewer/src/ImageViewer.vue new file mode 100644 index 0000000..b9afe17 --- /dev/null +++ b/promo-ui2/src/components/ImageViewer/src/ImageViewer.vue @@ -0,0 +1,34 @@ + + + diff --git a/promo-ui2/src/components/ImageViewer/src/types/index.ts b/promo-ui2/src/components/ImageViewer/src/types/index.ts new file mode 100644 index 0000000..2fff4c0 --- /dev/null +++ b/promo-ui2/src/components/ImageViewer/src/types/index.ts @@ -0,0 +1,9 @@ +export interface ImageViewerProps { + urlList?: string[] + zIndex?: number + initialIndex?: number + infinite?: boolean + hideOnClickModal?: boolean + teleported?: boolean + show?: boolean +} diff --git a/promo-ui2/src/components/Infotip/index.ts b/promo-ui2/src/components/Infotip/index.ts new file mode 100644 index 0000000..eb99944 --- /dev/null +++ b/promo-ui2/src/components/Infotip/index.ts @@ -0,0 +1,5 @@ +import Infotip from './src/Infotip.vue' + +export type { InfoTipSchema } from './src/types' + +export { Infotip } diff --git a/promo-ui2/src/components/Infotip/src/Infotip.vue b/promo-ui2/src/components/Infotip/src/Infotip.vue new file mode 100644 index 0000000..ff9136c --- /dev/null +++ b/promo-ui2/src/components/Infotip/src/Infotip.vue @@ -0,0 +1,53 @@ + + + diff --git a/promo-ui2/src/components/Infotip/src/types/index.ts b/promo-ui2/src/components/Infotip/src/types/index.ts new file mode 100644 index 0000000..50398db --- /dev/null +++ b/promo-ui2/src/components/Infotip/src/types/index.ts @@ -0,0 +1,4 @@ +export interface InfoTipSchema { + label: string + keys?: string[] +} diff --git a/promo-ui2/src/components/InputPassword/index.ts b/promo-ui2/src/components/InputPassword/index.ts new file mode 100644 index 0000000..1dcc38e --- /dev/null +++ b/promo-ui2/src/components/InputPassword/index.ts @@ -0,0 +1,3 @@ +import InputPassword from './src/InputPassword.vue' + +export { InputPassword } diff --git a/promo-ui2/src/components/InputPassword/src/InputPassword.vue b/promo-ui2/src/components/InputPassword/src/InputPassword.vue new file mode 100644 index 0000000..d9aa928 --- /dev/null +++ b/promo-ui2/src/components/InputPassword/src/InputPassword.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/promo-ui2/src/components/JsonEditor/index.ts b/promo-ui2/src/components/JsonEditor/index.ts new file mode 100644 index 0000000..53a2d06 --- /dev/null +++ b/promo-ui2/src/components/JsonEditor/index.ts @@ -0,0 +1,4 @@ +import JsonEditor from './src/JsonEditor.vue' +export type { JsonEditorProps } from './src/types' + +export { JsonEditor } diff --git a/promo-ui2/src/components/JsonEditor/src/JsonEditor.vue b/promo-ui2/src/components/JsonEditor/src/JsonEditor.vue new file mode 100644 index 0000000..a1c0a43 --- /dev/null +++ b/promo-ui2/src/components/JsonEditor/src/JsonEditor.vue @@ -0,0 +1,98 @@ + + + diff --git a/promo-ui2/src/components/JsonEditor/src/types/index.ts b/promo-ui2/src/components/JsonEditor/src/types/index.ts new file mode 100644 index 0000000..d77097f --- /dev/null +++ b/promo-ui2/src/components/JsonEditor/src/types/index.ts @@ -0,0 +1,23 @@ +export interface JsonEditorProps { + value: any + deep?: number + showLength?: boolean + showLineNumbers?: boolean + showLineNumber?: boolean + showIcon?: boolean + showDoubleQuotes?: boolean + virtual?: boolean + height?: number + itemHeight?: number + rootPath?: string + nodeSelectable?: (...args: any[]) => boolean + selectableType?: 'multiple' | 'single' + showSelectController?: boolean + selectOnClickNode?: boolean + highlightSelectedNode?: boolean + collapsedOnClickBrackets?: boolean + renderNodeKey?: (...args: any[]) => any + renderNodeValue?: (...args: any[]) => any + editable?: boolean + editableTrigger?: 'click' | 'dblclick' +} diff --git a/promo-ui2/src/components/LocaleDropdown/index.ts b/promo-ui2/src/components/LocaleDropdown/index.ts new file mode 100644 index 0000000..4d9b98a --- /dev/null +++ b/promo-ui2/src/components/LocaleDropdown/index.ts @@ -0,0 +1,5 @@ +import LocaleDropdown from './src/LocaleDropdown.vue' + +export type { Language, LocaleDropdownType } from './src/types' + +export { LocaleDropdown } diff --git a/promo-ui2/src/components/LocaleDropdown/src/LocaleDropdown.vue b/promo-ui2/src/components/LocaleDropdown/src/LocaleDropdown.vue new file mode 100644 index 0000000..e1a0342 --- /dev/null +++ b/promo-ui2/src/components/LocaleDropdown/src/LocaleDropdown.vue @@ -0,0 +1,52 @@ + + + diff --git a/promo-ui2/src/components/LocaleDropdown/src/types/index.ts b/promo-ui2/src/components/LocaleDropdown/src/types/index.ts new file mode 100644 index 0000000..c749dce --- /dev/null +++ b/promo-ui2/src/components/LocaleDropdown/src/types/index.ts @@ -0,0 +1,10 @@ +export interface Language { + el: Recordable + name: string +} + +export interface LocaleDropdownType { + lang: LocaleType + name?: string + elLocale?: Language +} diff --git a/promo-ui2/src/components/Logo/index.ts b/promo-ui2/src/components/Logo/index.ts new file mode 100644 index 0000000..1c4224c --- /dev/null +++ b/promo-ui2/src/components/Logo/index.ts @@ -0,0 +1,3 @@ +import Logo from './src/Logo.vue' + +export { Logo } diff --git a/promo-ui2/src/components/Logo/src/Logo.vue b/promo-ui2/src/components/Logo/src/Logo.vue new file mode 100644 index 0000000..ddd215b --- /dev/null +++ b/promo-ui2/src/components/Logo/src/Logo.vue @@ -0,0 +1,76 @@ + + + diff --git a/promo-ui2/src/components/Menu/index.ts b/promo-ui2/src/components/Menu/index.ts new file mode 100644 index 0000000..a6ec696 --- /dev/null +++ b/promo-ui2/src/components/Menu/index.ts @@ -0,0 +1,3 @@ +import Menu from './src/Menu.vue' + +export { Menu } diff --git a/promo-ui2/src/components/Menu/src/Menu.vue b/promo-ui2/src/components/Menu/src/Menu.vue new file mode 100644 index 0000000..083d03c --- /dev/null +++ b/promo-ui2/src/components/Menu/src/Menu.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/promo-ui2/src/components/Menu/src/components/useRenderMenuItem.tsx b/promo-ui2/src/components/Menu/src/components/useRenderMenuItem.tsx new file mode 100644 index 0000000..96914cc --- /dev/null +++ b/promo-ui2/src/components/Menu/src/components/useRenderMenuItem.tsx @@ -0,0 +1,59 @@ +import { ElSubMenu, ElMenuItem } from 'element-plus' +import { unref } from 'vue' +import { hasOneShowingChild } from '../helper' +import { isUrl } from '@/utils/is' +import { useRenderMenuTitle } from './useRenderMenuTitle' +import { pathResolve } from '@/utils/routerHelper' +import { useDesign } from '@/hooks/web/useDesign' + +const { getPrefixCls } = useDesign() +const prefixCls = getPrefixCls('submenu') + +const { renderMenuTitle } = useRenderMenuTitle() + +export const useRenderMenuItem = (menuMode) => + // allRouters: AppRouteRecordRaw[] = [], + { + const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { + return routers + .filter((v) => !v.meta?.hidden) + .map((v) => { + const meta = v.meta ?? {} + const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) + const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath(allRouters, v.path).join('/') + + if ( + oneShowingChild && + (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && + !meta?.alwaysShow + ) { + return ( + + {{ + default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta) + }} + + ) + } else { + return ( + + {{ + title: () => renderMenuTitle(meta), + default: () => renderMenuItem(v.children!, fullPath) + }} + + ) + } + }) + } + + return { + renderMenuItem + } + } diff --git a/promo-ui2/src/components/Menu/src/components/useRenderMenuTitle.tsx b/promo-ui2/src/components/Menu/src/components/useRenderMenuTitle.tsx new file mode 100644 index 0000000..8941d9d --- /dev/null +++ b/promo-ui2/src/components/Menu/src/components/useRenderMenuTitle.tsx @@ -0,0 +1,27 @@ +import type { RouteMeta } from 'vue-router' +import { Icon } from '@/components/Icon' +import { useI18n } from '@/hooks/web/useI18n' + +export const useRenderMenuTitle = () => { + const renderMenuTitle = (meta: RouteMeta) => { + const { t } = useI18n() + const { title = 'Please set title', icon } = meta + + return icon ? ( + <> + + + {t(title as string)} + + + ) : ( + + {t(title as string)} + + ) + } + + return { + renderMenuTitle + } +} diff --git a/promo-ui2/src/components/Menu/src/helper.ts b/promo-ui2/src/components/Menu/src/helper.ts new file mode 100644 index 0000000..003cf10 --- /dev/null +++ b/promo-ui2/src/components/Menu/src/helper.ts @@ -0,0 +1,54 @@ +import { ref, unref } from 'vue' +import { findPath } from '@/utils/tree' + +type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean } + +interface HasOneShowingChild { + oneShowingChild?: boolean + onlyOneChild?: OnlyOneChildType +} + +export const getAllParentPath = (treeData: T[], path: string) => { + const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[] + return (menuList || []).map((item) => item.path) +} + +export const hasOneShowingChild = ( + children: AppRouteRecordRaw[] = [], + parent: AppRouteRecordRaw +): HasOneShowingChild => { + const onlyOneChild = ref() + + const showingChildren = children.filter((v) => { + const meta = v.meta ?? {} + if (meta.hidden) { + return false + } else { + // Temp set(will be used if only has one showing child) + onlyOneChild.value = v + return true + } + }) + + // When there is only one child router, the child router is displayed by default + if (showingChildren.length === 1) { + return { + oneShowingChild: true, + onlyOneChild: unref(onlyOneChild) + } + } + + // Show parent if there are no child router to display + if (!showingChildren.length) { + onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } + return { + oneShowingChild: true, + onlyOneChild: unref(onlyOneChild) + } + } + + return { + oneShowingChild: false, + onlyOneChild: unref(onlyOneChild) + } +} diff --git a/promo-ui2/src/components/Permission/index.ts b/promo-ui2/src/components/Permission/index.ts new file mode 100644 index 0000000..250ae75 --- /dev/null +++ b/promo-ui2/src/components/Permission/index.ts @@ -0,0 +1,4 @@ +import Permission from './src/Permission.vue' +import { hasPermi } from './src/utils' + +export { Permission, hasPermi } diff --git a/promo-ui2/src/components/Permission/src/Permission.vue b/promo-ui2/src/components/Permission/src/Permission.vue new file mode 100644 index 0000000..8581a9d --- /dev/null +++ b/promo-ui2/src/components/Permission/src/Permission.vue @@ -0,0 +1,29 @@ + + + diff --git a/promo-ui2/src/components/Permission/src/utils.ts b/promo-ui2/src/components/Permission/src/utils.ts new file mode 100644 index 0000000..4c0a9b5 --- /dev/null +++ b/promo-ui2/src/components/Permission/src/utils.ts @@ -0,0 +1,14 @@ +import { useI18n } from '@/hooks/web/useI18n' +import router from '@/router' + +export const hasPermi = (value: string) => { + const { t } = useI18n() + const permission = (router.currentRoute.value.meta.permission || []) as string[] + if (!value) { + throw new Error(t('permission.hasPermission')) + } + if (permission.includes(value)) { + return true + } + return false +} diff --git a/promo-ui2/src/components/Qrcode/index.ts b/promo-ui2/src/components/Qrcode/index.ts new file mode 100644 index 0000000..263f039 --- /dev/null +++ b/promo-ui2/src/components/Qrcode/index.ts @@ -0,0 +1,5 @@ +import Qrcode from './src/Qrcode.vue' + +export type { QrcodeLogo } from './src/types' + +export { Qrcode } diff --git a/promo-ui2/src/components/Qrcode/src/Qrcode.vue b/promo-ui2/src/components/Qrcode/src/Qrcode.vue new file mode 100644 index 0000000..3b3cc1c --- /dev/null +++ b/promo-ui2/src/components/Qrcode/src/Qrcode.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/promo-ui2/src/components/Qrcode/src/types/index.ts b/promo-ui2/src/components/Qrcode/src/types/index.ts new file mode 100644 index 0000000..86cdf0b --- /dev/null +++ b/promo-ui2/src/components/Qrcode/src/types/index.ts @@ -0,0 +1,9 @@ +export interface QrcodeLogo { + src?: string + logoSize?: number + bgColor?: string + borderSize?: number + crossOrigin?: string + borderRadius?: number + logoRadius?: number +} diff --git a/promo-ui2/src/components/Screenfull/index.ts b/promo-ui2/src/components/Screenfull/index.ts new file mode 100644 index 0000000..faec2d8 --- /dev/null +++ b/promo-ui2/src/components/Screenfull/index.ts @@ -0,0 +1,3 @@ +import Screenfull from './src/Screenfull.vue' + +export { Screenfull } diff --git a/promo-ui2/src/components/Screenfull/src/Screenfull.vue b/promo-ui2/src/components/Screenfull/src/Screenfull.vue new file mode 100644 index 0000000..048df9a --- /dev/null +++ b/promo-ui2/src/components/Screenfull/src/Screenfull.vue @@ -0,0 +1,30 @@ + + + diff --git a/promo-ui2/src/components/Search/index.ts b/promo-ui2/src/components/Search/index.ts new file mode 100644 index 0000000..696ad84 --- /dev/null +++ b/promo-ui2/src/components/Search/index.ts @@ -0,0 +1,15 @@ +import { FormSchema, FormSetProps } from '../Form' +import Search from './src/Search.vue' + +export type { SearchProps } from './src/types' + +export interface SearchExpose { + setValues: (data: Recordable) => void + setProps: (props: Recordable) => void + delSchema: (field: string) => void + addSchema: (formSchema: FormSchema, index?: number) => void + setSchema: (schemaProps: FormSetProps[]) => void + getFormData: () => Promise +} + +export { Search } diff --git a/promo-ui2/src/components/Search/src/Search.vue b/promo-ui2/src/components/Search/src/Search.vue new file mode 100644 index 0000000..3e57ed6 --- /dev/null +++ b/promo-ui2/src/components/Search/src/Search.vue @@ -0,0 +1,271 @@ + + + diff --git a/promo-ui2/src/components/Search/src/components/ActionButton.vue b/promo-ui2/src/components/Search/src/components/ActionButton.vue new file mode 100644 index 0000000..8de1838 --- /dev/null +++ b/promo-ui2/src/components/Search/src/components/ActionButton.vue @@ -0,0 +1,59 @@ + + + diff --git a/promo-ui2/src/components/Search/src/types/index.ts b/promo-ui2/src/components/Search/src/types/index.ts new file mode 100644 index 0000000..7a1901d --- /dev/null +++ b/promo-ui2/src/components/Search/src/types/index.ts @@ -0,0 +1,16 @@ +import { FormSchema } from '@/components/Form' + +export interface SearchProps { + schema?: FormSchema[] + isCol?: boolean + labelWidth?: string | number + layout?: 'inline' | 'bottom' + buttonPosition?: 'left' | 'right' | 'center' + showSearch?: boolean + showReset?: boolean + showExpand?: boolean + expandField?: string + inline?: boolean + removeNoValueItem?: boolean + model?: Recordable +} diff --git a/promo-ui2/src/components/Setting/index.ts b/promo-ui2/src/components/Setting/index.ts new file mode 100644 index 0000000..b64c9ad --- /dev/null +++ b/promo-ui2/src/components/Setting/index.ts @@ -0,0 +1,3 @@ +import Setting from './src/Setting.vue' + +export { Setting } diff --git a/promo-ui2/src/components/Setting/src/Setting.vue b/promo-ui2/src/components/Setting/src/Setting.vue new file mode 100644 index 0000000..ecbe72d --- /dev/null +++ b/promo-ui2/src/components/Setting/src/Setting.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/promo-ui2/src/components/Setting/src/components/ColorRadioPicker.vue b/promo-ui2/src/components/Setting/src/components/ColorRadioPicker.vue new file mode 100644 index 0000000..9774176 --- /dev/null +++ b/promo-ui2/src/components/Setting/src/components/ColorRadioPicker.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/promo-ui2/src/components/Setting/src/components/InterfaceDisplay.vue b/promo-ui2/src/components/Setting/src/components/InterfaceDisplay.vue new file mode 100644 index 0000000..466dc65 --- /dev/null +++ b/promo-ui2/src/components/Setting/src/components/InterfaceDisplay.vue @@ -0,0 +1,227 @@ + + + diff --git a/promo-ui2/src/components/Setting/src/components/LayoutRadioPicker.vue b/promo-ui2/src/components/Setting/src/components/LayoutRadioPicker.vue new file mode 100644 index 0000000..2b6d4b5 --- /dev/null +++ b/promo-ui2/src/components/Setting/src/components/LayoutRadioPicker.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/promo-ui2/src/components/SizeDropdown/index.ts b/promo-ui2/src/components/SizeDropdown/index.ts new file mode 100644 index 0000000..516488d --- /dev/null +++ b/promo-ui2/src/components/SizeDropdown/index.ts @@ -0,0 +1,3 @@ +import SizeDropdown from './src/SizeDropdown.vue' + +export { SizeDropdown } diff --git a/promo-ui2/src/components/SizeDropdown/src/SizeDropdown.vue b/promo-ui2/src/components/SizeDropdown/src/SizeDropdown.vue new file mode 100644 index 0000000..eb55917 --- /dev/null +++ b/promo-ui2/src/components/SizeDropdown/src/SizeDropdown.vue @@ -0,0 +1,39 @@ + + + diff --git a/promo-ui2/src/components/TabMenu/index.ts b/promo-ui2/src/components/TabMenu/index.ts new file mode 100644 index 0000000..b5fd71c --- /dev/null +++ b/promo-ui2/src/components/TabMenu/index.ts @@ -0,0 +1,3 @@ +import TabMenu from './src/TabMenu.vue' + +export { TabMenu } diff --git a/promo-ui2/src/components/TabMenu/src/TabMenu.vue b/promo-ui2/src/components/TabMenu/src/TabMenu.vue new file mode 100644 index 0000000..e6cf366 --- /dev/null +++ b/promo-ui2/src/components/TabMenu/src/TabMenu.vue @@ -0,0 +1,251 @@ + + + diff --git a/promo-ui2/src/components/TabMenu/src/helper.ts b/promo-ui2/src/components/TabMenu/src/helper.ts new file mode 100644 index 0000000..cb4696e --- /dev/null +++ b/promo-ui2/src/components/TabMenu/src/helper.ts @@ -0,0 +1,51 @@ +import { getAllParentPath } from '@/components/Menu/src/helper' +import { isUrl } from '@/utils/is' +import { cloneDeep } from 'lodash-es' +import { reactive } from 'vue' + +export type TabMapTypes = { + [key: string]: string[] +} + +export const tabPathMap = reactive({}) + +export const initTabMap = (routes: AppRouteRecordRaw[]) => { + for (const v of routes) { + const meta = v.meta ?? {} + if (!meta?.hidden) { + tabPathMap[v.path] = [] + } + } +} + +export const filterMenusPath = ( + routes: AppRouteRecordRaw[], + allRoutes: AppRouteRecordRaw[] +): AppRouteRecordRaw[] => { + const res: AppRouteRecordRaw[] = [] + for (const v of routes) { + let data: Nullable = null + const meta = v.meta ?? {} + if (!meta.hidden || meta.canTo) { + const allParentPath = getAllParentPath(allRoutes, v.path) + + const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/') + + data = cloneDeep(v) + data.path = fullPath + if (v.children && data) { + data.children = filterMenusPath(v.children, allRoutes) + } + + if (data) { + res.push(data) + } + + if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) { + tabPathMap[allParentPath[0]].push(fullPath) + } + } + } + + return res +} diff --git a/promo-ui2/src/components/Table/index.ts b/promo-ui2/src/components/Table/index.ts new file mode 100644 index 0000000..472ae5d --- /dev/null +++ b/promo-ui2/src/components/Table/index.ts @@ -0,0 +1,21 @@ +import Table from './src/Table.vue' +import { ElTable } from 'element-plus' +import { TableColumn, TableSetProps } from './src/types' + +export type { + TableColumn, + TableSlotDefault, + Pagination, + TableSetProps, + TableProps +} from './src/types' + +export interface TableExpose { + setProps: (props: Recordable) => void + setColumn: (columnProps: TableSetProps[]) => void + addColumn: (column: TableColumn, index?: number) => void + delColumn: (field: string) => void + elTableRef: ComponentRef +} + +export { Table } diff --git a/promo-ui2/src/components/Table/src/Table.vue b/promo-ui2/src/components/Table/src/Table.vue new file mode 100644 index 0000000..9d496f0 --- /dev/null +++ b/promo-ui2/src/components/Table/src/Table.vue @@ -0,0 +1,587 @@ + diff --git a/promo-ui2/src/components/Table/src/components/ColumnSetting.vue b/promo-ui2/src/components/Table/src/components/ColumnSetting.vue new file mode 100644 index 0000000..f0c3825 --- /dev/null +++ b/promo-ui2/src/components/Table/src/components/ColumnSetting.vue @@ -0,0 +1,166 @@ + + + diff --git a/promo-ui2/src/components/Table/src/components/TableActions.vue b/promo-ui2/src/components/Table/src/components/TableActions.vue new file mode 100644 index 0000000..05336bb --- /dev/null +++ b/promo-ui2/src/components/Table/src/components/TableActions.vue @@ -0,0 +1,105 @@ + diff --git a/promo-ui2/src/components/Table/src/helper/index.ts b/promo-ui2/src/components/Table/src/helper/index.ts new file mode 100644 index 0000000..d8b34a8 --- /dev/null +++ b/promo-ui2/src/components/Table/src/helper/index.ts @@ -0,0 +1,8 @@ +export const setIndex = (reserveIndex: boolean, index: number, size: number, current: number) => { + const newIndex = index + 1 + if (reserveIndex) { + return size * (current - 1) + newIndex + } else { + return newIndex + } +} diff --git a/promo-ui2/src/components/Table/src/types/index.ts b/promo-ui2/src/components/Table/src/types/index.ts new file mode 100644 index 0000000..21200a7 --- /dev/null +++ b/promo-ui2/src/components/Table/src/types/index.ts @@ -0,0 +1,98 @@ +import { TableProps as ElTableProps } from 'element-plus' +export interface TableColumn { + field: string + label?: string + type?: string + /** + * 是否隐藏 + */ + hidden?: boolean + children?: TableColumn[] + slots?: { + default?: (...args: any[]) => JSX.Element | JSX.Element[] | null + header?: (...args: any[]) => JSX.Element | null + } + index?: number | ((index: number) => number) + columnKey?: string + width?: string | number + minWidth?: string | number + fixed?: boolean | 'left' | 'right' + renderHeader?: (...args: any[]) => JSX.Element | null + // sortable?: boolean + sortMethod?: (...args: any[]) => number + sortBy?: string | string[] | ((...args: any[]) => string | string[]) + sortOrders?: (string | null)[] + resizable?: boolean + formatter?: (...args: any[]) => any + showOverflowTooltip?: boolean + align?: 'left' | 'center' | 'right' + headerAlign?: 'left' | 'center' | 'right' + className?: string + labelClassName?: string + selectable?: (...args: any[]) => boolean + reserveSelection?: boolean + filters?: Array<{ text: string; value: string }> + filterPlacement?: string + filterMultiple?: boolean + filterMethod?: (...args: any[]) => boolean + filteredValue?: string[] + [key: string]: any +} + +export interface TableSlotDefault { + row: Recordable + column: TableColumn + $index: number + [key: string]: any +} + +export interface Pagination { + small?: boolean + background?: boolean + pageSize?: number + defaultPageSize?: number + total?: number + pageCount?: number + pagerCount?: number + currentPage?: number + defaultCurrentPage?: number + layout?: string + pageSizes?: number[] + popperClass?: string + prevText?: string + nextText?: string + disabled?: boolean + hideOnSinglePage?: boolean +} + +export interface TableSetProps { + field: string + path: string + value: any +} + +export interface TableProps extends Omit>, 'data'> { + pageSize?: number + currentPage?: number + showAction?: boolean + // 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip, + showOverflowTooltip?: boolean + // 表头 + columns?: TableColumn[] + // 是否展示分页 + pagination?: Pagination | undefined + // 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key) + reserveSelection?: boolean + // 加载状态 + loading?: boolean + // 是否叠加索引 + reserveIndex?: boolean + // 对齐方式 + align?: 'left' | 'center' | 'right' + // 表头对齐方式 + headerAlign?: 'left' | 'center' | 'right' + imagePreview?: string[] + videoPreview?: string[] + sortable?: boolean + data?: Recordable +} diff --git a/promo-ui2/src/components/TagsView/index.ts b/promo-ui2/src/components/TagsView/index.ts new file mode 100644 index 0000000..30e604a --- /dev/null +++ b/promo-ui2/src/components/TagsView/index.ts @@ -0,0 +1,3 @@ +import TagsView from './src/TagsView.vue' + +export { TagsView } diff --git a/promo-ui2/src/components/TagsView/src/TagsView.vue b/promo-ui2/src/components/TagsView/src/TagsView.vue new file mode 100644 index 0000000..da3336b --- /dev/null +++ b/promo-ui2/src/components/TagsView/src/TagsView.vue @@ -0,0 +1,590 @@ + + + + + diff --git a/promo-ui2/src/components/TagsView/src/helper.ts b/promo-ui2/src/components/TagsView/src/helper.ts new file mode 100644 index 0000000..912eb8e --- /dev/null +++ b/promo-ui2/src/components/TagsView/src/helper.ts @@ -0,0 +1,21 @@ +import type { RouteLocationNormalizedLoaded } from 'vue-router' +import { pathResolve } from '@/utils/routerHelper' + +export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => { + let tags: RouteLocationNormalizedLoaded[] = [] + routes.forEach((route) => { + const meta = route.meta ?? {} + const tagPath = pathResolve(parentPath, route.path) + if (meta?.affix) { + tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded) + } + if (route.children) { + const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath) + if (tempTags.length >= 1) { + tags = [...tags, ...tempTags] + } + } + }) + + return tags +} diff --git a/promo-ui2/src/components/ThemeSwitch/index.ts b/promo-ui2/src/components/ThemeSwitch/index.ts new file mode 100644 index 0000000..823a276 --- /dev/null +++ b/promo-ui2/src/components/ThemeSwitch/index.ts @@ -0,0 +1,3 @@ +import ThemeSwitch from './src/ThemeSwitch.vue' + +export { ThemeSwitch } diff --git a/promo-ui2/src/components/ThemeSwitch/src/ThemeSwitch.vue b/promo-ui2/src/components/ThemeSwitch/src/ThemeSwitch.vue new file mode 100644 index 0000000..94b02c6 --- /dev/null +++ b/promo-ui2/src/components/ThemeSwitch/src/ThemeSwitch.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/promo-ui2/src/components/Tree/index.ts b/promo-ui2/src/components/Tree/index.ts new file mode 100644 index 0000000..bee5f92 --- /dev/null +++ b/promo-ui2/src/components/Tree/index.ts @@ -0,0 +1,3 @@ +import Tree from './src/Tree.vue' + +export { Tree } diff --git a/promo-ui2/src/components/Tree/src/Tree.vue b/promo-ui2/src/components/Tree/src/Tree.vue new file mode 100644 index 0000000..66597d1 --- /dev/null +++ b/promo-ui2/src/components/Tree/src/Tree.vue @@ -0,0 +1,147 @@ + + + diff --git a/promo-ui2/src/components/UserInfo/index.ts b/promo-ui2/src/components/UserInfo/index.ts new file mode 100644 index 0000000..c3a34ab --- /dev/null +++ b/promo-ui2/src/components/UserInfo/index.ts @@ -0,0 +1,3 @@ +import UserInfo from './src/UserInfo.vue' + +export { UserInfo } diff --git a/promo-ui2/src/components/UserInfo/src/UserInfo.vue b/promo-ui2/src/components/UserInfo/src/UserInfo.vue new file mode 100644 index 0000000..78b7ca9 --- /dev/null +++ b/promo-ui2/src/components/UserInfo/src/UserInfo.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/promo-ui2/src/components/UserInfo/src/components/LockDialog.vue b/promo-ui2/src/components/UserInfo/src/components/LockDialog.vue new file mode 100644 index 0000000..a8dc34f --- /dev/null +++ b/promo-ui2/src/components/UserInfo/src/components/LockDialog.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/promo-ui2/src/components/UserInfo/src/components/LockPage.vue b/promo-ui2/src/components/UserInfo/src/components/LockPage.vue new file mode 100644 index 0000000..e1a19c0 --- /dev/null +++ b/promo-ui2/src/components/UserInfo/src/components/LockPage.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/promo-ui2/src/components/VideoPlayer/index.ts b/promo-ui2/src/components/VideoPlayer/index.ts new file mode 100644 index 0000000..247deca --- /dev/null +++ b/promo-ui2/src/components/VideoPlayer/index.ts @@ -0,0 +1,27 @@ +import { VNode, createVNode, render } from 'vue' +import VideoPlayer from './src/VideoPlayer.vue' +import { isClient } from '@/utils/is' +import { VideoPlayerViewer } from '@/components/VideoPlayerViewer' +import { toAnyString } from '@/utils' + +export { VideoPlayer } + +let instance: Nullable = null + +export function createVideoViewer(options: { url: string; poster?: string; show?: boolean }) { + if (!isClient) return + const { url, poster } = options + + const propsData: Partial<{ url: string; poster?: string; show?: boolean; id?: string }> = {} + const container = document.createElement('div') + const id = toAnyString() + container.id = id + propsData.url = url + propsData.poster = poster + propsData.show = true + propsData.id = id + + document.body.appendChild(container) + instance = createVNode(VideoPlayerViewer, propsData) + render(instance, container) +} diff --git a/promo-ui2/src/components/VideoPlayer/src/VideoPlayer.vue b/promo-ui2/src/components/VideoPlayer/src/VideoPlayer.vue new file mode 100644 index 0000000..83c8d30 --- /dev/null +++ b/promo-ui2/src/components/VideoPlayer/src/VideoPlayer.vue @@ -0,0 +1,59 @@ + + + diff --git a/promo-ui2/src/components/VideoPlayerViewer/index.ts b/promo-ui2/src/components/VideoPlayerViewer/index.ts new file mode 100644 index 0000000..7f99155 --- /dev/null +++ b/promo-ui2/src/components/VideoPlayerViewer/index.ts @@ -0,0 +1,3 @@ +import VideoPlayerViewer from './src/VideoPlayerViewer.vue' + +export { VideoPlayerViewer } diff --git a/promo-ui2/src/components/VideoPlayerViewer/src/VideoPlayerViewer.vue b/promo-ui2/src/components/VideoPlayerViewer/src/VideoPlayerViewer.vue new file mode 100644 index 0000000..56968df --- /dev/null +++ b/promo-ui2/src/components/VideoPlayerViewer/src/VideoPlayerViewer.vue @@ -0,0 +1,49 @@ + + diff --git a/promo-ui2/src/components/Waterfall/index.ts b/promo-ui2/src/components/Waterfall/index.ts new file mode 100644 index 0000000..6fed67f --- /dev/null +++ b/promo-ui2/src/components/Waterfall/index.ts @@ -0,0 +1,3 @@ +import Waterfall from './src/Waterfall.vue' + +export { Waterfall } diff --git a/promo-ui2/src/components/Waterfall/src/Waterfall.vue b/promo-ui2/src/components/Waterfall/src/Waterfall.vue new file mode 100644 index 0000000..32a0aa7 --- /dev/null +++ b/promo-ui2/src/components/Waterfall/src/Waterfall.vue @@ -0,0 +1,234 @@ + + + diff --git a/promo-ui2/src/components/index.ts b/promo-ui2/src/components/index.ts new file mode 100644 index 0000000..9a179ea --- /dev/null +++ b/promo-ui2/src/components/index.ts @@ -0,0 +1,10 @@ +import type { App } from 'vue' +import { Icon } from './Icon' +import { Permission } from './Permission' +import { BaseButton } from './Button' + +export const setupGlobCom = (app: App): void => { + app.component('Icon', Icon) + app.component('Permission', Permission) + app.component('BaseButton', BaseButton) +} diff --git a/promo-ui2/src/constants/index.ts b/promo-ui2/src/constants/index.ts new file mode 100644 index 0000000..cdf427e --- /dev/null +++ b/promo-ui2/src/constants/index.ts @@ -0,0 +1,39 @@ +/** + * 请求成功状态码 + */ +export const SUCCESS_CODE = 0 + +/** + * 请求contentType + */ +export const CONTENT_TYPE: AxiosContentType = 'application/json' + +/** + * 请求超时时间 + */ +export const REQUEST_TIMEOUT = 60000 + +/** + * 不重定向白名单 + */ +export const NO_REDIRECT_WHITE_LIST = ['/login'] + +/** + * 不重置路由白名单 + */ +export const NO_RESET_WHITE_LIST = ['Redirect', 'RedirectWrap', 'Login', 'NoFind', 'Root'] + +/** + * 表格默认过滤列设置字段 + */ +export const DEFAULT_FILTER_COLUMN = ['expand', 'selection'] + +/** + * 是否根据headers->content-type自动转换数据格式 + */ +export const TRANSFORM_REQUEST_DATA = true + +/** + * 全局图标前缀 + */ +export const ICON_PREFIX = 'vi-' diff --git a/promo-ui2/src/directives/index.ts b/promo-ui2/src/directives/index.ts new file mode 100644 index 0000000..11b1da8 --- /dev/null +++ b/promo-ui2/src/directives/index.ts @@ -0,0 +1,10 @@ +import type { App } from 'vue' +import { setupPermissionDirective } from './permission/hasPermi' + +/** + * 导出指令:v-xxx + * @methods hasPermi 按钮权限,用法: v-hasPermi + */ +export const setupPermission = (app: App) => { + setupPermissionDirective(app) +} diff --git a/promo-ui2/src/directives/permission/hasPermi.ts b/promo-ui2/src/directives/permission/hasPermi.ts new file mode 100644 index 0000000..0a74d94 --- /dev/null +++ b/promo-ui2/src/directives/permission/hasPermi.ts @@ -0,0 +1,37 @@ +import type { App, Directive, DirectiveBinding } from 'vue' +import { useI18n } from '@/hooks/web/useI18n' +import router from '@/router' + +const { t } = useI18n() + +const hasPermission = (value: string): boolean => { + const permission = (router.currentRoute.value.meta.permission || []) as string[] + if (!value) { + throw new Error(t('permission.hasPermission')) + } + if (permission.includes(value)) { + return true + } + return false +} +function hasPermi(el: Element, binding: DirectiveBinding) { + const value = binding.value + + const flag = hasPermission(value) + if (!flag) { + el.parentNode?.removeChild(el) + } +} +const mounted = (el: Element, binding: DirectiveBinding) => { + hasPermi(el, binding) +} + +const permiDirective: Directive = { + mounted +} + +export const setupPermissionDirective = (app: App) => { + app.directive('hasPermi', permiDirective) +} + +export default permiDirective diff --git a/promo-ui2/src/hooks/event/useEventBus.ts b/promo-ui2/src/hooks/event/useEventBus.ts new file mode 100644 index 0000000..d9ee832 --- /dev/null +++ b/promo-ui2/src/hooks/event/useEventBus.ts @@ -0,0 +1,26 @@ +import mitt from 'mitt' +import { onBeforeUnmount } from 'vue' + +interface Option { + name: string // 事件名称 + callback: Fn // 回调 +} + +const emitter = mitt() + +export const useEventBus = (option?: Option) => { + if (option) { + emitter.on(option.name, option.callback) + + onBeforeUnmount(() => { + emitter.off(option.name) + }) + } + + return { + on: emitter.on, + off: emitter.off, + emit: emitter.emit, + all: emitter.all + } +} diff --git a/promo-ui2/src/hooks/event/useScrollTo.ts b/promo-ui2/src/hooks/event/useScrollTo.ts new file mode 100644 index 0000000..74fd673 --- /dev/null +++ b/promo-ui2/src/hooks/event/useScrollTo.ts @@ -0,0 +1,62 @@ +import { ref, unref } from 'vue' + +export interface ScrollToParams { + el: HTMLElement + to: number + position: string + duration?: number + callback?: () => void +} + +const easeInOutQuad = (t: number, b: number, c: number, d: number) => { + t /= d / 2 + if (t < 1) { + return (c / 2) * t * t + b + } + t-- + return (-c / 2) * (t * (t - 2) - 1) + b +} +const move = (el: HTMLElement, position: string, amount: number) => { + el[position] = amount +} + +export function useScrollTo({ + el, + position = 'scrollLeft', + to, + duration = 500, + callback +}: ScrollToParams) { + const isActiveRef = ref(false) + const start = el[position] + const change = to - start + const increment = 20 + let currentTime = 0 + + function animateScroll() { + if (!unref(isActiveRef)) { + return + } + currentTime += increment + const val = easeInOutQuad(currentTime, start, change, duration) + move(el, position, val) + if (currentTime < duration && unref(isActiveRef)) { + requestAnimationFrame(animateScroll) + } else { + if (callback) { + callback() + } + } + } + + function run() { + isActiveRef.value = true + animateScroll() + } + + function stop() { + isActiveRef.value = false + } + + return { start: run, stop } +} diff --git a/promo-ui2/src/hooks/web/useClipboard.ts b/promo-ui2/src/hooks/web/useClipboard.ts new file mode 100644 index 0000000..fb8c3d6 --- /dev/null +++ b/promo-ui2/src/hooks/web/useClipboard.ts @@ -0,0 +1,47 @@ +import { ref } from 'vue' + +const useClipboard = () => { + const copied = ref(false) + const text = ref('') + const isSupported = ref(false) + + if (!navigator.clipboard && !document.execCommand) { + isSupported.value = false + } else { + isSupported.value = true + } + + const copy = (str: string) => { + if (navigator.clipboard) { + navigator.clipboard.writeText(str).then(() => { + text.value = str + copied.value = true + resetCopied() + }) + return + } + const input = document.createElement('input') + input.setAttribute('readonly', 'readonly') + input.setAttribute('value', str) + document.body.appendChild(input) + input.select() + input.setSelectionRange(0, 9999) + if (document.execCommand('copy')) { + text.value = str + document.execCommand('copy') + copied.value = true + resetCopied() + } + document.body.removeChild(input) + } + + const resetCopied = () => { + setTimeout(() => { + copied.value = false + }, 1500) + } + + return { copy, text, copied, isSupported } +} + +export { useClipboard } diff --git a/promo-ui2/src/hooks/web/useConfigGlobal.ts b/promo-ui2/src/hooks/web/useConfigGlobal.ts new file mode 100644 index 0000000..d1e490b --- /dev/null +++ b/promo-ui2/src/hooks/web/useConfigGlobal.ts @@ -0,0 +1,10 @@ +import { ConfigGlobalTypes } from '@/components/ConfigGlobal' +import { inject } from 'vue' + +export const useConfigGlobal = () => { + const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes + + return { + configGlobal + } +} diff --git a/promo-ui2/src/hooks/web/useCrudSchemas.ts b/promo-ui2/src/hooks/web/useCrudSchemas.ts new file mode 100644 index 0000000..5e61d65 --- /dev/null +++ b/promo-ui2/src/hooks/web/useCrudSchemas.ts @@ -0,0 +1,163 @@ +import { reactive } from 'vue' +import { eachTree, treeMap, filter } from '@/utils/tree' +import { FormSchema } from '@/components/Form' +import { TableColumn } from '@/components/Table' +import { DescriptionsSchema } from '@/components/Descriptions' + +export type CrudSchema = Omit & { + search?: CrudSearchParams + table?: CrudTableParams + form?: CrudFormParams + detail?: CrudDescriptionsParams + children?: CrudSchema[] +} + +interface CrudSearchParams extends Omit { + // 是否隐藏在查询项 + hidden?: boolean +} + +interface CrudTableParams extends Omit { + // 是否隐藏表头 + hidden?: boolean +} + +interface CrudFormParams extends Omit { + // 是否隐藏表单项 + hidden?: boolean +} + +interface CrudDescriptionsParams extends Omit { + // 是否隐藏表单项 + hidden?: boolean +} + +interface AllSchemas { + searchSchema: FormSchema[] + tableColumns: TableColumn[] + formSchema: FormSchema[] + detailSchema: DescriptionsSchema[] +} + +/** + * @deprecated 不推荐使用,感觉过于繁琐,不是很灵活 可能会在某个版本中删除 + */ +export const useCrudSchemas = ( + crudSchema: CrudSchema[] +): { + allSchemas: AllSchemas +} => { + // 所有结构数据 + const allSchemas = reactive({ + searchSchema: [], + tableColumns: [], + formSchema: [], + detailSchema: [] + }) + + const searchSchema = filterSearchSchema(crudSchema) + // @ts-ignore + allSchemas.searchSchema = searchSchema || [] + + const tableColumns = filterTableSchema(crudSchema) + allSchemas.tableColumns = tableColumns || [] + + const formSchema = filterFormSchema(crudSchema) + allSchemas.formSchema = formSchema + + const detailSchema = filterDescriptionsSchema(crudSchema) + allSchemas.detailSchema = detailSchema + + return { + allSchemas + } +} + +// 过滤 Search 结构 +const filterSearchSchema = (crudSchema: CrudSchema[]): FormSchema[] => { + const searchSchema: FormSchema[] = [] + const length = crudSchema.length + + for (let i = 0; i < length; i++) { + const schemaItem = crudSchema[i] + if (schemaItem.search?.hidden === true) { + continue + } + // 判断是否隐藏 + const searchSchemaItem = { + component: schemaItem?.search?.component || 'Input', + ...schemaItem.search, + field: schemaItem.field, + label: schemaItem.search?.label || schemaItem.label + } + + searchSchema.push(searchSchemaItem) + } + + return searchSchema +} + +// 过滤 table 结构 +const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => { + const tableColumns = treeMap(crudSchema, { + conversion: (schema: CrudSchema) => { + if (!schema?.table?.hidden) { + return { + ...schema, + ...schema.table + } + } + } + }) + + // 第一次过滤会有 undefined 所以需要二次过滤 + return filter(tableColumns as TableColumn[], (data) => { + if (data.children === void 0) { + delete data.children + } + return !!data.field + }) +} + +// 过滤 form 结构 +const filterFormSchema = (crudSchema: CrudSchema[]): FormSchema[] => { + const formSchema: FormSchema[] = [] + const length = crudSchema.length + + for (let i = 0; i < length; i++) { + const formItem = crudSchema[i] + const formSchemaItem = { + component: formItem?.form?.component || 'Input', + ...formItem.form, + field: formItem.field, + label: formItem.form?.label || formItem.label + } + + formSchema.push(formSchemaItem) + } + + return formSchema +} + +// 过滤 descriptions 结构 +const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[] => { + const descriptionsSchema: FormSchema[] = [] + + eachTree(crudSchema, (schemaItem: CrudSchema) => { + // 判断是否隐藏 + if (!schemaItem?.detail?.hidden) { + const descriptionsSchemaItem = { + ...schemaItem.detail, + field: schemaItem.field, + label: schemaItem.detail?.label || schemaItem.label + } + + // 删除不必要的字段 + delete descriptionsSchemaItem.hidden + + descriptionsSchema.push(descriptionsSchemaItem) + } + }) + + return descriptionsSchema +} diff --git a/promo-ui2/src/hooks/web/useDesign.ts b/promo-ui2/src/hooks/web/useDesign.ts new file mode 100644 index 0000000..1ec349f --- /dev/null +++ b/promo-ui2/src/hooks/web/useDesign.ts @@ -0,0 +1,18 @@ +import variables from '@/styles/variables.module.less' + +export const useDesign = () => { + const lessVariables = variables + + /** + * @param scope 类名 + * @returns 返回空间名-类名 + */ + const getPrefixCls = (scope: string) => { + return `${lessVariables.namespace}-${scope}` + } + + return { + variables: lessVariables, + getPrefixCls + } +} diff --git a/promo-ui2/src/hooks/web/useForm.ts b/promo-ui2/src/hooks/web/useForm.ts new file mode 100644 index 0000000..cdf5eac --- /dev/null +++ b/promo-ui2/src/hooks/web/useForm.ts @@ -0,0 +1,149 @@ +import type { Form, FormExpose } from '@/components/Form' +import type { ElForm, ElFormItem } from 'element-plus' +import { ref, unref, nextTick } from 'vue' +import { FormSchema, FormSetProps, FormProps } from '@/components/Form' +import { isEmptyVal, isObject } from '@/utils/is' + +export const useForm = () => { + // From实例 + const formRef = ref() + + // ElForm实例 + const elFormRef = ref>() + + /** + * @param ref Form实例 + * @param elRef ElForm实例 + */ + const register = (ref: typeof Form & FormExpose, elRef: ComponentRef) => { + formRef.value = ref + elFormRef.value = elRef + } + + const getForm = async () => { + await nextTick() + const form = unref(formRef) + if (!form) { + console.error('The form is not registered. Please use the register method to register') + } + return form + } + + // 一些内置的方法 + const methods = { + /** + * @description 设置form组件的props + * @param props form组件的props + */ + setProps: async (props: FormProps = {}) => { + const form = await getForm() + form?.setProps(props) + if (props.model) { + form?.setValues(props.model) + } + }, + + /** + * @description 设置form的值 + * @param data 需要设置的数据 + */ + setValues: async (data: Recordable) => { + const form = await getForm() + form?.setValues(data) + }, + + /** + * @description 设置schema + * @param schemaProps 需要设置的schemaProps + */ + setSchema: async (schemaProps: FormSetProps[]) => { + const form = await getForm() + form?.setSchema(schemaProps) + }, + + /** + * @description 新增schema + * @param formSchema 需要新增数据 + * @param index 在哪里新增 + */ + addSchema: async (formSchema: FormSchema, index?: number) => { + const form = await getForm() + form?.addSchema(formSchema, index) + }, + + /** + * @description 删除schema + * @param field 删除哪个数据 + */ + delSchema: async (field: string) => { + const form = await getForm() + form?.delSchema(field) + }, + + /** + * @description 获取表单数据 + * @returns form data + */ + getFormData: async (filterEmptyVal = true): Promise => { + const form = await getForm() + const model = form?.formModel as any + if (filterEmptyVal) { + // 使用reduce过滤空值,并返回一个新对象 + return Object.keys(model).reduce((prev, next) => { + const value = model[next] + if (!isEmptyVal(value)) { + if (isObject(value)) { + if (Object.keys(value).length > 0) { + prev[next] = value + } + } else { + prev[next] = value + } + } + return prev + }, {}) as T + } else { + return model as T + } + }, + + /** + * @description 获取表单组件的实例 + * @param field 表单项唯一标识 + * @returns component instance + */ + getComponentExpose: async (field: string) => { + const form = await getForm() + return form?.getComponentExpose(field) + }, + + /** + * @description 获取formItem组件的实例 + * @param field 表单项唯一标识 + * @returns formItem instance + */ + getFormItemExpose: async (field: string) => { + const form = await getForm() + return form?.getFormItemExpose(field) as ComponentRef + }, + + /** + * @description 获取ElForm组件的实例 + * @returns ElForm instance + */ + getElFormExpose: async () => { + await getForm() + return unref(elFormRef) + }, + + getFormExpose: async () => { + await getForm() + return unref(formRef) + } + } + + return { + formRegister: register, + formMethods: methods + } +} diff --git a/promo-ui2/src/hooks/web/useGuide.ts b/promo-ui2/src/hooks/web/useGuide.ts new file mode 100644 index 0000000..7fd2fb0 --- /dev/null +++ b/promo-ui2/src/hooks/web/useGuide.ts @@ -0,0 +1,49 @@ +import { Config, driver } from 'driver.js' +import 'driver.js/dist/driver.css' +import { useDesign } from '@/hooks/web/useDesign' +import { useI18n } from '@/hooks/web/useI18n' + +const { t } = useI18n() + +const { variables } = useDesign() + +export const useGuide = (options?: Config) => { + const driverObj = driver( + options || { + showProgress: true, + nextBtnText: t('common.nextLabel'), + prevBtnText: t('common.prevLabel'), + doneBtnText: t('common.doneLabel'), + steps: [ + { + element: `#${variables.namespace}-menu`, + popover: { + title: t('common.menu'), + description: t('common.menuDes'), + side: 'right' + } + }, + { + element: `#${variables.namespace}-tool-header`, + popover: { + title: t('common.tool'), + description: t('common.toolDes'), + side: 'left' + } + }, + { + element: `#${variables.namespace}-tags-view`, + popover: { + title: t('common.tagsView'), + description: t('common.tagsViewDes'), + side: 'bottom' + } + } + ] + } + ) + + return { + ...driverObj + } +} diff --git a/promo-ui2/src/hooks/web/useI18n.ts b/promo-ui2/src/hooks/web/useI18n.ts new file mode 100644 index 0000000..f1e0e7b --- /dev/null +++ b/promo-ui2/src/hooks/web/useI18n.ts @@ -0,0 +1,52 @@ +import { i18n } from '@/plugins/vueI18n' + +type I18nGlobalTranslation = { + (key: string): string + (key: string, locale: string): string + (key: string, locale: string, list: unknown[]): string + (key: string, locale: string, named: Record): string + (key: string, list: unknown[]): string + (key: string, named: Record): string +} + +type I18nTranslationRestParameters = [string, any] + +const getKey = (namespace: string | undefined, key: string) => { + if (!namespace) { + return key + } + if (key.startsWith(namespace)) { + return key + } + return `${namespace}.${key}` +} + +export const useI18n = ( + namespace?: string +): { + t: I18nGlobalTranslation +} => { + const normalFn = { + t: (key: string) => { + return getKey(namespace, key) + } + } + + if (!i18n) { + return normalFn + } + + const { t, ...methods } = i18n.global + + const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => { + if (!key) return '' + if (!key.includes('.') && !namespace) return key + return (t as any)(getKey(namespace, key), ...(arg as I18nTranslationRestParameters)) + } + return { + ...methods, + t: tFn + } +} + +export const t = (key: string) => key diff --git a/promo-ui2/src/hooks/web/useIcon.ts b/promo-ui2/src/hooks/web/useIcon.ts new file mode 100644 index 0000000..b76bb29 --- /dev/null +++ b/promo-ui2/src/hooks/web/useIcon.ts @@ -0,0 +1,7 @@ +import { h } from 'vue' +import type { VNode } from 'vue' +import { Icon, IconTypes } from '@/components/Icon' + +export const useIcon = (props: IconTypes): VNode => { + return h(Icon, props) +} diff --git a/promo-ui2/src/hooks/web/useLocale.ts b/promo-ui2/src/hooks/web/useLocale.ts new file mode 100644 index 0000000..c65070e --- /dev/null +++ b/promo-ui2/src/hooks/web/useLocale.ts @@ -0,0 +1,35 @@ +import { i18n } from '@/plugins/vueI18n' +import { useLocaleStoreWithOut } from '@/store/modules/locale' +import { setHtmlPageLang } from '@/plugins/vueI18n/helper' + +const setI18nLanguage = (locale: LocaleType) => { + const localeStore = useLocaleStoreWithOut() + + if (i18n.mode === 'legacy') { + i18n.global.locale = locale + } else { + ;(i18n.global.locale as any).value = locale + } + localeStore.setCurrentLocale({ + lang: locale + }) + setHtmlPageLang(locale) +} + +export const useLocale = () => { + // Switching the language will change the locale of useI18n + // And submit to configuration modification + const changeLocale = async (locale: LocaleType) => { + const globalI18n = i18n.global + + const langModule = await import(`../../locales/${locale}.ts`) + + globalI18n.setLocaleMessage(locale, langModule.default) + + setI18nLanguage(locale) + } + + return { + changeLocale + } +} diff --git a/promo-ui2/src/hooks/web/useMonacoEditor.ts b/promo-ui2/src/hooks/web/useMonacoEditor.ts new file mode 100644 index 0000000..ab464ea --- /dev/null +++ b/promo-ui2/src/hooks/web/useMonacoEditor.ts @@ -0,0 +1,129 @@ +import * as monaco from 'monaco-editor' +import { ref, nextTick, onBeforeUnmount } from 'vue' +import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' +import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker' +import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker' +import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker' +import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker' + +self.MonacoEnvironment = { + getWorker(_, label) { + if (label === 'json') { + return new jsonWorker() + } + if (label === 'css' || label === 'scss' || label === 'less') { + return new cssWorker() + } + if (label === 'html' || label === 'handlebars' || label === 'razor') { + return new htmlWorker() + } + if (label === 'typescript' || label === 'javascript') { + return new tsWorker() + } + return new editorWorker() + } +} + +export function useMonacoEditor(language: string = 'javascript') { + // 编辑器示例 + let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null + // 目标元素 + const monacoEditorRef = ref() + + // 创建实例 + function createEditor(editorOption: monaco.editor.IStandaloneEditorConstructionOptions = {}) { + if (!monacoEditorRef.value) return + monacoEditor = monaco.editor.create(monacoEditorRef.value, { + // 初始模型 + model: monaco.editor.createModel('', language), + // 是否启用预览图 + minimap: { enabled: true }, + // 圆角 + roundedSelection: true, + // 主题 + theme: 'vs-dark', + // 主键 + multiCursorModifier: 'ctrlCmd', + // 滚动条 + scrollbar: { + verticalScrollbarSize: 8, + horizontalScrollbarSize: 8 + }, + // 行号 + lineNumbers: 'on', + // tab大小 + tabSize: 2, + //字体大小 + fontSize: 14, + // 控制编辑器在用户键入、粘贴、移动或缩进行时是否应自动调整缩进 + autoIndent: 'advanced', + // 自动布局 + automaticLayout: true, + ...editorOption + }) + return monacoEditor + } + + // 格式化 + async function formatDoc() { + await monacoEditor?.getAction('editor.action.formatDocument')?.run() + } + + // 数据更新 + function updateVal(val: string) { + nextTick(() => { + if (getOption(monaco.editor.EditorOption.readOnly)) { + updateOptions({ readOnly: false }) + } + monacoEditor?.setValue(val) + setTimeout(async () => { + await formatDoc() + }, 10) + }) + } + + // 配置更新 + function updateOptions(opt: monaco.editor.IStandaloneEditorConstructionOptions) { + monacoEditor?.updateOptions(opt) + } + + // 获取配置 + function getOption(name: monaco.editor.EditorOption) { + return monacoEditor?.getOption(name) + } + + // 获取实例 + function getEditor() { + return monacoEditor + } + + function changeLanguage(newLanguage: string) { + const model = monacoEditor?.getModel() + if (model) { + monaco.editor.setModelLanguage(model, newLanguage) + } + } + + function changeTheme(newTheme: string) { + monaco.editor.setTheme(newTheme) + } + + // 页面离开 销毁 + onBeforeUnmount(() => { + if (monacoEditor) { + monacoEditor.dispose() + } + }) + + return { + monacoEditorRef, + createEditor, + getEditor, + updateVal, + updateOptions, + getOption, + formatDoc, + changeLanguage, + changeTheme + } +} diff --git a/promo-ui2/src/hooks/web/useNProgress.ts b/promo-ui2/src/hooks/web/useNProgress.ts new file mode 100644 index 0000000..76859e5 --- /dev/null +++ b/promo-ui2/src/hooks/web/useNProgress.ts @@ -0,0 +1,34 @@ +import { nextTick, unref } from 'vue' +import type { NProgressOptions } from 'nprogress' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import { useCssVar } from '@vueuse/core' + +const primaryColor = useCssVar('--el-color-primary', document.documentElement) + +export const useNProgress = () => { + NProgress.configure({ showSpinner: false } as NProgressOptions) + + const initColor = async () => { + await nextTick() + const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef + if (bar) { + bar.style.background = unref(primaryColor.value) as string + } + } + + initColor() + + const start = () => { + NProgress.start() + } + + const done = () => { + NProgress.done() + } + + return { + start, + done + } +} diff --git a/promo-ui2/src/hooks/web/useNetwork.ts b/promo-ui2/src/hooks/web/useNetwork.ts new file mode 100644 index 0000000..66fa446 --- /dev/null +++ b/promo-ui2/src/hooks/web/useNetwork.ts @@ -0,0 +1,21 @@ +import { ref, onBeforeUnmount } from 'vue' + +const useNetwork = () => { + const online = ref(true) + + const updateNetwork = () => { + online.value = navigator.onLine + } + + window.addEventListener('online', updateNetwork) + window.addEventListener('offline', updateNetwork) + + onBeforeUnmount(() => { + window.removeEventListener('online', updateNetwork) + window.removeEventListener('offline', updateNetwork) + }) + + return { online } +} + +export { useNetwork } diff --git a/promo-ui2/src/hooks/web/useNow.ts b/promo-ui2/src/hooks/web/useNow.ts new file mode 100644 index 0000000..09d3176 --- /dev/null +++ b/promo-ui2/src/hooks/web/useNow.ts @@ -0,0 +1,60 @@ +import { dateUtil } from '@/utils/dateUtil' +import { reactive, toRefs } from 'vue' +import { tryOnMounted, tryOnUnmounted } from '@vueuse/core' + +export const useNow = (immediate = true) => { + let timer: IntervalHandle + + const state = reactive({ + year: 0, + month: 0, + week: '', + day: 0, + hour: '', + minute: '', + second: 0, + meridiem: '' + }) + + const update = () => { + const now = dateUtil() + + const h = now.format('HH') + const m = now.format('mm') + const s = now.get('s') + + state.year = now.get('y') + state.month = now.get('M') + 1 + state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()] + state.day = now.get('date') + state.hour = h + state.minute = m + state.second = s + + state.meridiem = now.format('A') + } + + function start() { + update() + clearInterval(timer) + timer = setInterval(() => update(), 1000) + } + + function stop() { + clearInterval(timer) + } + + tryOnMounted(() => { + immediate && start() + }) + + tryOnUnmounted(() => { + stop() + }) + + return { + ...toRefs(state), + start, + stop + } +} diff --git a/promo-ui2/src/hooks/web/usePageLoading.ts b/promo-ui2/src/hooks/web/usePageLoading.ts new file mode 100644 index 0000000..995c941 --- /dev/null +++ b/promo-ui2/src/hooks/web/usePageLoading.ts @@ -0,0 +1,20 @@ +import { useAppStoreWithOut } from '@/store/modules/app' + +export const usePageLoading = () => { + const loadStart = () => { + const appStore = useAppStoreWithOut() + + appStore.setPageLoading(true) + } + + const loadDone = () => { + const appStore = useAppStoreWithOut() + + appStore.setPageLoading(false) + } + + return { + loadStart, + loadDone + } +} diff --git a/promo-ui2/src/hooks/web/useSearch.ts b/promo-ui2/src/hooks/web/useSearch.ts new file mode 100644 index 0000000..788c49f --- /dev/null +++ b/promo-ui2/src/hooks/web/useSearch.ts @@ -0,0 +1,91 @@ +import { ref, unref, nextTick } from 'vue' +import { FormSchema, FormSetProps } from '@/components/Form' +import { SearchExpose, SearchProps } from '@/components/Search' + +export const useSearch = () => { + // Search实例 + const searchRef = ref() + + /** + * @param ref Search实例 + * @param elRef ElForm实例 + */ + const register = (ref: SearchExpose) => { + searchRef.value = ref + } + + const getSearch = async () => { + await nextTick() + const search = unref(searchRef) + if (!search) { + console.error('The Search is not registered. Please use the register method to register') + } + return search + } + + // 一些内置的方法 + const methods = { + /** + * @description 设置search组件的props + * @param field FormItem的field + */ + setProps: async (props: SearchProps = {}) => { + const search = await getSearch() + search?.setProps(props) + if (props.model) { + search?.setValues(props.model) + } + }, + + /** + * @description 设置form的值 + * @param data 需要设置的数据 + */ + setValues: async (data: Recordable) => { + const search = await getSearch() + search?.setValues(data) + }, + + /** + * @description 设置schema + * @param schemaProps 需要设置的schemaProps + */ + setSchema: async (schemaProps: FormSetProps[]) => { + const search = await getSearch() + search?.setSchema(schemaProps) + }, + + /** + * @description 新增schema + * @param formSchema 需要新增数据 + * @param index 在哪里新增 + */ + addSchema: async (formSchema: FormSchema, index?: number) => { + const search = await getSearch() + search?.addSchema(formSchema, index) + }, + + /** + * @description 删除schema + * @param field 删除哪个数据 + */ + delSchema: async (field: string) => { + const search = await getSearch() + search?.delSchema(field) + }, + + /** + * @description 获取表单数据 + * @returns form data + */ + getFormData: async (): Promise => { + const search = await getSearch() + return search?.getFormData() as T + } + } + + return { + searchRegister: register, + searchMethods: methods + } +} diff --git a/promo-ui2/src/hooks/web/useStorage.ts b/promo-ui2/src/hooks/web/useStorage.ts new file mode 100644 index 0000000..e33a6a5 --- /dev/null +++ b/promo-ui2/src/hooks/web/useStorage.ts @@ -0,0 +1,46 @@ +// 获取传入的值的类型 +const getValueType = (value: any) => { + const type = Object.prototype.toString.call(value) + return type.slice(8, -1) +} + +export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => { + const setStorage = (key: string, value: any) => { + const valueType = getValueType(value) + window[type].setItem(key, JSON.stringify({ type: valueType, value })) + } + + const getStorage = (key: string) => { + const value = window[type].getItem(key) + if (value) { + const { value: val } = JSON.parse(value) + return val + } else { + return value + } + } + + const removeStorage = (key: string) => { + window[type].removeItem(key) + } + + const clear = (excludes?: string[]) => { + // 获取排除项 + const keys = Object.keys(window[type]) + const defaultExcludes = ['dynamicRouter', 'serverDynamicRouter'] + const excludesArr = excludes ? [...excludes, ...defaultExcludes] : defaultExcludes + const excludesKeys = excludesArr ? keys.filter((key) => !excludesArr.includes(key)) : keys + // 排除项不清除 + excludesKeys.forEach((key) => { + window[type].removeItem(key) + }) + // window[type].clear() + } + + return { + setStorage, + getStorage, + removeStorage, + clear + } +} diff --git a/promo-ui2/src/hooks/web/useTable.ts b/promo-ui2/src/hooks/web/useTable.ts new file mode 100644 index 0000000..d831f1c --- /dev/null +++ b/promo-ui2/src/hooks/web/useTable.ts @@ -0,0 +1,195 @@ +import { useI18n } from '@/hooks/web/useI18n' +import { Table, TableExpose, TableProps, TableSetProps, TableColumn } from '@/components/Table' +import { ElTable, ElMessageBox, ElMessage } from 'element-plus' +import { ref, watch, unref, nextTick, onMounted } from 'vue' + +const { t } = useI18n() + +interface UseTableConfig { + /** + * 是否初始化的时候请求一次 + */ + immediate?: boolean + fetchDataApi: () => Promise<{ + list: any[] + total?: number + }> + fetchDelApi?: () => Promise +} + +export const useTable = (config: UseTableConfig) => { + const { immediate = true } = config + + const loading = ref(false) + const currentPage = ref(1) + const pageSize = ref(10) + const total = ref(0) + const dataList = ref([]) + let isPageSizeChange = false + + watch( + () => currentPage.value, + () => { + if (!isPageSizeChange) methods.getList() + isPageSizeChange = false + } + ) + + watch( + () => pageSize.value, + () => { + if (unref(currentPage) === 1) { + methods.getList() + } else { + currentPage.value = 1 + isPageSizeChange = true + methods.getList() + } + } + ) + + onMounted(() => { + if (immediate) { + methods.getList() + } + }) + + // Table实例 + const tableRef = ref() + + // ElTable实例 + const elTableRef = ref>() + + const register = (ref: typeof Table & TableExpose, elRef: ComponentRef) => { + tableRef.value = ref + elTableRef.value = unref(elRef) + } + + const getTable = async () => { + await nextTick() + const table = unref(tableRef) + if (!table) { + console.error('The table is not registered. Please use the register method to register') + } + return table + } + + const methods = { + /** + * 获取表单数据 + */ + getList: async () => { + loading.value = true + try { + const res = await config?.fetchDataApi() + console.log('fetchDataApi res', res) + if (res) { + dataList.value = res.list + total.value = res.total || 0 + } + } catch (err) { + console.log('fetchDataApi error') + } finally { + loading.value = false + } + }, + + /** + * @description 设置table组件的props + * @param props table组件的props + */ + setProps: async (props: TableProps = {}) => { + const table = await getTable() + table?.setProps(props) + }, + + /** + * @description 设置column + * @param columnProps 需要设置的列 + */ + setColumn: async (columnProps: TableSetProps[]) => { + const table = await getTable() + table?.setColumn(columnProps) + }, + + /** + * @description 新增column + * @param tableColumn 需要新增数据 + * @param index 在哪里新增 + */ + addColumn: async (tableColumn: TableColumn, index?: number) => { + const table = await getTable() + table?.addColumn(tableColumn, index) + }, + + /** + * @description 删除column + * @param field 删除哪个数据 + */ + delColumn: async (field: string) => { + const table = await getTable() + table?.delColumn(field) + }, + + /** + * @description 获取ElTable组件的实例 + * @returns ElTable instance + */ + getElTableExpose: async () => { + await getTable() + return unref(elTableRef) + }, + + refresh: () => { + methods.getList() + }, + + // sortableChange: (e: any) => { + // console.log('sortableChange', e) + // const { oldIndex, newIndex } = e + // dataList.value.splice(newIndex, 0, dataList.value.splice(oldIndex, 1)[0]) + // // to do something + // } + // 删除数据 + delList: async (idsLength: number) => { + const { fetchDelApi } = config + if (!fetchDelApi) { + console.warn('fetchDelApi is undefined') + return + } + ElMessageBox.confirm(t('common.delMessage'), t('common.delWarning'), { + confirmButtonText: t('common.delOk'), + cancelButtonText: t('common.delCancel'), + type: 'warning' + }).then(async () => { + const res = await fetchDelApi() + if (res) { + ElMessage.success(t('common.delSuccess')) + + // 计算出临界点 + const current = + unref(total) % unref(pageSize) === idsLength || unref(pageSize) === 1 + ? unref(currentPage) > 1 + ? unref(currentPage) - 1 + : unref(currentPage) + : unref(currentPage) + + currentPage.value = current + methods.getList() + } + }) + } + } + + return { + tableRegister: register, + tableMethods: methods, + tableState: { + currentPage, + pageSize, + total, + dataList, + loading + } + } +} diff --git a/promo-ui2/src/hooks/web/useTagsView.ts b/promo-ui2/src/hooks/web/useTagsView.ts new file mode 100644 index 0000000..31eadb0 --- /dev/null +++ b/promo-ui2/src/hooks/web/useTagsView.ts @@ -0,0 +1,63 @@ +import { useTagsViewStoreWithOut } from '@/store/modules/tagsView' +import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router' +import { computed, nextTick, unref } from 'vue' + +export const useTagsView = () => { + const tagsViewStore = useTagsViewStoreWithOut() + + const { replace, currentRoute } = useRouter() + + const selectedTag = computed(() => tagsViewStore.getSelectedTag) + + const closeAll = (callback?: Fn) => { + tagsViewStore.delAllViews() + callback?.() + } + + const closeLeft = (callback?: Fn) => { + tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeRight = (callback?: Fn) => { + tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeOther = (callback?: Fn) => { + tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { + if (view?.meta?.affix) return + tagsViewStore.delView(view || unref(currentRoute)) + + callback?.() + } + + const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { + tagsViewStore.delCachedView() + const { path, query } = view || unref(currentRoute) + await nextTick() + replace({ + path: '/redirect' + path, + query: query + }) + callback?.() + } + + const setTitle = (title: string, path?: string) => { + tagsViewStore.setTitle(title, path) + } + + return { + closeAll, + closeLeft, + closeRight, + closeOther, + closeCurrent, + refreshPage, + setTitle + } +} diff --git a/promo-ui2/src/hooks/web/useTimeAgo.ts b/promo-ui2/src/hooks/web/useTimeAgo.ts new file mode 100644 index 0000000..5675e5e --- /dev/null +++ b/promo-ui2/src/hooks/web/useTimeAgo.ts @@ -0,0 +1,50 @@ +import { useTimeAgo as useTimeAgoCore, UseTimeAgoMessages } from '@vueuse/core' +import { computed, unref } from 'vue' +import { useLocaleStoreWithOut } from '@/store/modules/locale' + +const TIME_AGO_MESSAGE_MAP: { + 'zh-CN': UseTimeAgoMessages + en: UseTimeAgoMessages +} = { + 'zh-CN': { + justNow: '刚刚', + invalid: '无效时间', + past: (n) => (n.match(/\d/) ? `${n}前` : n), + future: (n) => (n.match(/\d/) ? `${n}后` : n), + month: (n, past) => (n === 1 ? (past ? '上个月' : '下个月') : `${n} 个月`), + year: (n, past) => (n === 1 ? (past ? '去年' : '明年') : `${n} 年`), + day: (n, past) => (n === 1 ? (past ? '昨天' : '明天') : `${n} 天`), + week: (n, past) => (n === 1 ? (past ? '上周' : '下周') : `${n} 周`), + hour: (n) => `${n} 小时`, + minute: (n) => `${n} 分钟`, + second: (n) => `${n} 秒` + }, + en: { + justNow: '刚刚', + invalid: 'Invalid Date', + past: (n) => (n.match(/\d/) ? `${n} ago` : n), + future: (n) => (n.match(/\d/) ? `in ${n}` : n), + month: (n, past) => + n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`, + year: (n, past) => + n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`, + day: (n, past) => (n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`), + week: (n, past) => + n === 1 ? (past ? 'last week' : 'next week') : `${n} week${n > 1 ? 's' : ''}`, + hour: (n) => `${n} hour${n > 1 ? 's' : ''}`, + minute: (n) => `${n} minute${n > 1 ? 's' : ''}`, + second: (n) => `${n} second${n > 1 ? 's' : ''}` + } +} + +export const useTimeAgo = (time: Date | number | string) => { + const localeStore = useLocaleStoreWithOut() + + const currentLocale = computed(() => localeStore.getCurrentLocale) + + const timeAgo = useTimeAgoCore(time, { + messages: TIME_AGO_MESSAGE_MAP[unref(currentLocale).lang] + }) + + return timeAgo +} diff --git a/promo-ui2/src/hooks/web/useTitle.ts b/promo-ui2/src/hooks/web/useTitle.ts new file mode 100644 index 0000000..546267d --- /dev/null +++ b/promo-ui2/src/hooks/web/useTitle.ts @@ -0,0 +1,25 @@ +import { watch, ref } from 'vue' +import { isString } from '@/utils/is' +import { useAppStoreWithOut } from '@/store/modules/app' +import { useI18n } from '@/hooks/web/useI18n' + +export const useTitle = (newTitle?: string) => { + const { t } = useI18n() + const appStore = useAppStoreWithOut() + + const title = ref( + newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle + ) + + watch( + title, + (n, o) => { + if (isString(n) && n !== o && document) { + document.title = n + } + }, + { immediate: true } + ) + + return title +} diff --git a/promo-ui2/src/hooks/web/useValidator.ts b/promo-ui2/src/hooks/web/useValidator.ts new file mode 100644 index 0000000..00586bd --- /dev/null +++ b/promo-ui2/src/hooks/web/useValidator.ts @@ -0,0 +1,109 @@ +import { useI18n } from '@/hooks/web/useI18n' +import { FormItemRule } from 'element-plus' + +const { t } = useI18n() + +interface LengthRange { + min: number + max: number + message?: string +} + +export const useValidator = () => { + const required = (message?: string): FormItemRule => { + return { + required: true, + message: message || t('common.required') + } + } + + const lengthRange = (options: LengthRange): FormItemRule => { + const { min, max, message } = options + + return { + min, + max, + message: message || t('common.lengthRange', { min, max }) + } + } + + const notSpace = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (val?.indexOf(' ') !== -1) { + callback(new Error(message || t('common.notSpace'))) + } else { + callback() + } + } + } + } + + const notSpecialCharacters = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { + callback(new Error(message || t('common.notSpecialCharacters'))) + } else { + callback() + } + } + } + } + + const phone = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (!val) return callback() + if (!/^1[3456789]\d{9}$/.test(val)) { + callback(new Error(message || '请输入正确的手机号码')) + } else { + callback() + } + } + } + } + + const email = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (!val) return callback() + if (!/^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(val)) { + callback(new Error(message || '请输入正确的邮箱')) + } else { + callback() + } + } + } + } + + const maxlength = (max: number): FormItemRule => { + return { + max, + message: '长度不能超过' + max + '个字符' + } + } + + const check = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (!val) { + callback(new Error(message || t('common.required'))) + } else { + callback() + } + } + } + } + + return { + required, + lengthRange, + notSpace, + notSpecialCharacters, + phone, + email, + maxlength, + check + } +} diff --git a/promo-ui2/src/hooks/web/useWatermark.ts b/promo-ui2/src/hooks/web/useWatermark.ts new file mode 100644 index 0000000..4a31359 --- /dev/null +++ b/promo-ui2/src/hooks/web/useWatermark.ts @@ -0,0 +1,55 @@ +const domSymbol = Symbol('watermark-dom') + +export function useWatermark(appendEl: HTMLElement | null = document.body) { + let func: Fn = () => {} + const id = domSymbol.toString() + const clear = () => { + const domId = document.getElementById(id) + if (domId) { + const el = appendEl + el && el.removeChild(domId) + } + window.removeEventListener('resize', func) + } + const createWatermark = (str: string) => { + clear() + + const can = document.createElement('canvas') + can.width = 300 + can.height = 240 + + const cans = can.getContext('2d') + if (cans) { + cans.rotate((-20 * Math.PI) / 120) + cans.font = '15px Vedana' + cans.fillStyle = 'rgba(0, 0, 0, 0.15)' + cans.textAlign = 'left' + cans.textBaseline = 'middle' + cans.fillText(str, can.width / 20, can.height) + } + + const div = document.createElement('div') + div.id = id + div.style.pointerEvents = 'none' + div.style.top = '0px' + div.style.left = '0px' + div.style.position = 'absolute' + div.style.zIndex = '100000000' + div.style.width = document.documentElement.clientWidth + 'px' + div.style.height = document.documentElement.clientHeight + 'px' + div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat' + const el = appendEl + el && el.appendChild(div) + return id + } + + function setWatermark(str: string) { + createWatermark(str) + func = () => { + createWatermark(str) + } + window.addEventListener('resize', func) + } + + return { setWatermark, clear } +} diff --git a/promo-ui2/src/layout/Layout.vue b/promo-ui2/src/layout/Layout.vue new file mode 100644 index 0000000..8f570c3 --- /dev/null +++ b/promo-ui2/src/layout/Layout.vue @@ -0,0 +1,74 @@ + + + diff --git a/promo-ui2/src/layout/components/AppView.vue b/promo-ui2/src/layout/components/AppView.vue new file mode 100644 index 0000000..c491f1e --- /dev/null +++ b/promo-ui2/src/layout/components/AppView.vue @@ -0,0 +1,37 @@ + + + diff --git a/promo-ui2/src/layout/components/ToolHeader.vue b/promo-ui2/src/layout/components/ToolHeader.vue new file mode 100644 index 0000000..8ca1c18 --- /dev/null +++ b/promo-ui2/src/layout/components/ToolHeader.vue @@ -0,0 +1,63 @@ + + + diff --git a/promo-ui2/src/layout/components/useRenderLayout.tsx b/promo-ui2/src/layout/components/useRenderLayout.tsx new file mode 100644 index 0000000..9f4b3a7 --- /dev/null +++ b/promo-ui2/src/layout/components/useRenderLayout.tsx @@ -0,0 +1,306 @@ +import { computed } from 'vue' +import { useAppStore } from '@/store/modules/app' +import { Menu } from '@/components/Menu' +import { TabMenu } from '@/components/TabMenu' +import { TagsView } from '@/components/TagsView' +import { Logo } from '@/components/Logo' +import AppView from './AppView.vue' +import ToolHeader from './ToolHeader.vue' +import { ElScrollbar } from 'element-plus' +import { useDesign } from '@/hooks/web/useDesign' + +const { getPrefixCls } = useDesign() + +const prefixCls = getPrefixCls('layout') + +const appStore = useAppStore() + +const pageLoading = computed(() => appStore.getPageLoading) + +// 标签页 +const tagsView = computed(() => appStore.getTagsView) + +// 菜单折叠 +const collapse = computed(() => appStore.getCollapse) + +// logo +const logo = computed(() => appStore.logo) + +// 固定头部 +const fixedHeader = computed(() => appStore.getFixedHeader) + +// 是否是移动端 +const mobile = computed(() => appStore.getMobile) + +// 固定菜单 +const fixedMenu = computed(() => appStore.getFixedMenu) + +export const useRenderLayout = () => { + const renderClassic = () => { + return ( + <> +
+ {logo.value ? ( + + ) : undefined} + +
+
+ +
+ + + {tagsView.value ? ( + + ) : undefined} +
+ + +
+
+ + ) + } + + const renderTopLeft = () => { + return ( + <> +
+ {logo.value ? : undefined} + + +
+
+ +
+ + {tagsView.value ? ( + + ) : undefined} + + + +
+
+ + ) + } + + const renderTop = () => { + return ( + <> +
+ {logo.value ? : undefined} + + +
+
+ + {tagsView.value ? ( + + ) : undefined} + + + +
+ + ) + } + + const renderCutMenu = () => { + return ( + <> +
+ {logo.value ? : undefined} + + +
+
+ +
+ + {tagsView.value ? ( + + ) : undefined} + + + +
+
+ + ) + } + + return { + renderClassic, + renderTopLeft, + renderTop, + renderCutMenu + } +} diff --git a/promo-ui2/src/locales/en.ts b/promo-ui2/src/locales/en.ts new file mode 100644 index 0000000..04e4e42 --- /dev/null +++ b/promo-ui2/src/locales/en.ts @@ -0,0 +1,584 @@ +export default { + common: { + inputText: 'Please input', + selectText: 'Please select', + startTimeText: 'Start time', + endTimeText: 'End time', + login: 'Login', + required: 'This is required', + loginOut: 'Login out', + document: 'Document', + reminder: 'Reminder', + loginOutMessage: 'Exit the system?', + back: 'Back', + ok: 'OK', + cancel: 'Cancel', + reload: 'Reload current', + closeTab: 'Close current', + closeTheLeftTab: 'Close left', + closeTheRightTab: 'Close right', + closeOther: 'Close other', + closeAll: 'Close all', + prevLabel: 'Prev', + nextLabel: 'Next', + skipLabel: 'Jump', + doneLabel: 'End', + menu: 'Menu', + menuDes: 'Menu bar rendered in routed structure', + collapse: 'Collapse', + collapseDes: 'Expand and zoom the menu bar', + tagsView: 'Tags view', + tagsViewDes: 'Used to record routing history', + tool: 'Tool', + toolDes: 'Used to set up custom systems', + query: 'Query', + reset: 'Reset', + shrink: 'Put away', + expand: 'Expand', + delMessage: 'Delete the selected data?', + delWarning: 'Warning', + delOk: 'OK', + delCancel: 'Cancel', + delNoData: 'Please select the data to delete', + delSuccess: 'Deleted successfully', + refresh: 'Refresh', + fullscreen: 'Fullscreen', + size: 'Size', + columnSetting: 'Column setting', + lengthRange: 'The length should be between {min} and {max}', + notSpace: 'Spaces are not allowed', + notSpecialCharacters: 'Special characters are not allowed', + isEqual: 'The two are not equal', + // 列设置 + setting: 'Setting' + }, + lock: { + lockScreen: 'Lock screen', + lock: 'Lock', + lockPassword: 'Lock screen password', + unlock: 'Click to unlock', + backToLogin: 'Back to login', + entrySystem: 'Entry the system', + placeholder: 'Please enter the lock screen password', + message: 'Lock screen password error' + }, + error: { + noPermission: `Sorry, you don't have permission to access this page.`, + pageError: 'Sorry, the page you visited does not exist.', + networkError: 'Sorry, the server reported an error.', + returnToHome: 'Return to home' + }, + setting: { + projectSetting: 'Project setting', + theme: 'Theme', + layout: 'Layout', + systemTheme: 'System theme', + menuTheme: 'Menu theme', + interfaceDisplay: 'Interface display', + breadcrumb: 'Breadcrumb', + breadcrumbIcon: 'Breadcrumb icon', + collapseMenu: 'Collapse menu', + hamburgerIcon: 'Hamburger icon', + screenfullIcon: 'Screenfull icon', + sizeIcon: 'Size icon', + localeIcon: 'Locale icon', + tagsView: 'Tags view', + logo: 'Logo', + greyMode: 'Grey mode', + fixedHeader: 'Fixed header', + headerTheme: 'Header theme', + cutMenu: 'Cut Menu', + copy: 'Copy', + clearAndReset: 'Clear cache and reset', + copySuccess: 'Copy success', + copyFailed: 'Copy failed', + footer: 'Footer', + uniqueOpened: 'Unique opened', + tagsViewIcon: 'Tags view icon', + // 开启动态路由 + dynamicRouter: 'Enable dynamic router', + serverDynamicRouter: 'Server dynamic router', + reExperienced: 'Please exit the login experience again', + fixedMenu: 'Fixed menu' + }, + size: { + default: 'Default', + large: 'Large', + small: 'Small' + }, + login: { + welcome: 'Welcome to the system', + message: 'Backstage management system', + username: 'Username', + password: 'Password', + register: 'Register', + checkPassword: 'Confirm password', + login: 'Sign in', + otherLogin: 'Sign in with', + remember: 'Remember me', + hasUser: 'Existing account? Go to login', + forgetPassword: 'Forget password', + usernamePlaceholder: 'Please input username', + passwordPlaceholder: 'Please input password', + code: 'Verification code', + codePlaceholder: 'Please input verification code', + getCode: 'Get code' + }, + router: { + login: 'Login', + level: 'Multi level menu', + menu: 'Menu', + menu1: 'Menu1', + menu11: 'Menu1-1', + menu111: 'Menu1-1-1', + menu12: 'Menu1-2', + menu2: 'Menu2', + dashboard: 'Dashboard', + analysis: 'Analysis', + workplace: 'Workplace', + guide: 'Guide', + component: 'Component', + icon: 'Icon', + echart: 'Echart', + countTo: 'Count to', + watermark: 'Watermark', + qrcode: 'Qrcode', + highlight: 'Highlight', + infotip: 'Infotip', + form: 'Form', + defaultForm: 'All examples', + search: 'Search', + table: 'Table', + defaultTable: 'Basic example', + editor: 'Editor', + richText: 'Rich text', + jsonEditor: 'JSON Editor', + codeEditor: 'Code Editor', + dialog: 'Dialog', + imageViewer: 'Image viewer', + descriptions: 'Descriptions', + example: 'Example', + exampleDialog: 'Example dialog', + examplePage: 'Example page', + exampleAdd: 'Example page - add', + exampleEdit: 'Example page - edit', + exampleDetail: 'Example page - detail', + errorPage: 'Error page', + authorization: 'Authorization', + user: 'User management', + role: 'Role management', + document: 'Document', + inputPassword: 'InputPassword', + sticky: 'Sticky', + treeTable: 'Tree table', + PicturePreview: 'Table Image Preview', + department: 'Department management', + menuManagement: 'Menu management', + // 权限测试页面 + permission: 'Permission test page', + function: 'Function', + multipleTabs: 'Multiple tabs', + details: 'Details', + iconPicker: 'Icon picker', + request: 'Request', + waterfall: 'Waterfall', + imageCropping: 'Image cropping', + videoPlayer: 'Video player', + // 表格视频预览 + tableVideoPreview: 'Table video preview', + cardTable: 'Card table', + personalCenter: 'Personal center', + personal: 'Personal', + avatars: 'Avatars', + iAgree: 'I agree', + tree: 'Tree' + }, + permission: { + hasPermission: 'Please set the operation permission value' + }, + analysis: { + newUser: 'New user', + unreadInformation: 'Unread information', + transactionAmount: 'Transaction amount', + totalShopping: 'Total Shopping', + monthlySales: 'Monthly sales', + userAccessSource: 'User access source', + january: 'January', + february: 'February', + march: 'March', + april: 'April', + may: 'May', + june: 'June', + july: 'July', + august: 'August', + september: 'September', + october: 'October', + november: 'November', + december: 'December', + estimate: 'Estimate', + actual: 'Actual', + directAccess: 'Airect access', + mailMarketing: 'Mail marketing', + allianceAdvertising: 'Alliance advertising', + videoAdvertising: 'Video advertising', + searchEngines: 'Search engines', + weeklyUserActivity: 'Weekly user activity', + activeQuantity: 'Active quantity', + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday', + sunday: 'Sunday' + }, + workplace: { + goodMorning: 'Good morning', + happyDay: 'Wish you happy every day!', + toady: `It's sunny today`, + project: 'Project', + access: 'Project access', + toDo: 'To do', + introduction: 'A serious introduction', + more: 'More', + shortcutOperation: 'Shortcut operation', + operation: 'Operation', + index: 'Index', + personal: 'Personal', + team: 'Team', + quote: 'Quote', + contribution: 'Contribution', + hot: 'Hot', + yield: 'Yield', + dynamic: 'Dynamic', + push: 'push', + pushCode: 'Archer push code to Github', + follow: 'Follow' + }, + formDemo: { + input: 'Input', + inputNumber: 'InputNumber', + default: 'Default', + icon: 'Icon', + mixed: 'Mixed', + password: 'Password', + textarea: 'Textarea', + remoteSearch: 'Remote search', + slot: 'Slot', + position: 'Position', + autocomplete: 'Autocomplete', + select: 'Select', + optionSlot: 'Option Slot', + selectGroup: 'Select Group', + selectV2: 'SelectV2', + cascader: 'Cascader', + switch: 'Switch', + rate: 'Rate', + colorPicker: 'Color Picker', + transfer: 'Transfer', + render: 'Render', + radio: 'Radio', + radioGroup: 'Radio Group', + button: 'Button', + checkbox: 'Checkbox', + checkboxButton: 'Checkbox Button', + checkboxGroup: 'Checkbox Group', + slider: 'Slider', + datePicker: 'Date Picker', + shortcuts: 'Shortcuts', + today: 'Today', + yesterday: 'Yesterday', + aWeekAgo: 'A week ago', + week: 'Week', + year: 'Year', + month: 'Month', + dates: 'Dates', + daterange: 'Date Range', + monthrange: 'Month Range', + dateTimePicker: 'DateTimePicker', + dateTimerange: 'Datetime Range', + timePicker: 'Time Picker', + timeSelect: 'Time Select', + inputPassword: 'input Password', + passwordStrength: 'Password Strength', + defaultForm: 'All examples', + formDes: + 'The secondary encapsulation of form components based on ElementPlus realizes data-driven and supports all Form parameters', + example: 'example', + operate: 'operate', + change: 'Change', + restore: 'Restore', + disabled: 'Disabled', + disablement: 'Disablement', + delete: 'Delete', + add: 'Add', + setValue: 'Set value', + resetValue: 'Reset value', + set: 'Set', + subitem: 'Subitem', + formValidation: 'Form validation', + verifyReset: 'Verify reset', + // 富文本编辑器 + richText: 'Rich text', + jsonEditor: 'JSON Editor', + form: 'Form', + // 远程加载 + remoteLoading: 'Remote loading', + // 聚焦 + focus: 'Focus', + treeSelect: 'Tree Select', + showCheckbox: 'Show Checkbox', + selectAnyLevel: 'Select Any Level', + multiple: 'Multiple', + filterable: 'Filterable', + // 自定义节点内容 + customContent: 'Custom content', + // 懒加载 + lazyLoad: 'Lazy load', + upload: 'Upload', + // 用户头像 + userAvatar: 'User avatar', + iconPicker: 'Icon picker', + iAgree: 'I agree' + }, + guideDemo: { + guide: 'Guide', + start: 'Start', + message: + 'The guide page is very useful for some people who enter the project for the first time. You can briefly introduce the functions of the project. The boot page is based on driver.js' + }, + iconDemo: { + icon: 'Icon', + localIcon: 'Local Icon', + iconify: 'Iconify component', + recommendedUse: 'Recommended use', + recommendeDes: + 'Iconify component basically contains all icons. You can query any icon you want. And packaging will only package the icons used.', + accessAddress: 'Access address' + }, + echartDemo: { + echart: 'Echart', + echartDes: + 'Based on the secondary packaging components of eckarts, the width is adaptive. The corresponding chart can be displayed by passing in the options and height attributes.' + }, + countToDemo: { + countTo: 'CountTo', + countToDes: + 'The transformation is based on vue-count-to and supports all vue-count-to parameters.', + suffix: 'Suffix', + prefix: 'Prefix', + separator: 'Separator', + duration: 'Duration', + endVal: 'End val', + startVal: 'Start val', + start: 'Start', + pause: 'Pause', + resume: 'Resume' + }, + watermarkDemo: { + watermark: 'Watermark', + createdWatermark: 'Created watermark', + clearWatermark: 'Clear watermark', + resetWatermark: 'Reset watermark' + }, + qrcodeDemo: { + qrcode: 'Qrcode', + qrcodeDes: 'Secondary packaging based on qrcode', + basicUsage: 'Basic usage', + imgTag: 'Img tag', + style: 'Style config', + click: 'Click event', + asynchronousContent: 'Asynchronous content', + invalid: 'Invalid', + logoConfig: 'Logo config', + logoStyle: 'Logo style', + size: 'size config' + }, + treeDemo: { + treeTitle: 'Tree control (right-click node to customize menu options)', + message: + 'The tree component is based on the secondary packaging of the tree component of ElementPlus' + }, + highlightDemo: { + highlight: 'Highlight', + message: 'The best time to plant a tree is ten years ago, followed by now.', + keys1: 'ten years ago', + keys2: 'now' + }, + infotipDemo: { + infotip: 'Infotip', + infotipDes: 'Secondary packaging of components based on Highlight', + title: 'matters needing attention' + }, + levelDemo: { + menu: 'Multi level menu cache' + }, + searchDemo: { + search: 'Search', + searchDes: + 'Based on the secondary encapsulation of form components, the functions of query and reset are realized', + operate: 'operate', + change: 'Change', + grid: 'grid', + button: 'Button', + restore: 'Restore', + inline: 'inline', + bottom: 'Bottom', + position: 'position', + left: 'left', + center: 'center', + right: 'right', + dynamicOptions: 'Dynamic options', + // 删除单选框 + deleteRadio: 'Delete radio', + // 还原单选框 + restoreRadio: 'Restore radio', + loading: 'Loading', + reset: 'Reset' + }, + stickyDemo: { + sticky: 'Sticky' + }, + tableDemo: { + table: 'Table', + tableDes: 'Secondary packaging of Table components based on ElementPlus', + index: 'Index', + title: 'Title', + author: 'Author', + displayTime: 'Display time', + importance: 'Importance', + pageviews: 'Pageviews', + action: 'Action', + important: 'Important', + good: 'Good', + commonly: 'Commonly', + operate: 'operate', + example: 'example', + show: 'Show', + hidden: 'Hidden', + pagination: 'pagination', + reserveIndex: 'Reserve index', + restoreIndex: 'Restore index', + showSelections: 'Show selections', + hiddenSelections: 'Restore selections', + showExpandedRows: 'Show expanded rows', + hiddenExpandedRows: 'Hidden expanded rows', + changeTitle: 'Change title', + header: 'Header', + selectAllNone: 'Select all / none', + delOrAddAction: 'Delete or add action', + showOrHiddenStripe: 'Show or hidden stripe', + showOrHiddenBorder: 'Show or hidden border', + fixedHeaderOrAuto: 'Fixed header or auto', + getSelections: 'Get selections', + preview: 'Preview', + showOrHiddenSortable: 'Show or hidden sortable', + videoPreview: 'Video preview', + cardTable: 'Card table' + }, + richText: { + richText: 'Rich text', + richTextDes: 'Secondary packaging based on wangeditor', + jsonEditor: 'JSON Editor', + jsonEditorDes: 'Secondary packaging based on vue-json-pretty', + codeEditor: 'Code Editor', + codeEditorDes: 'Secondary packaging based on monaco-editor' + }, + dialogDemo: { + dialog: 'Dialog', + resizeDialog: 'Resize dialog', + dialogDes: 'Secondary packaging of Dialog components based on ElementPlus', + open: 'Open', + close: 'Close', + combineWithForm: 'Combine with form', + submit: 'Submit' + }, + imageViewerDemo: { + open: 'Open', + imageViewer: 'Image viewer', + imageViewerDes: 'Secondary packaging of ImageViewer components based on ElementPlus' + }, + descriptionsDemo: { + descriptions: 'Descriptions', + descriptionsDes: 'Secondary packaging of Descriptions components based on ElementPlus', + username: 'Username', + nickName: 'NickName', + phone: 'Phone', + email: 'Email', + addr: 'Address', + form: 'Combined with Form component' + }, + exampleDemo: { + title: 'Title', + add: 'Add', + del: 'Delete', + edit: 'Edit', + author: 'Author', + displayTime: 'Display time', + importance: 'Importance', + pageviews: 'Pageviews', + important: 'Important', + content: 'Content', + save: 'Save', + detail: 'Detail' + }, + userDemo: { + title: 'User management', + message: + 'Because it is simulated data, only two accounts with different permissions are provided, which can be modified and combined by developers according to the actual situation.', + index: 'Index', + action: 'Action', + username: 'Username', + password: 'Password', + role: 'Role', + remark: 'Remark', + remarkMessage1: 'Back end control routing permission', + remarkMessage2: 'Front end control routing permission', + // 部门列表 + departmentList: 'Department list', + // 搜索部门 + searchDepartment: 'Search department', + account: 'Account', + email: 'Email', + createTime: 'Create time', + // 所属部门 + department: 'Department', + departmentName: 'Department name', + status: 'Status', + enable: 'Enable', + disable: 'Disable', + superiorDepartment: 'Superior department' + }, + menu: { + menuName: 'Menu name', + icon: 'Icon', + // 权限 + permission: 'Permission', + component: 'Component', + path: 'Path', + status: 'Status', + hidden: 'Hidden', + alwaysShow: 'Always show', + noCache: 'No cache', + breadcrumb: 'Breadcrumb', + affix: 'Affix', + noTagsView: 'No tags view', + activeMenu: 'Active menu', + canTo: 'Can to', + name: 'Name' + }, + role: { + roleName: 'Role name', + role: 'Role', + // 菜单分配 + menu: 'Menu allocation' + }, + inputPasswordDemo: { + title: 'InputPassword', + inputPasswordDes: 'Secondary packaging of Input components based on ElementPlus' + }, + avatarsDemo: { + title: + 'Avatar component for avatar list, secondary packaging based on element plus Avatar component' + } +} diff --git a/promo-ui2/src/locales/zh-CN.ts b/promo-ui2/src/locales/zh-CN.ts new file mode 100644 index 0000000..f1f51a8 --- /dev/null +++ b/promo-ui2/src/locales/zh-CN.ts @@ -0,0 +1,577 @@ +export default { + app: { + turnover: '流水明细', + subFlow: '下级流水', + mymoney: '我的统计' + }, + common: { + inputText: '请输入', + selectText: '请选择', + startTimeText: '开始时间', + endTimeText: '结束时间', + login: '登录', + required: '该项为必填项', + loginOut: '退出系统', + document: '项目文档', + reminder: '温馨提示', + loginOutMessage: '是否退出本系统?', + back: '返回', + ok: '确定', + cancel: '取消', + reload: '重新加载', + closeTab: '关闭标签页', + closeTheLeftTab: '关闭左侧标签页', + closeTheRightTab: '关闭右侧标签页', + closeOther: '关闭其它标签页', + closeAll: '关闭全部标签页', + prevLabel: '上一步', + nextLabel: '下一步', + skipLabel: '跳过', + doneLabel: '结束', + menu: '菜单', + menuDes: '以路由的结构渲染的菜单栏', + collapse: '展开缩收', + collapseDes: '展开和缩放菜单栏', + tagsView: '标签页', + tagsViewDes: '用于记录路由历史记录', + tool: '工具', + toolDes: '用于设置定制系统', + query: '查询', + reset: '重置', + shrink: '收起', + expand: '展开', + delMessage: '是否删除所选中数据?', + delWarning: '提示', + delOk: '确定', + delCancel: '取消', + delNoData: '请选择需要删除的数据', + delSuccess: '删除成功', + refresh: '刷新', + fullscreen: '全屏', + size: '尺寸', + columnSetting: '列设置', + lengthRange: '长度在 {min} 到 {max} 个字符', + notSpace: '不能包含空格', + notSpecialCharacters: '不能包含特殊字符', + isEqual: '两次输入不一致', + setting: '设置' + }, + lock: { + lockScreen: '锁定屏幕', + lock: '锁定', + lockPassword: '锁屏密码', + unlock: '点击解锁', + backToLogin: '返回登录', + entrySystem: '进入系统', + placeholder: '请输入锁屏密码', + message: '锁屏密码错误' + }, + error: { + noPermission: `抱歉,您无权访问此页面。`, + pageError: '抱歉,您访问的页面不存在。', + networkError: '抱歉,服务器报告错误。', + returnToHome: '返回首页' + }, + setting: { + projectSetting: '项目配置', + theme: '主题', + layout: '布局', + systemTheme: '系统主题', + menuTheme: '菜单主题', + interfaceDisplay: '界面显示', + breadcrumb: '面包屑', + breadcrumbIcon: '面包屑图标', + collapseMenu: '折叠菜单', + hamburgerIcon: '折叠图标', + screenfullIcon: '全屏图标', + sizeIcon: '尺寸图标', + localeIcon: '多语言图标', + tagsView: '标签页', + logo: 'Logo', + greyMode: '灰色模式', + fixedHeader: '固定头部', + headerTheme: '头部主题', + cutMenu: '切割菜单', + copy: '拷贝', + clearAndReset: '清除缓存并且重置', + copySuccess: '拷贝成功', + copyFailed: '拷贝失败', + footer: '页脚', + uniqueOpened: '菜单手风琴', + tagsViewIcon: '标签页图标', + dynamicRouter: '开启动态路由', + serverDynamicRouter: '服务端动态路由', + reExperienced: '请重新退出登录体验', + fixedMenu: '固定菜单' + }, + size: { + default: '默认', + large: '大', + small: '小' + }, + login: { + welcome: '欢迎使用本系统', + message: '开箱管理系统', + username: '手机号', + password: '密码', + register: '注册', + checkPassword: '确认密码', + login: '登录', + otherLogin: '其它登录方式', + remember: '记住我', + hasUser: '已有账号?去登录', + forgetPassword: '忘记密码', + usernamePlaceholder: '请输入用户名', + passwordPlaceholder: '请输入密码', + code: '验证码', + codePlaceholder: '请输入验证码', + getCode: '获取验证码' + }, + router: { + login: '登录', + level: '多级菜单', + menu: '菜单', + menu1: '菜单1', + menu11: '菜单1-1', + menu111: '菜单1-1-1', + menu12: '菜单1-2', + menu2: '菜单2', + dashboard: '首页', + analysis: '数据面板', + workplace: '工作台', + guide: '引导', + component: '组件', + icon: '图标', + echart: '图表', + countTo: '数字动画', + watermark: '水印', + qrcode: '二维码', + highlight: '高亮', + infotip: '信息提示', + form: '表单', + defaultForm: '全部示例', + search: '查询', + table: '表格', + defaultTable: '基础示例', + editor: '编辑器', + richText: '富文本', + jsonEditor: 'JSON编辑器', + codeEditor: '代码编辑器', + dialog: '弹窗', + imageViewer: '图片预览', + descriptions: '描述', + example: '综合示例', + exampleDialog: '综合示例 - 弹窗', + examplePage: '综合示例 - 页面', + exampleAdd: '综合示例 - 新增', + exampleEdit: '综合示例 - 编辑', + exampleDetail: '综合示例 - 详情', + errorPage: '错误页面', + authorization: '权限管理', + user: '用户管理', + role: '角色管理', + document: '文档', + inputPassword: '密码输入框', + sticky: '黏性', + treeTable: '树形表格', + PicturePreview: '表格图片预览', + department: '部门管理', + menuManagement: '菜单管理', + permission: '权限测试页', + function: '功能', + multipleTabs: '多开标签页', + details: '详情页', + iconPicker: '图标选择器', + request: '请求', + waterfall: '瀑布流', + imageCropping: '图片裁剪', + videoPlayer: '视频播放器', + tableVideoPreview: '表格视频预览', + cardTable: '卡片表格', + personalCenter: '个人中心', + personal: '个人', + avatars: '头像列表', + iAgree: '我同意', + tree: 'Tree 树形控件' + }, + permission: { + hasPermission: '请设置操作权限值' + }, + analysis: { + newUser: '新增用户', + unreadInformation: '未读消息', + transactionAmount: '成交金额', + totalShopping: '购物总量', + monthlySales: '每月销售额', + userAccessSource: '用户访问来源', + january: '一月', + february: '二月', + march: '三月', + april: '四月', + may: '五月', + june: '六月', + july: '七月', + august: '八月', + september: '九月', + october: '十月', + november: '十一月', + december: '十二月', + estimate: '预计', + actual: '实际', + directAccess: '直接访问', + mailMarketing: '邮件营销', + allianceAdvertising: '联盟广告', + videoAdvertising: '视频广告', + searchEngines: '搜索引擎', + weeklyUserActivity: '每周用户活跃量', + activeQuantity: '活跃量', + monday: '周一', + tuesday: '周二', + wednesday: '周三', + thursday: '周四', + friday: '周五', + saturday: '周六', + sunday: '周日' + }, + workplace: { + goodMorning: '早安', + happyDay: '祝你开心每一天!', + toady: '今日晴', + project: '项目数', + access: '项目访问', + toDo: '待办', + introduction: '一个正经的简介', + more: '更多', + shortcutOperation: '快捷操作', + operation: '操作', + index: '指数', + personal: '个人', + team: '团队', + quote: '引用', + contribution: '贡献', + hot: '热度', + yield: '产量', + dynamic: '动态', + push: '推送', + pushCode: 'Archer 推送 代码到 Github', + follow: '关注' + }, + formDemo: { + input: '输入框', + inputNumber: '数字输入框', + default: '默认', + icon: '图标', + mixed: '复合型', + password: '密码框', + textarea: '多行文本', + remoteSearch: '远程搜索', + slot: '插槽', + position: '位置', + autocomplete: '自动补全', + select: '选择器', + optionSlot: '选项插槽', + selectGroup: '选项分组', + selectV2: '虚拟列表选择器', + cascader: '级联选择器', + switch: '开关', + rate: '评分', + colorPicker: '颜色选择器', + transfer: '穿梭框', + render: '渲染器', + radio: '单选框', + radioGroup: '单选框组', + button: '按钮', + checkbox: '多选框', + checkboxButton: '多选框按钮', + checkboxGroup: '多选框组', + slider: '滑块', + datePicker: '日期选择器', + shortcuts: '快捷选项', + today: '今天', + yesterday: '昨天', + aWeekAgo: '一周前', + week: '周', + year: '年', + month: '月', + dates: '日期', + daterange: '日期范围', + monthrange: '月份范围', + dateTimePicker: '日期时间选择器', + dateTimerange: '日期时间范围', + timePicker: '时间选择器', + timeSelect: '时间选择', + inputPassword: '密码输入框', + passwordStrength: '密码强度', + defaultForm: '全部示例', + formDes: '基于 ElementPlus 的 Form 组件二次封装,实现数据驱动,支持所有 Form 参数', + example: '示例', + operate: '操作', + change: '更改', + restore: '还原', + disabled: '禁用', + disablement: '解除禁用', + delete: '删除', + add: '添加', + setValue: '设置值', + resetValue: '重置值', + set: '设置', + subitem: '子项', + formValidation: '表单验证', + verifyReset: '验证重置', + // 富文本编辑器 + richText: '富文本编辑器', + // JSON编辑器 + jsonEditor: 'JSON编辑器', + form: '表单', + // 远程加载 + remoteLoading: '远程加载', + // 聚焦 + focus: '聚焦', + treeSelect: '树形选择器', + showCheckbox: '显示复选框', + selectAnyLevel: '选择任意级别', + multiple: '多选', + filterable: '可筛选', + customContent: '自定义内容', + lazyLoad: '懒加载', + upload: '上传', + userAvatar: '用户头像', + iconPicker: '图标选择器', + iAgree: '我同意' + }, + guideDemo: { + guide: '引导页', + start: '开始', + message: + '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。引导页基于 driver.js' + }, + iconDemo: { + icon: '图标', + localIcon: '本地图标', + iconify: 'Iconify组件', + recommendedUse: '推荐使用', + recommendeDes: + 'Iconify组件基本包含所有的图标,你可以查询到你想要的任何图标。并且打包只会打包所用到的图标。', + accessAddress: '访问地址' + }, + echartDemo: { + echart: '图表', + echartDes: + '基于 echarts 二次封装组件,自适应宽度,只需传入 options 与 height 属性即可展示对应的图表。' + }, + countToDemo: { + countTo: '数字动画', + countToDes: '基于 vue-count-to 进行改造,支持所有 vue-count-to 参数。', + suffix: '后缀', + prefix: '前缀', + separator: '分割符号', + duration: '持续时间', + endVal: '结束值', + startVal: '开始值', + start: '开始', + pause: '暂停', + resume: '继续' + }, + watermarkDemo: { + watermark: '水印', + createdWatermark: '创建水印', + clearWatermark: '清除水印', + resetWatermark: '重置水印' + }, + qrcodeDemo: { + qrcode: '二维码', + qrcodeDes: '基于 qrcode 二次封装', + basicUsage: '基础用法', + imgTag: 'img标签', + style: '样式配置', + click: '点击事件', + asynchronousContent: '异步内容', + invalid: '失效', + logoConfig: 'logo配置', + logoStyle: 'logo样式', + size: '大小配置' + }, + treeDemo: { + treeTitle: '树形控件(节点右键可自定义菜单选项)', + message: '基于 ElementPlus 的 Tree 组件二次封装' + }, + highlightDemo: { + highlight: '高亮', + message: '种一棵树最好的时间是十年前,其次就是现在。', + keys1: '十年前', + keys2: '现在' + }, + infotipDemo: { + infotip: '信息提示', + infotipDes: '基于 Highlight 组件二次封装', + title: '注意事项' + }, + levelDemo: { + menu: '多级菜单缓存' + }, + searchDemo: { + search: '查询', + searchDes: '基于 Form 组件二次封装,实现查询、重置功能', + operate: '操作', + change: '更改', + grid: '栅格', + button: '按钮', + restore: '还原', + inline: '内联', + bottom: '底部', + position: '位置', + left: '左', + center: '中', + right: '右', + dynamicOptions: '动态选项', + // 删除单选框 + deleteRadio: '删除单选框', + // 还原单选框 + restoreRadio: '还原单选框', + loading: '加载中', + reset: '重置' + }, + stickyDemo: { + sticky: '黏性' + }, + tableDemo: { + table: '表格', + tableDes: '基于 ElementPlus 的 Table 组件二次封装', + index: '序号', + title: '标题', + author: '作者', + displayTime: '创建时间', + importance: '重要性', + pageviews: '阅读数', + action: '操作', + important: '重要', + good: '良好', + commonly: '一般', + operate: '操作', + example: '示例', + show: '显示', + hidden: '隐藏', + pagination: '分页', + reserveIndex: '叠加序号', + restoreIndex: '还原序号', + showSelections: '显示多选', + hiddenSelections: '隐藏多选', + showExpandedRows: '显示展开行', + hiddenExpandedRows: '隐藏展开行', + changeTitle: '修改标题', + header: '头部', + selectAllNone: '全选/全不选', + delOrAddAction: '删除/添加操作列', + showOrHiddenStripe: '显示/隐藏斑马纹', + showOrHiddenBorder: '显示/隐藏边框', + fixedHeaderOrAuto: '固定头部/自动', + getSelections: '获取多选数据', + preview: '封面', + showOrHiddenSortable: '显示/隐藏排序', + videoPreview: '视频预览', + cardTable: '卡片表格' + }, + richText: { + richText: '富文本', + richTextDes: '基于 wangeditor 二次封装', + jsonEditor: 'JSON编辑器', + jsonEditorDes: '基于 vue-json-pretty 二次封装', + codeEditor: '代码编辑器', + codeEditorDes: '基于 monaco-editor 二次封装' + }, + dialogDemo: { + dialog: '弹窗', + resizeDialog: '可自定义调节弹窗大小的弹窗', + dialogDes: '基于 ElementPlus 的 Dialog 组件二次封装', + open: '打开', + close: '关闭', + combineWithForm: '与表单结合', + submit: '提交' + }, + imageViewerDemo: { + open: '打开', + imageViewer: '图片预览', + imageViewerDes: '基于 ElementPlus 的 ImageViewer 组件二次封装' + }, + descriptionsDemo: { + descriptions: '描述', + descriptionsDes: '基于 ElementPlus 的 Descriptions 组件二次封装', + username: '用户名', + nickName: '昵称', + phone: '联系电话', + email: '邮箱', + addr: '地址', + form: '与 Form 组件组合' + }, + exampleDemo: { + title: '标题', + add: '新增', + del: '删除', + edit: '编辑', + author: '作者', + displayTime: '创建时间', + importance: '重要性', + pageviews: '阅读数', + important: '重要', + content: '内容', + save: '保存', + detail: '详情' + }, + userDemo: { + title: '用户管理', + message: '由于是模拟数据,所以只提供了两种不同权限的帐号,开发者可根据实际情况自行改造结合。', + index: '序号', + action: '操作', + username: '用户名', + password: '密码', + role: '角色', + remark: '备注', + remarkMessage1: '后端控制路由权限', + remarkMessage2: '前端控制路由权限', + // 部门列表 + departmentList: '部门列表', + searchDepartment: '搜索部门', + account: '账号', + email: '邮箱', + createTime: '创建时间', + // 所属部门 + department: '所属部门', + departmentName: '部门名称', + status: '状态', + // 启用 + enable: '启用', + // 禁用 + disable: '禁用', + // 上级部门 + superiorDepartment: '上级部门' + }, + menu: { + menuName: '菜单名称', + icon: '图标', + permission: '按钮权限', + component: '组件', + path: '路径', + status: '状态', + hidden: '是否隐藏', + alwaysShow: '是否一直显示', + noCache: '是否清除缓存', + breadcrumb: '是否显示面包屑', + affix: '是否固定在标签页', + noTagsView: '是否隐藏标签页', + activeMenu: '高亮菜单', + canTo: '是否可跳转', + name: '组件名称' + }, + role: { + roleName: '角色名称', + role: '角色', + menu: '菜单分配' + }, + inputPasswordDemo: { + title: '密码输入框', + inputPasswordDes: '基于 ElementPlus 的 Input 组件二次封装' + }, + avatarsDemo: { + title: '头像列表组件,基于element-plus的Avatar组件二次封装' + } +} diff --git a/promo-ui2/src/main.ts b/promo-ui2/src/main.ts new file mode 100644 index 0000000..d0f08af --- /dev/null +++ b/promo-ui2/src/main.ts @@ -0,0 +1,58 @@ +import 'vue/jsx' + +// 引入windi css +import '@/plugins/unocss' + +// 导入全局的svg图标 +import '@/plugins/svgIcon' + +// 初始化多语言 +import { setupI18n } from '@/plugins/vueI18n' + +// 引入状态管理 +import { setupStore } from '@/store' + +// 全局组件 +import { setupGlobCom } from '@/components' + +// 引入element-plus +import { setupElementPlus } from '@/plugins/elementPlus' + +// 引入全局样式 +import '@/styles/index.less' + +// 引入动画 +import '@/plugins/animate.css' + +// 路由 +import { setupRouter } from './router' + +// 权限 +import { setupPermission } from './directives' + +import { createApp } from 'vue' + +import App from './App.vue' + +import './permission' + +// 创建实例 +const setupAll = async () => { + const app = createApp(App) + + await setupI18n(app) + + setupStore(app) + + setupGlobCom(app) + + setupElementPlus(app) + + setupRouter(app) + + setupPermission(app) + + app.mount('#app') +} + +setupAll() diff --git a/promo-ui2/src/permission.ts b/promo-ui2/src/permission.ts new file mode 100644 index 0000000..2eebbc6 --- /dev/null +++ b/promo-ui2/src/permission.ts @@ -0,0 +1,121 @@ +import router from './router' +import { useAppStoreWithOut } from '@/store/modules/app' +import type { RouteRecordRaw } from 'vue-router' +import { useTitle } from '@/hooks/web/useTitle' +import { useNProgress } from '@/hooks/web/useNProgress' +import { usePermissionStoreWithOut } from '@/store/modules/permission' +import { usePageLoading } from '@/hooks/web/usePageLoading' +import { NO_REDIRECT_WHITE_LIST } from '@/constants' +import { useUserStoreWithOut } from '@/store/modules/user' +import { getMyMenus } from '@/api/permission' +import { isRealRoute } from '@/utils/routerHelper' + +const { start, done } = useNProgress() + +const { loadStart, loadDone } = usePageLoading() + +// 默认落地页 = 超管分配的第一个菜单页;只认「真能解析出路由」的路径,避免菜单表里失效路径导致 404 +const firstMenuPath = (paths?: unknown) => + ((paths as string[]) || []).find((p) => { + if (typeof p !== 'string' || !p.includes('/index')) return false + const matched = router.resolve(p).matched + return matched.length > 0 && !matched.some((m) => m.name === '404Page') + }) as string | undefined + +router.beforeEach(async (to, from, next) => { + start() + loadStart() + const permissionStore = usePermissionStoreWithOut() + const appStore = useAppStoreWithOut() + const userStore = useUserStoreWithOut() + console.log( + '[permission] to.path=', + to.path, + 'to.matched=', + to.matched.length, + 'isAddRouters=', + permissionStore.getIsAddRouters, + 'hasUser=', + !!userStore.getUserInfo + ) + if (userStore.getUserInfo) { + if (to.path === '/login') { + next({ path: firstMenuPath(userStore.getRoleRouters) || '/' }) + } else { + if (permissionStore.getIsAddRouters) { + next() + return + } + + // 每次路由初始化时从后端实时获取最新菜单权限 + let roleRouters: string[] = [] + try { + const res = (await getMyMenus()) as any + if (res && res.data && res.data.length > 0) { + roleRouters = res.data + userStore.setRoleRouters(roleRouters) + } + } catch (e) { + // 接口失败时回退使用缓存 + roleRouters = (userStore.getRoleRouters as string[]) || [] + } + + // 是否使用动态路由 + if (appStore.getDynamicRouter) { + appStore.serverDynamicRouter + ? await permissionStore.generateRoutes( + 'server', + roleRouters as unknown as AppCustomRouteRecordRaw[] + ) + : await permissionStore.generateRoutes('frontEnd', roleRouters as string[]) + } else { + await permissionStore.generateRoutes('static') + } + + permissionStore.getAddRouters.forEach((route) => { + router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表 + }) + console.log( + '[permission] roleRouters=', + roleRouters, + 'addRouters=', + permissionStore.getAddRouters.map((r: any) => r.path) + ) + const redirectPath = from.query.redirect || to.path + const redirect = decodeURIComponent(redirectPath as string) + // 过滤掉兜底 404 路由 + const realRoutes = permissionStore.getAddRouters.filter( + (r) => r.path !== '/:path(.*)*' && r.name !== '404Page' + ) + const firstRoute = realRoutes[0] + const firstPath = firstMenuPath(roleRouters) || firstRoute?.path || '/' + // 只有真能解析出路由的 redirect 才作数,否则落到第一个可用菜单页。 + // 旧写法会把 redirect(例如强制登出时留在 URL 上的 /404)原样 next(),直接撞 404 兜底。 + const finalPath = isRealRoute(router, redirect) ? redirect : firstPath + const nextData = to.path === finalPath ? { ...to, replace: true } : { path: finalPath } + console.log( + '[permission] redirect=', + redirect, + 'firstPath=', + firstPath, + 'finalPath=', + finalPath + ) + permissionStore.setIsAddRouters(true) + next(nextData) + } + } else { + if (NO_REDIRECT_WHITE_LIST.indexOf(to.path) !== -1) { + next() + } else { + // 未登录:'/404' 这种本身就不该存在的路径不要写进 redirect,否则登录后又被跳回 404 + next(to.path === '/404' || to.name === '404Page' ? '/login' : `/login?redirect=${to.path}`) + } + } +}) + +router.afterEach((to) => { + useTitle(to?.meta?.title as string) + done() // 结束Progress + loadDone() +}) diff --git a/promo-ui2/src/plugins/animate.css/index.ts b/promo-ui2/src/plugins/animate.css/index.ts new file mode 100644 index 0000000..3e93451 --- /dev/null +++ b/promo-ui2/src/plugins/animate.css/index.ts @@ -0,0 +1 @@ +import 'animate.css' diff --git a/promo-ui2/src/plugins/echarts/index.ts b/promo-ui2/src/plugins/echarts/index.ts new file mode 100644 index 0000000..34f756f --- /dev/null +++ b/promo-ui2/src/plugins/echarts/index.ts @@ -0,0 +1,41 @@ +import * as echarts from 'echarts/core' + +import { + BarChart, + LineChart, + PieChart, + MapChart, + PictorialBarChart, + RadarChart +} from 'echarts/charts' + +import { + TitleComponent, + TooltipComponent, + GridComponent, + PolarComponent, + AriaComponent, + ParallelComponent, + LegendComponent +} from 'echarts/components' + +import { CanvasRenderer } from 'echarts/renderers' + +echarts.use([ + LegendComponent, + TitleComponent, + TooltipComponent, + GridComponent, + PolarComponent, + AriaComponent, + ParallelComponent, + BarChart, + LineChart, + PieChart, + MapChart, + CanvasRenderer, + PictorialBarChart, + RadarChart +]) + +export default echarts diff --git a/promo-ui2/src/plugins/elementPlus/index.ts b/promo-ui2/src/plugins/elementPlus/index.ts new file mode 100644 index 0000000..29a7840 --- /dev/null +++ b/promo-ui2/src/plugins/elementPlus/index.ts @@ -0,0 +1,24 @@ +import type { App } from 'vue' + +// 需要全局引入一些组件,如ElScrollbar,不然一些下拉项样式有问题 +import { ElLoading, ElScrollbar } from 'element-plus' + +const plugins = [ElLoading] + +const components = [ElScrollbar] + +export const setupElementPlus = (app: App) => { + plugins.forEach((plugin) => { + app.use(plugin) + }) + + // 为了开发环境启动更快,一次性引入所有样式 + if (import.meta.env.VITE_USE_ALL_ELEMENT_PLUS_STYLE === 'true') { + import('element-plus/dist/index.css') + return + } + + components.forEach((component) => { + app.component(component.name!, component) + }) +} diff --git a/promo-ui2/src/plugins/svgIcon/index.ts b/promo-ui2/src/plugins/svgIcon/index.ts new file mode 100644 index 0000000..1b11e2d --- /dev/null +++ b/promo-ui2/src/plugins/svgIcon/index.ts @@ -0,0 +1 @@ +import 'virtual:svg-icons-register' diff --git a/promo-ui2/src/plugins/unocss/index.ts b/promo-ui2/src/plugins/unocss/index.ts new file mode 100644 index 0000000..d366b5a --- /dev/null +++ b/promo-ui2/src/plugins/unocss/index.ts @@ -0,0 +1 @@ +import 'virtual:uno.css' diff --git a/promo-ui2/src/plugins/vueI18n/helper.ts b/promo-ui2/src/plugins/vueI18n/helper.ts new file mode 100644 index 0000000..da6bc8c --- /dev/null +++ b/promo-ui2/src/plugins/vueI18n/helper.ts @@ -0,0 +1,3 @@ +export const setHtmlPageLang = (locale: LocaleType) => { + document.querySelector('html')?.setAttribute('lang', locale) +} diff --git a/promo-ui2/src/plugins/vueI18n/index.ts b/promo-ui2/src/plugins/vueI18n/index.ts new file mode 100644 index 0000000..f845b13 --- /dev/null +++ b/promo-ui2/src/plugins/vueI18n/index.ts @@ -0,0 +1,42 @@ +import type { App } from 'vue' +import { createI18n } from 'vue-i18n' +import { useLocaleStoreWithOut } from '@/store/modules/locale' +import type { I18n, I18nOptions } from 'vue-i18n' +import { setHtmlPageLang } from './helper' + +export let i18n: ReturnType + +const createI18nOptions = async (): Promise => { + const localeStore = useLocaleStoreWithOut() + const locale = localeStore.getCurrentLocale + const localeMap = localeStore.getLocaleMap + const defaultLocal = await import(`../../locales/${locale.lang}.ts`) + const message = defaultLocal.default ?? {} + + setHtmlPageLang(locale.lang) + + localeStore.setCurrentLocale({ + lang: locale.lang + // elLocale: elLocal + }) + + return { + legacy: false, + locale: locale.lang, + fallbackLocale: locale.lang, + messages: { + [locale.lang]: message + }, + availableLocales: localeMap.map((v) => v.lang), + sync: true, + silentTranslationWarn: true, + missingWarn: false, + silentFallbackWarn: true + } +} + +export const setupI18n = async (app: App) => { + const options = await createI18nOptions() + i18n = createI18n(options) as I18n + app.use(i18n) +} diff --git a/promo-ui2/src/router/index.ts b/promo-ui2/src/router/index.ts new file mode 100644 index 0000000..1983cc5 --- /dev/null +++ b/promo-ui2/src/router/index.ts @@ -0,0 +1,444 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' +import type { App } from 'vue' +import { Layout } from '@/utils/routerHelper' +import { useI18n } from '@/hooks/web/useI18n' +import { NO_RESET_WHITE_LIST } from '@/constants' + +const { t } = useI18n() + +export const constantRouterMap: AppRouteRecordRaw[] = [ + { + path: '/', + component: Layout, + redirect: '/home/index', + name: 'Root', + meta: { + hidden: true + } + }, + { + path: '/redirect', + component: Layout, + name: 'RedirectWrap', + children: [ + { + path: '/redirect/:path(.*)', + name: 'Redirect', + component: () => import('@/views/Redirect/Redirect.vue'), + meta: {} + } + ], + meta: { + hidden: true, + noTagsView: true + } + }, + { + path: '/login', + component: () => import('@/views/Login/Login.vue'), + name: 'Login', + meta: { + hidden: true, + title: t('router.login'), + noTagsView: true + } + }, + { + path: '/personal', + component: Layout, + redirect: '/personal/personal-center', + name: 'Personal', + meta: { + title: t('router.personal'), + hidden: true, + canTo: true + }, + children: [ + { + path: 'personal-center', + component: () => import('@/views/Personal/PersonalCenter/PersonalCenter.vue'), + name: 'PersonalCenter', + meta: { + title: t('router.personalCenter'), + hidden: true, + canTo: true + } + } + ] + }, + { + path: '/404', + component: () => import('@/views/Error/404.vue'), + name: 'NoFind', + meta: { + hidden: true, + title: '404', + noTagsView: true + } + } +] + +export const asyncRouterMap: AppRouteRecordRaw[] = [ + { + path: '/dashboard', + component: Layout, + redirect: '/dashboard/index', + name: 'Dashboard', + meta: {}, + children: [ + { + path: 'index', + component: () => import('@/views/Dashboard/Analysis.vue'), + name: 'Analysis', + meta: { + title: t('router.analysis'), + icon: 'ep:data-analysis' + } + } + ] + }, + { + path: '/home', + component: Layout, + redirect: '/home/index', + name: 'Home', + meta: {}, + children: [ + { + path: 'index', + component: () => import('@/views/Home/index.vue'), + name: 'HomePage', + meta: { + title: '首页', + icon: 'ep:home-filled' + } + } + ] + }, + { + path: '/user', + component: Layout, + name: 'user', + meta: {}, + redirect: '/user', + children: [ + { + path: 'index', + name: 'index', + component: () => import('@/views/User/index.vue'), + meta: { title: '个人信息', icon: 'ep:user' } + } + ] + }, + { + path: '/statistics', + component: Layout, + redirect: '/statistics/index', + name: 'Statistics', + meta: {}, + children: [ + { + path: 'index', + component: () => import('@/views/Dashboard/Analysis.vue'), + name: 'Analysis', + meta: { + title: t('router.analysis'), + icon: 'ep:pie-chart' + } + } + ] + }, + { + path: '/turnover', + component: Layout, + name: 'turnover', + redirect: '/turnover', + meta: {}, + children: [ + { + path: 'index', + name: 'turnoverindex', + component: () => import('@/views/Turnover/index.vue'), + meta: { title: t('app.turnover'), icon: 'ep:document' } + } + ] + }, + { + path: '/subFlow', + component: Layout, + name: 'subFlow', + meta: {}, + redirect: '/subFlow', + children: [ + { + path: 'index', + name: 'subFlowindex', + component: () => import('@/views/subFlow/index.vue'), + meta: { title: t('app.subFlow'), icon: 'ep:sort' } + } + ] + }, + { + path: '/mymoney', + component: Layout, + name: 'mymoney', + meta: {}, + redirect: '/mymoney', + children: [ + { + path: 'index', + name: 'mymoneyIndex', + component: () => import('@/views/MyMoney/index.vue'), + meta: { title: t('app.mymoney'), icon: 'ep:money' } + } + ] + }, + { + path: '/permission', + component: Layout, + name: 'permission', + meta: {}, + redirect: '/permission/index', + children: [ + { + path: 'index', + name: 'permissionIndex', + component: () => import('@/views/Permission/index.vue'), + meta: { title: '权限管理', icon: 'ep:setting' } + } + ] + }, + { + path: '/cardMerchant', + component: Layout, + name: 'cardMerchant', + meta: {}, + redirect: '/cardMerchant/index', + children: [ + { + path: 'index', + name: 'cardMerchantIndex', + component: () => import('@/views/CardMerchant/index.vue'), + meta: { title: '卡密管理', icon: 'ep:ticket' } + } + ] + }, + { + path: '/businessManage', + component: Layout, + name: 'businessManage', + meta: {}, + redirect: '/businessManage/index', + children: [ + { + path: 'index', + name: 'businessManageIndex', + component: () => import('@/views/BusinessManage/index.vue'), + meta: { title: '招商管理', icon: 'ep:office-building' } + } + ] + }, + { + path: '/anchorManage', + component: Layout, + name: 'anchorManage', + meta: {}, + redirect: '/anchorManage/index', + children: [ + { + path: 'index', + name: 'anchorManageIndex', + component: () => import('@/views/AnchorManage/index.vue'), + meta: { title: '主播管理', icon: 'ep:microphone' } + } + ] + }, + { + path: '/cardMerchantManage', + component: Layout, + name: 'cardMerchantManage', + meta: {}, + redirect: '/cardMerchantManage/index', + children: [ + { + path: 'index', + name: 'cardMerchantManageIndex', + component: () => import('@/views/CardMerchantManage/index.vue'), + meta: { title: '卡商管理', icon: 'ep:credit-card' } + } + ] + }, + { + path: '/adminManage', + component: Layout, + name: 'adminManage', + meta: {}, + redirect: '/adminManage/index', + children: [ + { + path: 'index', + name: 'adminManageIndex', + component: () => import('@/views/AdminManage/index.vue'), + meta: { title: '管理员管理', icon: 'ep:user-filled' } + } + ] + }, + { + path: '/mySubordinates', + component: Layout, + name: 'mySubordinates', + meta: {}, + redirect: '/mySubordinates/index', + children: [ + { + path: 'index', + name: 'mySubordinatesIndex', + component: () => import('@/views/MySubordinates/index.vue'), + meta: { title: '我的下级', icon: 'ep:user' } + } + ] + }, + { + path: '/flowRecord', + component: Layout, + name: 'flowRecord', + meta: {}, + redirect: '/flowRecord/index', + children: [ + { + path: 'index', + name: 'flowRecordIndex', + component: () => import('@/views/FlowRecord/index.vue'), + meta: { title: '流水记录', icon: 'ep:document' } + } + ] + }, + { + path: '/cdkeyCreate', + component: Layout, + name: 'cdkeyCreate', + meta: {}, + redirect: '/cdkeyCreate/index', + children: [ + { + path: 'index', + name: 'cdkeyCreateIndex', + component: () => import('@/views/CdkeyCreate/index.vue'), + meta: { title: '创建CDKEY记录', icon: 'ep:document-add' } + } + ] + }, + { + path: '/cdkeyRedeem', + component: Layout, + name: 'cdkeyRedeem', + meta: {}, + redirect: '/cdkeyRedeem/index', + children: [ + { + path: 'index', + name: 'cdkeyRedeemIndex', + component: () => import('@/views/CdkeyRedeem/index.vue'), + meta: { title: '兑换CDKEY记录', icon: 'ep:document-checked' } + } + ] + }, + { + path: '/cdkeyCreateDaily', + component: Layout, + name: 'cdkeyCreateDaily', + meta: {}, + redirect: '/cdkeyCreateDaily/index', + children: [ + { + path: 'index', + name: 'cdkeyCreateDailyIndex', + component: () => import('@/views/CdkeyCreateDaily/index.vue'), + meta: { title: '创建CDKEY日报', icon: 'ep:calendar' } + } + ] + }, + { + path: '/cdkeyRedeemDaily', + component: Layout, + name: 'cdkeyRedeemDaily', + meta: {}, + redirect: '/cdkeyRedeemDaily/index', + children: [ + { + path: 'index', + name: 'cdkeyRedeemDailyIndex', + component: () => import('@/views/CdkeyRedeemDaily/index.vue'), + meta: { title: '兑换CDKEY日报', icon: 'ep:calendar' } + } + ] + }, + { + path: '/rollRoom', + component: Layout, + name: 'rollRoom', + meta: {}, + redirect: '/rollRoom/index', + children: [ + { + path: 'index', + name: 'rollRoomIndex', + component: () => import('@/views/RollRoom/index.vue'), + meta: { title: 'ROLL房管理', icon: 'ep:box' } + } + ] + }, + { + path: '/commissionReport', + component: Layout, + name: 'commissionReport', + meta: {}, + redirect: '/commissionReport/index', + children: [ + { + path: 'index', + name: 'commissionReportIndex', + component: () => import('@/views/CommissionReport/index.vue'), + meta: { title: '返点报表', icon: 'ep:data-line' } + } + ] + }, + { + path: '/subordinateRecharge', + component: Layout, + name: 'subordinateRecharge', + meta: {}, + redirect: '/subordinateRecharge/index', + children: [ + { + path: 'index', + name: 'subordinateRechargeIndex', + component: () => import('@/views/SubordinateRecharge/index.vue'), + meta: { title: '下级充值报表', icon: 'ep:coin' } + } + ] + } +] + +const router = createRouter({ + history: createWebHashHistory(), + strict: true, + routes: constantRouterMap as RouteRecordRaw[], + scrollBehavior: () => ({ left: 0, top: 0 }) +}) + +export const resetRouter = (): void => { + router.getRoutes().forEach((route) => { + const { name } = route + if (name && !NO_RESET_WHITE_LIST.includes(name as string)) { + router.hasRoute(name) && router.removeRoute(name) + } + }) +} + +export const setupRouter = (app: App) => { + app.use(router) +} + +export default router diff --git a/promo-ui2/src/router/role.ts b/promo-ui2/src/router/role.ts new file mode 100644 index 0000000..00e9d79 --- /dev/null +++ b/promo-ui2/src/router/role.ts @@ -0,0 +1,59 @@ +// 主播(下级是普通用户) +export const role01: string[] = [ + '/home/index', + '/dashboard', + '/dashboard/index', + '/user', + '/user/index', + '/turnover', + '/turnover/index', + '/mymoney', + '/mymoney/index' +] + +// 招商(下级是主播) +export const role04: string[] = [ + '/home/index', + '/dashboard', + '/dashboard/index', + '/user', + '/user/index', + '/subFlow', + '/subFlow/index', + '/mymoney', + '/mymoney/index' +] + +// 卡商(无下级,专责生成卡密) +export const role05: string[] = [ + '/home/index', + '/user', + '/user/index', + '/cardMerchant', + '/cardMerchant/index' +] + +// 超级管理员(所有菜单) +export const role99: string[] = [ + '/home/index', + '/dashboard', + '/dashboard/index', + '/user', + '/user/index', + '/turnover', + '/turnover/index', + '/subFlow', + '/subFlow/index', + '/mymoney', + '/mymoney/index', + '/cardMerchant', + '/cardMerchant/index', + '/permission', + '/permission/index', + '/businessManage', + '/businessManage/index', + '/anchorManage', + '/anchorManage/index', + '/cardMerchantManage', + '/cardMerchantManage/index' +] diff --git a/promo-ui2/src/store/index.ts b/promo-ui2/src/store/index.ts new file mode 100644 index 0000000..ee92ca8 --- /dev/null +++ b/promo-ui2/src/store/index.ts @@ -0,0 +1,13 @@ +import type { App } from 'vue' +import { createPinia } from 'pinia' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' + +const store = createPinia() + +store.use(piniaPluginPersistedstate) + +export const setupStore = (app: App) => { + app.use(store) +} + +export { store } diff --git a/promo-ui2/src/store/modules/app.ts b/promo-ui2/src/store/modules/app.ts new file mode 100644 index 0000000..d7f57ff --- /dev/null +++ b/promo-ui2/src/store/modules/app.ts @@ -0,0 +1,340 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +import { setCssVar, humpToUnderline } from '@/utils' +import { colorIsDark, hexToRGB, lighten, mix } from '@/utils/color' +import { ElMessage, ComponentSize } from 'element-plus' +import { useCssVar } from '@vueuse/core' +import { unref } from 'vue' +import { useDark } from '@vueuse/core' + +interface AppState { + breadcrumb: boolean + breadcrumbIcon: boolean + collapse: boolean + uniqueOpened: boolean + hamburger: boolean + screenfull: boolean + size: boolean + locale: boolean + tagsView: boolean + tagsViewIcon: boolean + logo: boolean + fixedHeader: boolean + greyMode: boolean + dynamicRouter: boolean + serverDynamicRouter: boolean + pageLoading: boolean + layout: LayoutType + title: string + isDark: boolean + currentSize: ComponentSize + sizeMap: ComponentSize[] + mobile: boolean + footer: boolean + theme: ThemeTypes + fixedMenu: boolean +} + +export const useAppStore = defineStore('app', { + state: (): AppState => { + return { + sizeMap: ['default', 'large', 'small'], + mobile: false, // 是否是移动端 + title: import.meta.env.VITE_APP_TITLE, // 标题 + pageLoading: false, // 路由跳转loading + breadcrumb: true, // 面包屑 + breadcrumbIcon: true, // 面包屑图标 + collapse: false, // 折叠菜单 + uniqueOpened: false, // 是否只保持一个子菜单的展开 + hamburger: true, // 折叠图标 + screenfull: true, // 全屏图标 + size: true, // 尺寸图标 + locale: true, // 多语言图标 + tagsView: true, // 标签页 + tagsViewIcon: true, // 是否显示标签图标 + logo: true, // logo + fixedHeader: true, // 固定toolheader + footer: true, // 显示页脚 + greyMode: false, // 是否开始灰色模式,用于特殊悼念日 + dynamicRouter: true, // 是否动态路由 + serverDynamicRouter: false, // 是否服务端渲染动态路由 + fixedMenu: false, // 是否固定菜单 + + layout: 'classic', // layout布局 + isDark: false, // 是否是暗黑模式 + currentSize: 'default', // 组件尺寸 + theme: { + // 主题色 + elColorPrimary: '#409eff', + // 左侧菜单边框颜色 + leftMenuBorderColor: 'inherit', + // 左侧菜单背景颜色 + leftMenuBgColor: '#001529', + // 左侧菜单浅色背景颜色 + leftMenuBgLightColor: '#0f2438', + // 左侧菜单选中背景颜色 + leftMenuBgActiveColor: 'var(--el-color-primary)', + // 左侧菜单收起选中背景颜色 + leftMenuCollapseBgActiveColor: 'var(--el-color-primary)', + // 左侧菜单字体颜色 + leftMenuTextColor: '#bfcbd9', + // 左侧菜单选中字体颜色 + leftMenuTextActiveColor: '#fff', + // logo字体颜色 + logoTitleTextColor: '#fff', + // logo边框颜色 + logoBorderColor: 'inherit', + // 头部背景颜色 + topHeaderBgColor: '#fff', + // 头部字体颜色 + topHeaderTextColor: 'inherit', + // 头部悬停颜色 + topHeaderHoverColor: '#f6f6f6', + // 头部边框颜色 + topToolBorderColor: '#eee' + } + } + }, + getters: { + getBreadcrumb(): boolean { + return this.breadcrumb + }, + getBreadcrumbIcon(): boolean { + return this.breadcrumbIcon + }, + getCollapse(): boolean { + return this.collapse + }, + getUniqueOpened(): boolean { + return this.uniqueOpened + }, + getHamburger(): boolean { + return this.hamburger + }, + getScreenfull(): boolean { + return this.screenfull + }, + getSize(): boolean { + return this.size + }, + getLocale(): boolean { + return this.locale + }, + getTagsView(): boolean { + return this.tagsView + }, + getTagsViewIcon(): boolean { + return this.tagsViewIcon + }, + getLogo(): boolean { + return this.logo + }, + getFixedHeader(): boolean { + return this.fixedHeader + }, + getGreyMode(): boolean { + return this.greyMode + }, + getDynamicRouter(): boolean { + return this.dynamicRouter + }, + getServerDynamicRouter(): boolean { + return this.serverDynamicRouter + }, + getFixedMenu(): boolean { + return this.fixedMenu + }, + getPageLoading(): boolean { + return this.pageLoading + }, + getLayout(): LayoutType { + return this.layout + }, + getTitle(): string { + return this.title + }, + getIsDark(): boolean { + return this.isDark + }, + getCurrentSize(): ComponentSize { + return this.currentSize + }, + getSizeMap(): ComponentSize[] { + return this.sizeMap + }, + getMobile(): boolean { + return this.mobile + }, + getTheme(): ThemeTypes { + return this.theme + }, + getFooter(): boolean { + return this.footer + } + }, + actions: { + setBreadcrumb(breadcrumb: boolean) { + this.breadcrumb = breadcrumb + }, + setBreadcrumbIcon(breadcrumbIcon: boolean) { + this.breadcrumbIcon = breadcrumbIcon + }, + setCollapse(collapse: boolean) { + this.collapse = collapse + }, + setUniqueOpened(uniqueOpened: boolean) { + this.uniqueOpened = uniqueOpened + }, + setHamburger(hamburger: boolean) { + this.hamburger = hamburger + }, + setScreenfull(screenfull: boolean) { + this.screenfull = screenfull + }, + setSize(size: boolean) { + this.size = size + }, + setLocale(locale: boolean) { + this.locale = locale + }, + setTagsView(tagsView: boolean) { + this.tagsView = tagsView + }, + setTagsViewIcon(tagsViewIcon: boolean) { + this.tagsViewIcon = tagsViewIcon + }, + setLogo(logo: boolean) { + this.logo = logo + }, + setFixedHeader(fixedHeader: boolean) { + this.fixedHeader = fixedHeader + }, + setGreyMode(greyMode: boolean) { + this.greyMode = greyMode + }, + setDynamicRouter(dynamicRouter: boolean) { + this.dynamicRouter = dynamicRouter + }, + setServerDynamicRouter(serverDynamicRouter: boolean) { + this.serverDynamicRouter = serverDynamicRouter + }, + setFixedMenu(fixedMenu: boolean) { + this.fixedMenu = fixedMenu + }, + setPageLoading(pageLoading: boolean) { + this.pageLoading = pageLoading + }, + setLayout(layout: LayoutType) { + if (this.mobile && layout !== 'classic') { + ElMessage.warning('移动端模式下不支持切换其它布局') + return + } + this.layout = layout + }, + setTitle(title: string) { + this.title = title + }, + setIsDark(isDark: boolean) { + this.isDark = isDark + if (this.isDark) { + document.documentElement.classList.add('dark') + document.documentElement.classList.remove('light') + } else { + document.documentElement.classList.add('light') + document.documentElement.classList.remove('dark') + } + this.setPrimaryLight() + }, + setCurrentSize(currentSize: ComponentSize) { + this.currentSize = currentSize + }, + setMobile(mobile: boolean) { + this.mobile = mobile + }, + setTheme(theme: ThemeTypes) { + this.theme = Object.assign(this.theme, theme) + }, + setCssVarTheme() { + for (const key in this.theme) { + setCssVar(`--${humpToUnderline(key)}`, this.theme[key]) + } + this.setPrimaryLight() + }, + setFooter(footer: boolean) { + this.footer = footer + }, + setPrimaryLight() { + if (this.theme.elColorPrimary) { + const elColorPrimary = this.theme.elColorPrimary + const color = this.isDark ? '#000000' : '#ffffff' + const lightList = [3, 5, 7, 8, 9] + lightList.forEach((v) => { + setCssVar(`--el-color-primary-light-${v}`, mix(color, elColorPrimary, v / 10)) + }) + setCssVar(`--el-color-primary-dark-2`, mix(color, elColorPrimary, 0.2)) + } + }, + setMenuTheme(color: string) { + const primaryColor = useCssVar('--el-color-primary', document.documentElement) + const isDarkColor = colorIsDark(color) + const theme: Recordable = { + // 左侧菜单边框颜色 + leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee', + // 左侧菜单背景颜色 + leftMenuBgColor: color, + // 左侧菜单浅色背景颜色 + leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color, + // 左侧菜单选中背景颜色 + leftMenuBgActiveColor: isDarkColor + ? 'var(--el-color-primary)' + : hexToRGB(unref(primaryColor) as string, 0.1), + // 左侧菜单收起选中背景颜色 + leftMenuCollapseBgActiveColor: isDarkColor + ? 'var(--el-color-primary)' + : hexToRGB(unref(primaryColor) as string, 0.1), + // 左侧菜单字体颜色 + leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333', + // 左侧菜单选中字体颜色 + leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)', + // logo字体颜色 + logoTitleTextColor: isDarkColor ? '#fff' : 'inherit', + // logo边框颜色 + logoBorderColor: isDarkColor ? color : '#eee' + } + this.setTheme(theme) + this.setCssVarTheme() + }, + setHeaderTheme(color: string) { + const isDarkColor = colorIsDark(color) + const textColor = isDarkColor ? '#fff' : 'inherit' + const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6' + const topToolBorderColor = isDarkColor ? color : '#eee' + setCssVar('--top-header-bg-color', color) + setCssVar('--top-header-text-color', textColor) + setCssVar('--top-header-hover-color', textHoverColor) + this.setTheme({ + topHeaderBgColor: color, + topHeaderTextColor: textColor, + topHeaderHoverColor: textHoverColor, + topToolBorderColor + }) + if (this.getLayout === 'top') { + this.setMenuTheme(color) + } + }, + initTheme() { + const isDark = useDark({ + valueDark: 'dark', + valueLight: 'light' + }) + isDark.value = this.getIsDark + const newTitle = import.meta.env.VITE_APP_TITLE + newTitle !== this.getTitle && this.setTitle(newTitle) + } + }, + persist: true +}) + +export const useAppStoreWithOut = () => { + return useAppStore(store) +} diff --git a/promo-ui2/src/store/modules/locale.ts b/promo-ui2/src/store/modules/locale.ts new file mode 100644 index 0000000..8d2ba35 --- /dev/null +++ b/promo-ui2/src/store/modules/locale.ts @@ -0,0 +1,59 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import en from 'element-plus/es/locale/lang/en' +import { useStorage } from '@/hooks/web/useStorage' +import { LocaleDropdownType } from '@/components/LocaleDropdown' + +const { getStorage, setStorage } = useStorage('localStorage') + +const elLocaleMap = { + 'zh-CN': zhCn, + en: en +} +interface LocaleState { + currentLocale: LocaleDropdownType + localeMap: LocaleDropdownType[] +} + +export const useLocaleStore = defineStore('locales', { + state: (): LocaleState => { + return { + currentLocale: { + lang: getStorage('lang') || 'zh-CN', + elLocale: elLocaleMap[getStorage('lang') || 'zh-CN'] + }, + // 多语言 + localeMap: [ + { + lang: 'zh-CN', + name: '简体中文' + }, + { + lang: 'en', + name: 'English' + } + ] + } + }, + getters: { + getCurrentLocale(): LocaleDropdownType { + return this.currentLocale + }, + getLocaleMap(): LocaleDropdownType[] { + return this.localeMap + } + }, + actions: { + setCurrentLocale(localeMap: LocaleDropdownType) { + // this.locale = Object.assign(this.locale, localeMap) + this.currentLocale.lang = localeMap?.lang + this.currentLocale.elLocale = elLocaleMap[localeMap?.lang] + setStorage('lang', localeMap?.lang) + } + } +}) + +export const useLocaleStoreWithOut = () => { + return useLocaleStore(store) +} diff --git a/promo-ui2/src/store/modules/lock.ts b/promo-ui2/src/store/modules/lock.ts new file mode 100644 index 0000000..70ff87f --- /dev/null +++ b/promo-ui2/src/store/modules/lock.ts @@ -0,0 +1,48 @@ +import { defineStore } from 'pinia' +import { store } from '../index' + +interface lockInfo { + isLock?: boolean + password?: string +} + +interface LockState { + lockInfo: lockInfo +} + +export const useLockStore = defineStore('lock', { + state: (): LockState => { + return { + lockInfo: { + // isLock: false, // 是否锁定屏幕 + // password: '' // 锁屏密码 + } + } + }, + getters: { + getLockInfo(): lockInfo { + return this.lockInfo + } + }, + actions: { + setLockInfo(lockInfo: lockInfo) { + this.lockInfo = lockInfo + }, + resetLockInfo() { + this.lockInfo = {} + }, + unLock(password: string) { + if (this.lockInfo?.password === password) { + this.resetLockInfo() + return true + } else { + return false + } + } + }, + persist: true +}) + +export const useLockStoreWithOut = () => { + return useLockStore(store) +} diff --git a/promo-ui2/src/store/modules/permission.ts b/promo-ui2/src/store/modules/permission.ts new file mode 100644 index 0000000..141d9a8 --- /dev/null +++ b/promo-ui2/src/store/modules/permission.ts @@ -0,0 +1,94 @@ +import { defineStore } from 'pinia' +import { asyncRouterMap, constantRouterMap } from '@/router' +import { + generateRoutesByFrontEnd, + generateRoutesByServer, + flatMultiLevelRoutes +} from '@/utils/routerHelper' +import { store } from '../index' +import { cloneDeep } from 'lodash-es' + +export interface PermissionState { + routers: AppRouteRecordRaw[] + addRouters: AppRouteRecordRaw[] + isAddRouters: boolean + menuTabRouters: AppRouteRecordRaw[] +} + +export const usePermissionStore = defineStore('permission', { + state: (): PermissionState => ({ + routers: [], + addRouters: [], + isAddRouters: false, + menuTabRouters: [] + }), + getters: { + getRouters(): AppRouteRecordRaw[] { + return this.routers + }, + getAddRouters(): AppRouteRecordRaw[] { + return flatMultiLevelRoutes(cloneDeep(this.addRouters)) + }, + getIsAddRouters(): boolean { + return this.isAddRouters + }, + getMenuTabRouters(): AppRouteRecordRaw[] { + return this.menuTabRouters + } + }, + actions: { + generateRoutes( + type: 'server' | 'frontEnd' | 'static', + routers?: AppCustomRouteRecordRaw[] | string[] + ): Promise { + return new Promise((resolve) => { + let routerMap: AppRouteRecordRaw[] = [] + if (type === 'server') { + // 模拟后端过滤菜单 + routerMap = generateRoutesByServer(routers as AppCustomRouteRecordRaw[]) + } else if (type === 'frontEnd') { + // 模拟前端过滤菜单 + routerMap = generateRoutesByFrontEnd(cloneDeep(asyncRouterMap), routers as string[]) + } else { + // 直接读取静态路由表 + routerMap = cloneDeep(asyncRouterMap) + } + // 动态路由,404一定要放到最后面 + this.addRouters = routerMap.concat([ + { + path: '/:path(.*)*', + redirect: '/404', + name: '404Page', + meta: { + hidden: true, + breadcrumb: false + } + } + ]) + // 渲染菜单的所有路由 + this.routers = cloneDeep(constantRouterMap).concat(routerMap) + resolve() + }) + }, + setIsAddRouters(state: boolean): void { + this.isAddRouters = state + }, + setMenuTabRouters(routers: AppRouteRecordRaw[]): void { + this.menuTabRouters = routers + } + }, + persist: [ + { + pick: ['routers'], + storage: localStorage + }, + { + pick: ['menuTabRouters'], + storage: localStorage + } + ] +}) + +export const usePermissionStoreWithOut = () => { + return usePermissionStore(store) +} diff --git a/promo-ui2/src/store/modules/tagsView.ts b/promo-ui2/src/store/modules/tagsView.ts new file mode 100644 index 0000000..d07a439 --- /dev/null +++ b/promo-ui2/src/store/modules/tagsView.ts @@ -0,0 +1,163 @@ +import router from '@/router' +import type { RouteLocationNormalizedLoaded } from 'vue-router' +import { getRawRoute } from '@/utils/routerHelper' +import { defineStore } from 'pinia' +import { store } from '../index' +import { findIndex } from '@/utils' +import { useUserStoreWithOut } from './user' + +export interface TagsViewState { + visitedViews: RouteLocationNormalizedLoaded[] + cachedViews: Set + selectedTag?: RouteLocationNormalizedLoaded +} + +export const useTagsViewStore = defineStore('tagsView', { + state: (): TagsViewState => ({ + visitedViews: [], + cachedViews: new Set(), + selectedTag: undefined + }), + getters: { + getVisitedViews(): RouteLocationNormalizedLoaded[] { + return this.visitedViews + }, + getCachedViews(): string[] { + return Array.from(this.cachedViews) + }, + getSelectedTag(): RouteLocationNormalizedLoaded | undefined { + return this.selectedTag + } + }, + actions: { + // 新增缓存和tag + addView(view: RouteLocationNormalizedLoaded): void { + this.addVisitedView(view) + this.addCachedView() + }, + // 新增tag + addVisitedView(view: RouteLocationNormalizedLoaded) { + if (this.visitedViews.some((v) => v.path === view.path)) return + if (view.meta?.noTagsView) return + this.visitedViews.push( + Object.assign({}, view, { + title: view.meta?.title || 'no-name' + }) + ) + }, + // 新增缓存 + addCachedView() { + const cacheMap: Set = new Set() + for (const v of this.visitedViews) { + const item = getRawRoute(v) + const needCache = !item?.meta?.noCache + if (!needCache) { + continue + } + const name = item.name as string + cacheMap.add(name) + } + if (Array.from(this.cachedViews).sort().toString() === Array.from(cacheMap).sort().toString()) + return + this.cachedViews = cacheMap + }, + // 删除某个 + delView(view: RouteLocationNormalizedLoaded) { + this.delVisitedView(view) + this.addCachedView() + }, + // 删除tag + delVisitedView(view: RouteLocationNormalizedLoaded) { + for (const [i, v] of this.visitedViews.entries()) { + if (v.path === view.path) { + this.visitedViews.splice(i, 1) + break + } + } + }, + // 删除缓存 + delCachedView() { + const route = router.currentRoute.value + const index = findIndex(this.getCachedViews, (v) => v === route.name) + if (index > -1) { + this.cachedViews.delete(this.getCachedViews[index]) + } + }, + // 删除所有缓存和tag + delAllViews() { + this.delAllVisitedViews() + this.addCachedView() + }, + // 删除所有tag + delAllVisitedViews() { + const userStore = useUserStoreWithOut() + + // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) + this.visitedViews = userStore.getUserInfo + ? this.visitedViews.filter((tag) => tag?.meta?.affix) + : [] + }, + // 删除其它 + delOthersViews(view: RouteLocationNormalizedLoaded) { + this.delOthersVisitedViews(view) + this.addCachedView() + }, + // 删除其它tag + delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { + this.visitedViews = this.visitedViews.filter((v) => { + return v?.meta?.affix || v.path === view.path + }) + }, + // 删除左侧 + delLeftViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex( + this.visitedViews, + (v) => v.path === view.path + ) + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.path === view.path || i > index + }) + this.addCachedView() + } + }, + // 删除右侧 + delRightViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex( + this.visitedViews, + (v) => v.path === view.path + ) + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.path === view.path || i < index + }) + this.addCachedView() + } + }, + updateVisitedView(view: RouteLocationNormalizedLoaded) { + for (let v of this.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + }, + // 设置当前选中的tag + setSelectedTag(tag: RouteLocationNormalizedLoaded) { + this.selectedTag = tag + }, + setTitle(title: string, path?: string) { + for (const v of this.visitedViews) { + if (v.path === (path ?? this.selectedTag?.path)) { + v.meta.title = title + break + } + } + } + }, + persist: false +}) + +export const useTagsViewStoreWithOut = () => { + return useTagsViewStore(store) +} diff --git a/promo-ui2/src/store/modules/user.ts b/promo-ui2/src/store/modules/user.ts new file mode 100644 index 0000000..182aa96 --- /dev/null +++ b/promo-ui2/src/store/modules/user.ts @@ -0,0 +1,101 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +import { UserLoginType, UserType } from '@/api/login/types' +import { ElMessageBox } from 'element-plus' +import { useI18n } from '@/hooks/web/useI18n' +import { useTagsViewStore } from './tagsView' +import router from '@/router' + +interface UserState { + userInfo?: UserType + tokenKey: string + token: string + roleRouters?: string[] | AppCustomRouteRecordRaw[] + rememberMe: boolean + loginInfo?: UserLoginType +} + +export const useUserStore = defineStore('user', { + state: (): UserState => { + return { + userInfo: undefined, + tokenKey: 'Authorization', + token: '', + roleRouters: undefined, + // 记住我 + rememberMe: true, + loginInfo: undefined + } + }, + getters: { + getTokenKey(): string { + return this.tokenKey + }, + getToken(): string { + return this.token + }, + getUserInfo(): UserType | undefined { + return this.userInfo + }, + getRoleRouters(): string[] | AppCustomRouteRecordRaw[] | undefined { + return this.roleRouters + }, + getRememberMe(): boolean { + return this.rememberMe + }, + getLoginInfo(): UserLoginType | undefined { + return this.loginInfo + } + }, + actions: { + setTokenKey(tokenKey: string) { + this.tokenKey = tokenKey + }, + setToken(token: string) { + this.token = token + }, + setUserInfo(userInfo?: UserType) { + this.userInfo = userInfo + }, + setRoleRouters(roleRouters: string[] | AppCustomRouteRecordRaw[]) { + this.roleRouters = roleRouters + }, + logoutConfirm() { + const { t } = useI18n() + ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), { + confirmButtonText: t('common.ok'), + cancelButtonText: t('common.cancel'), + type: 'warning' + }) + .then(async () => { + const res = true // await loginOutApi().catch(() => {}) + if (res) { + this.reset() + } + }) + .catch(() => {}) + }, + reset() { + const tagsViewStore = useTagsViewStore() + tagsViewStore.delAllViews() + this.setToken('') + this.setUserInfo(undefined) + this.setRoleRouters([]) + router.replace('/login') + }, + logout() { + this.reset() + }, + setRememberMe(rememberMe: boolean) { + this.rememberMe = rememberMe + }, + setLoginInfo(loginInfo: UserLoginType | undefined) { + this.loginInfo = loginInfo + } + }, + persist: true +}) + +export const useUserStoreWithOut = () => { + return useUserStore(store) +} diff --git a/promo-ui2/src/styles/index.less b/promo-ui2/src/styles/index.less new file mode 100644 index 0000000..6825d70 --- /dev/null +++ b/promo-ui2/src/styles/index.less @@ -0,0 +1,7 @@ +@import './var.css'; +@import 'element-plus/theme-chalk/dark/css-vars.css'; + +// 解决抽屉弹出时,body宽度变化的问题 +.el-popup-parent--hidden { + width: 100% !important; +} diff --git a/promo-ui2/src/styles/var.css b/promo-ui2/src/styles/var.css new file mode 100644 index 0000000..fb869ec --- /dev/null +++ b/promo-ui2/src/styles/var.css @@ -0,0 +1,69 @@ +:root { + --login-bg-color: #293146; + + /* left menu start */ + --left-menu-max-width: 200px; + + --left-menu-min-width: 64px; + + --left-menu-bg-color: #001529; + + --left-menu-bg-light-color: #0f2438; + + --left-menu-bg-active-color: var(--el-color-primary); + + --left-menu-text-color: #bfcbd9; + + --left-menu-text-active-color: #fff; + + --left-menu-collapse-bg-active-color: var(--el-color-primary); + /* left menu end */ + + /* logo start */ + --logo-height: 50px; + + --logo-title-text-color: #fff; + /* logo end */ + + /* header start */ + --top-header-bg-color: '#fff'; + + --top-header-text-color: 'inherit'; + + --top-header-hover-color: #f6f6f6; + + --top-tool-height: var(--logo-height); + + --top-tool-p-x: 0; + + --tags-view-height: 35px; + /* header start */ + + /* tab menu start */ + --tab-menu-max-width: 80px; + + --tab-menu-min-width: 30px; + + --tab-menu-collapse-height: 36px; + /* tab menu end */ + + --app-content-padding: 20px; + + --app-content-bg-color: #f5f7f9; + + --app-footer-height: 50px; + + --transition-time-02: 0.2s; +} + +.dark { + --app-content-bg-color: var(--el-bg-color); +} + +*, +:after, +:before { + margin: 0; + padding: 0; + box-sizing: border-box; +} diff --git a/promo-ui2/src/styles/variables.module.less b/promo-ui2/src/styles/variables.module.less new file mode 100644 index 0000000..a773269 --- /dev/null +++ b/promo-ui2/src/styles/variables.module.less @@ -0,0 +1,10 @@ +// 命名空间 +@adminNamespace: v; +// el命名空间 +@elNamespace: el; + +// 导出变量 +:export { + namespace: @adminNamespace; + elNamespace: @elNamespace; +} diff --git a/promo-ui2/src/utils/aes.ts b/promo-ui2/src/utils/aes.ts new file mode 100644 index 0000000..b475752 --- /dev/null +++ b/promo-ui2/src/utils/aes.ts @@ -0,0 +1,65 @@ +import CryptoJS from 'crypto-js' + +// AES 密钥和 IV,需与后端 application.yml 中 aes.key / aes.iv 保持一致 +// 必须严格 16 位(128 bit) +const AES_KEY = 'ruoyi@aes@202416' // 16 位,与当前 yml 一致 +const AES_IV = 'ruoyi@aes@iv2024' // 16 位,与当前 yml 一致 + +/** + * 全局开关:优先读取 localStorage 运行时覆盖候,其次才是编译时环境变量 + * localStorage.setItem('__aes_encrypt__', 'true') → 强制开启 + * localStorage.setItem('__aes_encrypt__', 'false') → 强制关闭 + * localStorage.removeItem('__aes_encrypt__') → 回归环境变量 + */ +function isAesEnabled(): boolean { + const override = localStorage.getItem('__aes_encrypt__') + if (override !== null) { + console.log('[AES] 开关被 localStorage 覆盖为:', override) + return override === 'true' + } + const envVal = import.meta.env.VITE_USE_AES_ENCRYPT + console.log('[AES] 当前环境变量 VITE_USE_AES_ENCRYPT =', envVal) + return envVal === 'true' +} + +// 兼容旧引用,反映编译时初始状态 +export const AES_ENABLED = import.meta.env.VITE_USE_AES_ENCRYPT === 'true' + +/** + * AES-CBC 加密(PKCS7Padding,输出 Base64 字符串) + * @param plainText 明文数据(会自动序列化) + * @param forceEncrypt 强制加密(忽略全局开关),默认 true(总是加密) + */ +export function aesEncrypt(plainText: any, forceEncrypt = true): string { + // 自动序列化 JSON 对象或字符串 + const textToEncrypt = typeof plainText === 'string' + ? plainText + : JSON.stringify(plainText) + + const key = CryptoJS.enc.Utf8.parse(AES_KEY) + const iv = CryptoJS.enc.Utf8.parse(AES_IV) + + // 总是执行加密 + const encrypted = CryptoJS.AES.encrypt(textToEncrypt, key, { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }) + const result = encrypted.toString() + console.log('[AES] 加密完成,密文前 50 位:', result.substring(0, 50) + '...') + return result +} + +/** + * AES-CBC 解密(PKCS7Padding,输入 Base64 字符串) + */ +export function aesDecrypt(cipherText: string): string { + const key = CryptoJS.enc.Utf8.parse(AES_KEY) + const iv = CryptoJS.enc.Utf8.parse(AES_IV) + const decrypted = CryptoJS.AES.decrypt(cipherText, key, { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }) + return decrypted.toString(CryptoJS.enc.Utf8) +} diff --git a/promo-ui2/src/utils/color.ts b/promo-ui2/src/utils/color.ts new file mode 100644 index 0000000..387ffc6 --- /dev/null +++ b/promo-ui2/src/utils/color.ts @@ -0,0 +1,172 @@ +/** + * 判断是否 十六进制颜色值. + * 输入形式可为 #fff000 #f00 + * + * @param String color 十六进制颜色值 + * @return Boolean + */ +export const isHexColor = (color: string) => { + const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/ + return reg.test(color) +} + +/** + * RGB 颜色值转换为 十六进制颜色值. + * r, g, 和 b 需要在 [0, 255] 范围内 + * + * @return String 类似#ff00ff + * @param r + * @param g + * @param b + */ +export const rgbToHex = (r: number, g: number, b: number) => { + // tslint:disable-next-line:no-bitwise + const hex = ((r << 16) | (g << 8) | b).toString(16) + return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex +} + +/** + * Transform a HEX color to its RGB representation + * @param {string} hex The color to transform + * @returns The RGB representation of the passed color + */ +export const hexToRGB = (hex: string, opacity?: number) => { + let sHex = hex.toLowerCase() + if (isHexColor(hex)) { + if (sHex.length === 4) { + let sColorNew = '#' + for (let i = 1; i < 4; i += 1) { + sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1)) + } + sHex = sColorNew + } + const sColorChange: number[] = [] + for (let i = 1; i < 7; i += 2) { + sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2))) + } + return opacity + ? 'RGBA(' + sColorChange.join(',') + ',' + opacity + ')' + : 'RGB(' + sColorChange.join(',') + ')' + } + return sHex +} + +export const colorIsDark = (color: string) => { + if (!isHexColor(color)) return + const [r, g, b] = hexToRGB(color) + .replace(/(?:\(|\)|rgb|RGB)*/g, '') + .split(',') + .map((item) => Number(item)) + return r * 0.299 + g * 0.578 + b * 0.114 < 192 +} + +/** + * Darkens a HEX color given the passed percentage + * @param {string} color The color to process + * @param {number} amount The amount to change the color by + * @returns {string} The HEX representation of the processed color + */ +export const darken = (color: string, amount: number) => { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color + amount = Math.trunc((255 * amount) / 100) + return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight( + color.substring(2, 4), + amount + )}${subtractLight(color.substring(4, 6), amount)}` +} + +/** + * Lightens a 6 char HEX color according to the passed percentage + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed color represented as HEX + */ +export const lighten = (color: string, amount: number) => { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color + amount = Math.trunc((255 * amount) / 100) + return `#${addLight(color.substring(0, 2), amount)}${addLight( + color.substring(2, 4), + amount + )}${addLight(color.substring(4, 6), amount)}` +} + +/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */ +/** + * Sums the passed percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +const addLight = (color: string, amount: number) => { + const cc = parseInt(color, 16) + amount + const c = cc > 255 ? 255 : cc + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` +} + +/** + * Calculates luminance of an rgb color + * @param {number} r red + * @param {number} g green + * @param {number} b blue + */ +const luminanace = (r: number, g: number, b: number) => { + const a = [r, g, b].map((v) => { + v /= 255 + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4) + }) + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722 +} + +/** + * Calculates contrast between two rgb colors + * @param {string} rgb1 rgb color 1 + * @param {string} rgb2 rgb color 2 + */ +const contrast = (rgb1: string[], rgb2: number[]) => { + return ( + (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) / + (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05) + ) +} + +/** + * Determines what the best text color is (black or white) based con the contrast with the background + * @param hexColor - Last selected color by the user + */ +export const calculateBestTextColor = (hexColor: string) => { + const rgbColor = hexToRGB(hexColor.substring(1)) + const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0]) + + return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF' +} + +/** + * Subtracts the indicated percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +const subtractLight = (color: string, amount: number) => { + const cc = parseInt(color, 16) - amount + const c = cc < 0 ? 0 : cc + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` +} + +/** + * Mixes two colors. + * + * @param {string} color1 - The first color, should be a 6-digit hexadecimal color code starting with `#`. + * @param {string} color2 - The second color, should be a 6-digit hexadecimal color code starting with `#`. + * @param {number} [weight=0.5] - The weight of color1 in the mix, should be a number between 0 and 1, where 0 represents 100% of color2, and 1 represents 100% of color1. + * @returns {string} The mixed color, a 6-digit hexadecimal color code starting with `#`. + */ +export const mix = (color1: string, color2: string, weight: number = 0.5): string => { + let color = '#' + for (let i = 0; i <= 2; i++) { + const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16) + const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16) + const c = Math.round(c1 * weight + c2 * (1 - weight)) + color += c.toString(16).padStart(2, '0') + } + return color +} diff --git a/promo-ui2/src/utils/dateUtil.ts b/promo-ui2/src/utils/dateUtil.ts new file mode 100644 index 0000000..36b87e3 --- /dev/null +++ b/promo-ui2/src/utils/dateUtil.ts @@ -0,0 +1,17 @@ +/** + * Independent time operation tool to facilitate subsequent switch to dayjs + */ +import dayjs from 'dayjs' + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' +const DATE_FORMAT = 'YYYY-MM-DD' + +export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string { + return dayjs(date).format(format) +} + +export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string { + return dayjs(date).format(format) +} + +export const dateUtil = dayjs diff --git a/promo-ui2/src/utils/domUtils.ts b/promo-ui2/src/utils/domUtils.ts new file mode 100644 index 0000000..dbc1989 --- /dev/null +++ b/promo-ui2/src/utils/domUtils.ts @@ -0,0 +1,289 @@ +import { isServer } from './is' +const ieVersion = isServer ? 0 : Number((document as any).documentMode) +const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g +const MOZ_HACK_REGEXP = /^moz([A-Z])/ + +export interface ViewportOffsetResult { + left: number + top: number + right: number + bottom: number + rightIncludeBody: number + bottomIncludeBody: number +} + +/* istanbul ignore next */ +const trim = function (string: string) { + return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '') +} + +/* istanbul ignore next */ +const camelCase = function (name: string) { + return name + .replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) { + return offset ? letter.toUpperCase() : letter + }) + .replace(MOZ_HACK_REGEXP, 'Moz$1') +} + +/* istanbul ignore next */ +export function hasClass(el: Element, cls: string) { + if (!el || !cls) return false + if (cls.indexOf(' ') !== -1) { + throw new Error('className should not contain space.') + } + if (el.classList) { + return el.classList.contains(cls) + } else { + return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1 + } +} + +/* istanbul ignore next */ +export function addClass(el: Element, cls: string) { + if (!el) return + let curClass = el.className + const classes = (cls || '').split(' ') + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i] + if (!clsName) continue + + if (el.classList) { + el.classList.add(clsName) + } else if (!hasClass(el, clsName)) { + curClass += ' ' + clsName + } + } + if (!el.classList) { + el.className = curClass + } +} + +/* istanbul ignore next */ +export function removeClass(el: Element, cls: string) { + if (!el || !cls) return + const classes = cls.split(' ') + let curClass = ' ' + el.className + ' ' + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i] + if (!clsName) continue + + if (el.classList) { + el.classList.remove(clsName) + } else if (hasClass(el, clsName)) { + curClass = curClass.replace(' ' + clsName + ' ', ' ') + } + } + if (!el.classList) { + el.className = trim(curClass) + } +} + +export function getBoundingClientRect(element: Element): DOMRect | number { + if (!element || !element.getBoundingClientRect) { + return 0 + } + return element.getBoundingClientRect() +} + +/** + * 获取当前元素的left、top偏移 + * left:元素最左侧距离文档左侧的距离 + * top:元素最顶端距离文档顶端的距离 + * right:元素最右侧距离文档右侧的距离 + * bottom:元素最底端距离文档底端的距离 + * rightIncludeBody:元素最左侧距离文档右侧的距离 + * bottomIncludeBody:元素最底端距离文档最底部的距离 + * + * @description: + */ +export function getViewportOffset(element: Element): ViewportOffsetResult { + const doc = document.documentElement + + const docScrollLeft = doc.scrollLeft + const docScrollTop = doc.scrollTop + const docClientLeft = doc.clientLeft + const docClientTop = doc.clientTop + + const pageXOffset = window.pageXOffset + const pageYOffset = window.pageYOffset + + const box = getBoundingClientRect(element) + + const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect + + const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0) + const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0) + const offsetLeft = retLeft + pageXOffset + const offsetTop = rectTop + pageYOffset + + const left = offsetLeft - scrollLeft + const top = offsetTop - scrollTop + + const clientWidth = window.document.documentElement.clientWidth + const clientHeight = window.document.documentElement.clientHeight + return { + left: left, + top: top, + right: clientWidth - rectWidth - left, + bottom: clientHeight - rectHeight - top, + rightIncludeBody: clientWidth - left, + bottomIncludeBody: clientHeight - top + } +} + +/* istanbul ignore next */ +export const on = function ( + element: HTMLElement | Document | Window, + event: string, + handler: EventListenerOrEventListenerObject +): void { + if (element && event && handler) { + element.addEventListener(event, handler, false) + } +} + +/* istanbul ignore next */ +export const off = function ( + element: HTMLElement | Document | Window, + event: string, + handler: any +): void { + if (element && event && handler) { + element.removeEventListener(event, handler, false) + } +} + +/* istanbul ignore next */ +export const once = function (el: HTMLElement, event: string, fn: EventListener): void { + const listener = function (this: any, ...args: unknown[]) { + if (fn) { + // @ts-ignore + fn.apply(this, args) + } + off(el, event, listener) + } + on(el, event, listener) +} + +/* istanbul ignore next */ +export const getStyle = + ieVersion < 9 + ? function (element: Element | any, styleName: string) { + if (isServer) return + if (!element || !styleName) return null + styleName = camelCase(styleName) + if (styleName === 'float') { + styleName = 'styleFloat' + } + try { + switch (styleName) { + case 'opacity': + try { + return element.filters.item('alpha').opacity / 100 + } catch (e) { + return 1.0 + } + default: + return element.style[styleName] || element.currentStyle + ? element.currentStyle[styleName] + : null + } + } catch (e) { + return element.style[styleName] + } + } + : function (element: Element | any, styleName: string) { + if (isServer) return + if (!element || !styleName) return null + styleName = camelCase(styleName) + if (styleName === 'float') { + styleName = 'cssFloat' + } + try { + const computed = (document as any).defaultView.getComputedStyle(element, '') + return element.style[styleName] || computed ? computed[styleName] : null + } catch (e) { + return element.style[styleName] + } + } + +/* istanbul ignore next */ +export function setStyle(element: Element | any, styleName: any, value: any) { + if (!element || !styleName) return + + if (typeof styleName === 'object') { + for (const prop in styleName) { + if (Object.prototype.hasOwnProperty.call(styleName, prop)) { + setStyle(element, prop, styleName[prop]) + } + } + } else { + styleName = camelCase(styleName) + if (styleName === 'opacity' && ieVersion < 9) { + element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')' + } else { + element.style[styleName] = value + } + } +} + +/* istanbul ignore next */ +export const isScroll = (el: Element, vertical: any) => { + if (isServer) return + + const determinedDirection = vertical !== null || vertical !== undefined + const overflow = determinedDirection + ? vertical + ? getStyle(el, 'overflow-y') + : getStyle(el, 'overflow-x') + : getStyle(el, 'overflow') + + return overflow.match(/(scroll|auto)/) +} + +/* istanbul ignore next */ +export const getScrollContainer = (el: Element, vertical?: any) => { + if (isServer) return + + let parent: any = el + while (parent) { + if ([window, document, document.documentElement].includes(parent)) { + return window + } + if (isScroll(parent, vertical)) { + return parent + } + parent = parent.parentNode + } + + return parent +} + +/* istanbul ignore next */ +export const isInContainer = (el: Element, container: any) => { + if (isServer || !el || !container) return false + + const elRect = el.getBoundingClientRect() + let containerRect + + if ([window, document, document.documentElement, null, undefined].includes(container)) { + containerRect = { + top: 0, + right: window.innerWidth, + bottom: window.innerHeight, + left: 0 + } + } else { + containerRect = container.getBoundingClientRect() + } + + return ( + elRect.top < containerRect.bottom && + elRect.bottom > containerRect.top && + elRect.right > containerRect.left && + elRect.left < containerRect.right + ) +} diff --git a/promo-ui2/src/utils/index.ts b/promo-ui2/src/utils/index.ts new file mode 100644 index 0000000..d91b679 --- /dev/null +++ b/promo-ui2/src/utils/index.ts @@ -0,0 +1,136 @@ +/** + * + * @param component 需要注册的组件 + * @param alias 组件别名 + * @returns any + */ +export const withInstall = (component: T, alias?: string) => { + const comp = component as any + comp.install = (app: any) => { + app.component(comp.name || comp.displayName, component) + if (alias) { + app.config.globalProperties[alias] = component + } + } + return component as T & Plugin +} + +/** + * @param str 需要转下划线的驼峰字符串 + * @returns 字符串下划线 + */ +export const humpToUnderline = (str: string): string => { + return str.replace(/([A-Z])/g, '-$1').toLowerCase() +} + +/** + * @param str 需要转驼峰的下划线字符串 + * @returns 字符串驼峰 + */ +export const underlineToHump = (str: string): string => { + if (!str) return '' + return str.replace(/\-(\w)/g, (_, letter: string) => { + return letter.toUpperCase() + }) +} + +/** + * 驼峰转横杠 + */ +export const humpToDash = (str: string): string => { + return str.replace(/([A-Z])/g, '-$1').toLowerCase() +} + +export const setCssVar = (prop: string, val: any, dom = document.documentElement) => { + dom.style.setProperty(prop, val) +} + +export const getCssVar = (prop: string, dom = document.documentElement) => { + return getComputedStyle(dom).getPropertyValue(prop) +} + +/** + * 查找数组对象的某个下标 + * @param {Array} ary 查找的数组 + * @param {Functon} fn 判断的方法 + */ +export const findIndex = (ary: Array, fn: Fn): number => { + if (ary.findIndex) { + return ary.findIndex(fn) + } + let index = -1 + ary.some((item: T, i: number, ary: Array) => { + const ret: T = fn(item, i, ary) + if (ret) { + index = i + return ret + } + }) + return index +} + +export const trim = (str: string) => { + return str.replace(/(^\s*)|(\s*$)/g, '') +} + +/** + * @param {Date | number | string} time 需要转换的时间 + * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss + */ +export function formatTime(time: Date | number | string, fmt: string) { + if (!time) return '' + else { + const date = new Date(time) + const o = { + 'M+': date.getMonth() + 1, + 'd+': date.getDate(), + 'H+': date.getHours(), + 'm+': date.getMinutes(), + 's+': date.getSeconds(), + 'q+': Math.floor((date.getMonth() + 3) / 3), + S: date.getMilliseconds() + } + if (/(y+)/.test(fmt)) { + fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) + } + for (const k in o) { + if (new RegExp('(' + k + ')').test(fmt)) { + fmt = fmt.replace( + RegExp.$1, + RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length) + ) + } + } + return fmt + } +} + +/** + * 生成随机字符串 + */ +export function toAnyString() { + const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => { + const r: number = (Math.random() * 16) | 0 + const v: number = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString() + }) + return str +} + +/** + * 首字母大写 + */ +export function firstUpperCase(str: string) { + return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) +} + +/** + * 把对象转为formData + */ +export function objToFormData(obj: Recordable) { + const formData = new FormData() + Object.keys(obj).forEach((key) => { + formData.append(key, obj[key]) + }) + return formData +} diff --git a/promo-ui2/src/utils/is.ts b/promo-ui2/src/utils/is.ts new file mode 100644 index 0000000..1651420 --- /dev/null +++ b/promo-ui2/src/utils/is.ts @@ -0,0 +1,117 @@ +// copy to vben-admin + +const toString = Object.prototype.toString + +export const is = (val: unknown, type: string) => { + return toString.call(val) === `[object ${type}]` +} + +export const isDef = (val?: T): val is T => { + return typeof val !== 'undefined' +} + +export const isUnDef = (val?: T): val is T => { + return !isDef(val) +} + +export const isObject = (val: any): val is Record => { + return val !== null && is(val, 'Object') +} + +export const isEmpty = (val: T): val is T => { + if (isArray(val) || isString(val)) { + return val.length === 0 + } + + if (val instanceof Map || val instanceof Set) { + return val.size === 0 + } + + if (isObject(val)) { + return Object.keys(val).length === 0 + } + + return false +} + +export const isDate = (val: unknown): val is Date => { + return is(val, 'Date') +} + +export const isNull = (val: unknown): val is null => { + return val === null +} + +export const isNullAndUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) && isNull(val) +} + +export const isNullOrUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) || isNull(val) +} + +export const isNumber = (val: unknown): val is number => { + return is(val, 'Number') +} + +export const isPromise = (val: unknown): val is Promise => { + return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch) +} + +export const isString = (val: unknown): val is string => { + return is(val, 'String') +} + +export const isFunction = (val: unknown): val is Function => { + return typeof val === 'function' +} + +export const isBoolean = (val: unknown): val is boolean => { + return is(val, 'Boolean') +} + +export const isRegExp = (val: unknown): val is RegExp => { + return is(val, 'RegExp') +} + +export const isArray = (val: any): val is Array => { + return val && Array.isArray(val) +} + +export const isWindow = (val: any): val is Window => { + return typeof window !== 'undefined' && is(val, 'Window') +} + +export const isElement = (val: unknown): val is Element => { + return isObject(val) && !!val.tagName +} + +export const isMap = (val: unknown): val is Map => { + return is(val, 'Map') +} + +export const isServer = typeof window === 'undefined' + +export const isClient = !isServer + +export const isUrl = (path: string): boolean => { + try { + new URL(path) + return true + } catch (_error) { + return false + } +} + +export const isDark = (): boolean => { + return window.matchMedia('(prefers-color-scheme: dark)').matches +} + +// 是否是图片链接 +export const isImgPath = (path: string): boolean => { + return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path) +} + +export const isEmptyVal = (val: any): boolean => { + return val === '' || val === null || val === undefined +} diff --git a/promo-ui2/src/utils/propTypes.ts b/promo-ui2/src/utils/propTypes.ts new file mode 100644 index 0000000..863f55c --- /dev/null +++ b/promo-ui2/src/utils/propTypes.ts @@ -0,0 +1,24 @@ +import { VueTypeValidableDef, VueTypesInterface, createTypes, toValidableType } from 'vue-types' +import { CSSProperties } from 'vue' + +type PropTypes = VueTypesInterface & { + readonly style: VueTypeValidableDef +} +const newPropTypes = createTypes({ + func: undefined, + bool: undefined, + string: undefined, + number: undefined, + object: undefined, + integer: undefined +}) as PropTypes + +class propTypes extends newPropTypes { + static get style() { + return toValidableType('style', { + type: [String, Object] + }) + } +} + +export { propTypes } diff --git a/promo-ui2/src/utils/routerHelper.ts b/promo-ui2/src/utils/routerHelper.ts new file mode 100644 index 0000000..5863341 --- /dev/null +++ b/promo-ui2/src/utils/routerHelper.ts @@ -0,0 +1,225 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import type { + Router, + RouteLocationNormalized, + RouteRecordNormalized, + RouteRecordRaw +} from 'vue-router' +import { isUrl } from '@/utils/is' +import { omit, cloneDeep } from 'lodash-es' + +const modules = import.meta.glob('../views/**/*.{vue,tsx}') + +/* Layout */ +export const Layout = () => import('@/layout/Layout.vue') + +export const getParentLayout = () => { + return () => + new Promise((resolve) => { + resolve({ + name: 'ParentLayout' + }) + }) +} + +export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormalized => { + if (!route) return route + const { matched, ...opt } = route + return { + ...opt, + matched: (matched + ? matched.map((item) => ({ + meta: item.meta, + name: item.name, + path: item.path + })) + : undefined) as RouteRecordNormalized[] + } +} + +// 前端控制路由生成(按keys顺序排序) +export const generateRoutesByFrontEnd = ( + routes: AppRouteRecordRaw[], + keys: string[], + basePath = '/' +): AppRouteRecordRaw[] => { + const res: AppRouteRecordRaw[] = [] + + for (const route of routes) { + const meta = route.meta ?? {} + // skip some route + if (meta.hidden && !meta.canTo) { + continue + } + + let data: Nullable = null + + let onlyOneChild: Nullable = null + if (route.children && route.children.length === 1 && !meta.alwaysShow) { + onlyOneChild = ( + isUrl(route.children[0].path) + ? route.children[0].path + : pathResolve(pathResolve(basePath, route.path), route.children[0].path) + ) as string + } + + // 开发者可以根据实际情况进行扩展 + for (const item of keys) { + // 通过路径去匹配 + if (isUrl(item) && (onlyOneChild === item || route.path === item)) { + data = Object.assign({}, route) + } else { + const routePath = (onlyOneChild ?? pathResolve(basePath, route.path)).trim() + if (routePath === item || meta.followRoute === item) { + data = Object.assign({}, route) + } + } + } + + // recursive child routes + if (route.children && data) { + data.children = generateRoutesByFrontEnd( + route.children, + keys, + pathResolve(basePath, data.path) + ) + } + if (data) { + res.push(data as AppRouteRecordRaw) + } + } + + // 按keys顺序排序(后端返回的顺序即管理员配置的排序) + res.sort((a, b) => { + const aPath = + a.children && a.children.length === 1 + ? pathResolve(pathResolve(basePath, a.path), a.children[0].path) + : pathResolve(basePath, a.path) + const bPath = + b.children && b.children.length === 1 + ? pathResolve(pathResolve(basePath, b.path), b.children[0].path) + : pathResolve(basePath, b.path) + const aIndex = keys.indexOf(aPath.trim()) + const bIndex = keys.indexOf(bPath.trim()) + return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex) + }) + + return res +} + +// 后端控制路由生成 +export const generateRoutesByServer = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => { + const res: AppRouteRecordRaw[] = [] + + for (const route of routes) { + const data: AppRouteRecordRaw = { + path: route.path, + name: route.name, + redirect: route.redirect, + meta: route.meta + } + if (route.component) { + const comModule = modules[`../${route.component}.vue`] || modules[`../${route.component}.tsx`] + const component = route.component as string + if (!comModule && !component.includes('#')) { + console.error(`未找到${route.component}.vue文件或${route.component}.tsx文件,请创建`) + } else { + // 动态加载路由文件,可根据实际情况进行自定义逻辑 + data.component = + component === '#' ? Layout : component.includes('##') ? getParentLayout() : comModule + } + } + // recursive child routes + if (route.children) { + data.children = generateRoutesByServer(route.children) + } + res.push(data as AppRouteRecordRaw) + } + return res +} + +export const pathResolve = (parentPath: string, path: string) => { + if (isUrl(path)) return path + const childPath = path.startsWith('/') || !path ? path : `/${path}` + return `${parentPath}${childPath}`.replace(/\/\//g, '/').trim() +} + +/** + * 跳转目标是否真能解析出路由(登录后 redirect、守卫跳转前统一用它过滤)。 + * '/'、'/404'、'/login' 和 404 兜底路由一律不算,否则会直接撞上 404 兜底。 + */ +export const isRealRoute = (router: Router, path?: unknown): path is string => + typeof path === 'string' && + !!path && + path !== '/' && + path !== '/404' && + !path.startsWith('/login') && + router.resolve(path).matched.some((m) => m.name !== 'NoFind' && m.name !== '404Page') + +// 路由降级 +export const flatMultiLevelRoutes = (routes: AppRouteRecordRaw[]) => { + const modules: AppRouteRecordRaw[] = cloneDeep(routes) + for (let index = 0; index < modules.length; index++) { + const route = modules[index] + if (!isMultipleRoute(route)) { + continue + } + promoteRouteLevel(route) + } + return modules +} + +// 层级是否大于2 +const isMultipleRoute = (route: AppRouteRecordRaw) => { + if (!route || !Reflect.has(route, 'children') || !route.children?.length) { + return false + } + + const children = route.children + + let flag = false + for (let index = 0; index < children.length; index++) { + const child = children[index] + if (child.children?.length) { + flag = true + break + } + } + return flag +} + +// 生成二级路由 +const promoteRouteLevel = (route: AppRouteRecordRaw) => { + let router: Router | null = createRouter({ + routes: [route as RouteRecordRaw], + history: createWebHashHistory() + }) + + const routes = router.getRoutes() + addToChildren(routes, route.children || [], route) + router = null + + route.children = route.children?.map((item) => omit(item, 'children')) +} + +// 添加所有子菜单 +const addToChildren = ( + routes: RouteRecordNormalized[], + children: AppRouteRecordRaw[], + routeModule: AppRouteRecordRaw +) => { + for (let index = 0; index < children.length; index++) { + const child = children[index] + const route = routes.find((item) => item.name === child.name) + if (!route) { + continue + } + routeModule.children = routeModule.children || [] + if (!routeModule.children.find((item) => item.name === route.name)) { + routeModule.children?.push(route as unknown as AppRouteRecordRaw) + } + if (child.children?.length) { + addToChildren(routes, child.children, routeModule) + } + } +} diff --git a/promo-ui2/src/utils/tree.ts b/promo-ui2/src/utils/tree.ts new file mode 100644 index 0000000..fe0207d --- /dev/null +++ b/promo-ui2/src/utils/tree.ts @@ -0,0 +1,207 @@ +interface TreeHelperConfig { + id: string + children: string + pid: string +} +const DEFAULT_CONFIG: TreeHelperConfig = { + id: 'id', + children: 'children', + pid: 'pid' +} + +const getConfig = (config: Partial) => Object.assign({}, DEFAULT_CONFIG, config) + +// tree from list +export const listToTree = (list: any[], config: Partial = {}): T[] => { + const conf = getConfig(config) as TreeHelperConfig + const nodeMap = new Map() + const result: T[] = [] + const { id, children, pid } = conf + + for (const node of list) { + node[children] = node[children] || [] + nodeMap.set(node[id], node) + } + for (const node of list) { + const parent = nodeMap.get(node[pid]) + ;(parent ? parent.children : result).push(node) + } + return result +} + +export const treeToList = (tree: any, config: Partial = {}): T => { + config = getConfig(config) + const { children } = config + const result: any = [...tree] + for (let i = 0; i < result.length; i++) { + if (!result[i][children!]) continue + result.splice(i + 1, 0, ...result[i][children!]) + } + return result +} + +export const findNode = ( + tree: any, + func: Fn, + config: Partial = {} +): T | null => { + config = getConfig(config) + const { children } = config + const list = [...tree] + for (const node of list) { + if (func(node)) return node + node[children!] && list.push(...node[children!]) + } + return null +} + +export const findNodeAll = ( + tree: any, + func: Fn, + config: Partial = {} +): T[] => { + config = getConfig(config) + const { children } = config + const list = [...tree] + const result: T[] = [] + for (const node of list) { + func(node) && result.push(node) + node[children!] && list.push(...node[children!]) + } + return result +} + +export const findPath = ( + tree: any, + func: Fn, + config: Partial = {} +): T | T[] | null => { + config = getConfig(config) + const path: T[] = [] + const list = [...tree] + const visitedSet = new Set() + const { children } = config + while (list.length) { + const node = list[0] + if (visitedSet.has(node)) { + path.pop() + list.shift() + } else { + visitedSet.add(node) + node[children!] && list.unshift(...node[children!]) + path.push(node) + if (func(node)) { + return path + } + } + } + return null +} + +export const findPathAll = (tree: any, func: Fn, config: Partial = {}) => { + config = getConfig(config) + const path: any[] = [] + const list = [...tree] + const result: any[] = [] + const visitedSet = new Set(), + { children } = config + while (list.length) { + const node = list[0] + if (visitedSet.has(node)) { + path.pop() + list.shift() + } else { + visitedSet.add(node) + node[children!] && list.unshift(...node[children!]) + path.push(node) + func(node) && result.push([...path]) + } + } + return result +} + +export const filter = ( + tree: T[], + func: (n: T) => boolean, + config: Partial = {} +): T[] => { + config = getConfig(config) + const children = config.children as string + function listFilter(list: T[]) { + return list + .map((node: any) => ({ ...node })) + .filter((node) => { + node[children] = node[children] && listFilter(node[children]) + return func(node) || (node[children] && node[children].length) + }) + } + return listFilter(tree) +} + +export const forEach = ( + tree: T[], + func: (n: T) => any, + config: Partial = {} +): void => { + config = getConfig(config) + const list: any[] = [...tree] + const { children } = config + for (let i = 0; i < list.length; i++) { + // func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿 + if (func(list[i])) { + return + } + children && list[i][children] && list.splice(i + 1, 0, ...list[i][children]) + } +} + +/** + * @description: Extract tree specified structure + */ +export const treeMap = ( + treeData: T[], + opt: { children?: string; conversion: Fn } +): T[] => { + return treeData.map((item) => treeMapEach(item, opt)) +} + +/** + * @description: Extract tree specified structure + */ +export const treeMapEach = ( + data: any, + { children = 'children', conversion }: { children?: string; conversion: Fn } +) => { + const haveChildren = Array.isArray(data[children]) && data[children].length > 0 + const conversionData = conversion(data) || {} + if (haveChildren) { + return { + ...conversionData, + [children]: data[children].map((i: number) => + treeMapEach(i, { + children, + conversion + }) + ) + } + } else { + return { + ...conversionData + } + } +} + +/** + * 递归遍历树结构 + * @param treeDatas 树 + * @param callBack 回调 + * @param parentNode 父节点 + */ +export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => { + treeDatas.forEach((element) => { + const newNode = callBack(element, parentNode) || element + if (element.children) { + eachTree(element.children, callBack, newNode) + } + }) +} diff --git a/promo-ui2/src/utils/tsxHelper.ts b/promo-ui2/src/utils/tsxHelper.ts new file mode 100644 index 0000000..6087fa3 --- /dev/null +++ b/promo-ui2/src/utils/tsxHelper.ts @@ -0,0 +1,16 @@ +import { Slots } from 'vue' +import { isFunction } from '@/utils/is' + +export const getSlot = (slots: Slots, slot = 'default', data?: Recordable) => { + // Reflect.has 判断一个对象是否存在某个属性 + if (!slots || !Reflect.has(slots, slot)) { + return null + } + if (!isFunction(slots[slot])) { + console.error(`${slot} is not a function!`) + return null + } + const slotFn = slots[slot] + if (!slotFn) return null + return slotFn(data) +} diff --git a/promo-ui2/src/views/AdminManage/index.vue b/promo-ui2/src/views/AdminManage/index.vue new file mode 100644 index 0000000..ecdb136 --- /dev/null +++ b/promo-ui2/src/views/AdminManage/index.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/promo-ui2/src/views/AnchorManage/index.vue b/promo-ui2/src/views/AnchorManage/index.vue new file mode 100644 index 0000000..c513935 --- /dev/null +++ b/promo-ui2/src/views/AnchorManage/index.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/promo-ui2/src/views/Authorization/Department/Department.vue b/promo-ui2/src/views/Authorization/Department/Department.vue new file mode 100644 index 0000000..c889705 --- /dev/null +++ b/promo-ui2/src/views/Authorization/Department/Department.vue @@ -0,0 +1,341 @@ + + + diff --git a/promo-ui2/src/views/Authorization/Department/components/Detail.vue b/promo-ui2/src/views/Authorization/Department/components/Detail.vue new file mode 100644 index 0000000..8262b95 --- /dev/null +++ b/promo-ui2/src/views/Authorization/Department/components/Detail.vue @@ -0,0 +1,20 @@ + + + diff --git a/promo-ui2/src/views/Authorization/Department/components/Write.vue b/promo-ui2/src/views/Authorization/Department/components/Write.vue new file mode 100644 index 0000000..2c3c527 --- /dev/null +++ b/promo-ui2/src/views/Authorization/Department/components/Write.vue @@ -0,0 +1,59 @@ + + + diff --git a/promo-ui2/src/views/Authorization/Menu/Menu.vue b/promo-ui2/src/views/Authorization/Menu/Menu.vue new file mode 100644 index 0000000..1ea4fb3 --- /dev/null +++ b/promo-ui2/src/views/Authorization/Menu/Menu.vue @@ -0,0 +1,212 @@ + + + diff --git a/promo-ui2/src/views/Authorization/Menu/components/AddButtonPermission.vue b/promo-ui2/src/views/Authorization/Menu/components/AddButtonPermission.vue new file mode 100644 index 0000000..32cb3c3 --- /dev/null +++ b/promo-ui2/src/views/Authorization/Menu/components/AddButtonPermission.vue @@ -0,0 +1,68 @@ + + + diff --git a/promo-ui2/src/views/Authorization/Menu/components/Detail.vue b/promo-ui2/src/views/Authorization/Menu/components/Detail.vue new file mode 100644 index 0000000..54b2cad --- /dev/null +++ b/promo-ui2/src/views/Authorization/Menu/components/Detail.vue @@ -0,0 +1,173 @@ + + + diff --git a/promo-ui2/src/views/Authorization/Menu/components/Write.vue b/promo-ui2/src/views/Authorization/Menu/components/Write.vue new file mode 100644 index 0000000..2da8b67 --- /dev/null +++ b/promo-ui2/src/views/Authorization/Menu/components/Write.vue @@ -0,0 +1,409 @@ + + + diff --git a/promo-ui2/src/views/Authorization/Role/Role.vue b/promo-ui2/src/views/Authorization/Role/Role.vue new file mode 100644 index 0000000..4b67c3f --- /dev/null +++ b/promo-ui2/src/views/Authorization/Role/Role.vue @@ -0,0 +1,173 @@ + + + diff --git a/promo-ui2/src/views/Authorization/Role/components/Detail.vue b/promo-ui2/src/views/Authorization/Role/components/Detail.vue new file mode 100644 index 0000000..97c1b57 --- /dev/null +++ b/promo-ui2/src/views/Authorization/Role/components/Detail.vue @@ -0,0 +1,106 @@ + + + diff --git a/promo-ui2/src/views/Authorization/Role/components/Write.vue b/promo-ui2/src/views/Authorization/Role/components/Write.vue new file mode 100644 index 0000000..0169427 --- /dev/null +++ b/promo-ui2/src/views/Authorization/Role/components/Write.vue @@ -0,0 +1,181 @@ + + + diff --git a/promo-ui2/src/views/Authorization/User/User.vue b/promo-ui2/src/views/Authorization/User/User.vue new file mode 100644 index 0000000..12ad2e2 --- /dev/null +++ b/promo-ui2/src/views/Authorization/User/User.vue @@ -0,0 +1,384 @@ + + + diff --git a/promo-ui2/src/views/Authorization/User/components/Detail.vue b/promo-ui2/src/views/Authorization/User/components/Detail.vue new file mode 100644 index 0000000..81dd5ee --- /dev/null +++ b/promo-ui2/src/views/Authorization/User/components/Detail.vue @@ -0,0 +1,20 @@ + + + diff --git a/promo-ui2/src/views/Authorization/User/components/Write.vue b/promo-ui2/src/views/Authorization/User/components/Write.vue new file mode 100644 index 0000000..ec60c8a --- /dev/null +++ b/promo-ui2/src/views/Authorization/User/components/Write.vue @@ -0,0 +1,60 @@ + + + diff --git a/promo-ui2/src/views/BusinessManage/index.vue b/promo-ui2/src/views/BusinessManage/index.vue new file mode 100644 index 0000000..50a8a94 --- /dev/null +++ b/promo-ui2/src/views/BusinessManage/index.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/promo-ui2/src/views/CardMerchant/index.vue b/promo-ui2/src/views/CardMerchant/index.vue new file mode 100644 index 0000000..d29d49f --- /dev/null +++ b/promo-ui2/src/views/CardMerchant/index.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/promo-ui2/src/views/CardMerchantManage/index.vue b/promo-ui2/src/views/CardMerchantManage/index.vue new file mode 100644 index 0000000..e3f45c9 --- /dev/null +++ b/promo-ui2/src/views/CardMerchantManage/index.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/promo-ui2/src/views/CdkeyCreate/index.vue b/promo-ui2/src/views/CdkeyCreate/index.vue new file mode 100644 index 0000000..052c754 --- /dev/null +++ b/promo-ui2/src/views/CdkeyCreate/index.vue @@ -0,0 +1,223 @@ + + + + + diff --git a/promo-ui2/src/views/CdkeyCreateDaily/index.vue b/promo-ui2/src/views/CdkeyCreateDaily/index.vue new file mode 100644 index 0000000..01b8437 --- /dev/null +++ b/promo-ui2/src/views/CdkeyCreateDaily/index.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/promo-ui2/src/views/CdkeyRedeem/index.vue b/promo-ui2/src/views/CdkeyRedeem/index.vue new file mode 100644 index 0000000..f7c44c4 --- /dev/null +++ b/promo-ui2/src/views/CdkeyRedeem/index.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/promo-ui2/src/views/CdkeyRedeemDaily/index.vue b/promo-ui2/src/views/CdkeyRedeemDaily/index.vue new file mode 100644 index 0000000..39f06b5 --- /dev/null +++ b/promo-ui2/src/views/CdkeyRedeemDaily/index.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/promo-ui2/src/views/CommissionReport/index.vue b/promo-ui2/src/views/CommissionReport/index.vue new file mode 100644 index 0000000..da4df38 --- /dev/null +++ b/promo-ui2/src/views/CommissionReport/index.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/promo-ui2/src/views/Components/Avatars.vue b/promo-ui2/src/views/Components/Avatars.vue new file mode 100644 index 0000000..430c758 --- /dev/null +++ b/promo-ui2/src/views/Components/Avatars.vue @@ -0,0 +1,53 @@ + + + diff --git a/promo-ui2/src/views/Components/CountTo.vue b/promo-ui2/src/views/Components/CountTo.vue new file mode 100644 index 0000000..a547e8e --- /dev/null +++ b/promo-ui2/src/views/Components/CountTo.vue @@ -0,0 +1,100 @@ + + + diff --git a/promo-ui2/src/views/Components/Descriptions.vue b/promo-ui2/src/views/Components/Descriptions.vue new file mode 100644 index 0000000..4b96bf6 --- /dev/null +++ b/promo-ui2/src/views/Components/Descriptions.vue @@ -0,0 +1,192 @@ + + + + + diff --git a/promo-ui2/src/views/Components/Dialog.vue b/promo-ui2/src/views/Components/Dialog.vue new file mode 100644 index 0000000..33726e4 --- /dev/null +++ b/promo-ui2/src/views/Components/Dialog.vue @@ -0,0 +1,165 @@ + + + diff --git a/promo-ui2/src/views/Components/Echart.vue b/promo-ui2/src/views/Components/Echart.vue new file mode 100644 index 0000000..683d6b4 --- /dev/null +++ b/promo-ui2/src/views/Components/Echart.vue @@ -0,0 +1,36 @@ + + + diff --git a/promo-ui2/src/views/Components/Editor/CodeEditor.vue b/promo-ui2/src/views/Components/Editor/CodeEditor.vue new file mode 100644 index 0000000..db80bb0 --- /dev/null +++ b/promo-ui2/src/views/Components/Editor/CodeEditor.vue @@ -0,0 +1,23 @@ + + diff --git a/promo-ui2/src/views/Components/Editor/Editor.vue b/promo-ui2/src/views/Components/Editor/Editor.vue new file mode 100644 index 0000000..9dde431 --- /dev/null +++ b/promo-ui2/src/views/Components/Editor/Editor.vue @@ -0,0 +1,32 @@ + + + diff --git a/promo-ui2/src/views/Components/Editor/JsonEditor.vue b/promo-ui2/src/views/Components/Editor/JsonEditor.vue new file mode 100644 index 0000000..c508fbd --- /dev/null +++ b/promo-ui2/src/views/Components/Editor/JsonEditor.vue @@ -0,0 +1,36 @@ + + + diff --git a/promo-ui2/src/views/Components/Form/DefaultForm.vue b/promo-ui2/src/views/Components/Form/DefaultForm.vue new file mode 100644 index 0000000..4165c16 --- /dev/null +++ b/promo-ui2/src/views/Components/Form/DefaultForm.vue @@ -0,0 +1,1863 @@ + + + + + diff --git a/promo-ui2/src/views/Components/Form/UseFormDemo.vue b/promo-ui2/src/views/Components/Form/UseFormDemo.vue new file mode 100644 index 0000000..de9cc64 --- /dev/null +++ b/promo-ui2/src/views/Components/Form/UseFormDemo.vue @@ -0,0 +1,470 @@ + + + + + diff --git a/promo-ui2/src/views/Components/Highlight.vue b/promo-ui2/src/views/Components/Highlight.vue new file mode 100644 index 0000000..96baedc --- /dev/null +++ b/promo-ui2/src/views/Components/Highlight.vue @@ -0,0 +1,20 @@ + + + diff --git a/promo-ui2/src/views/Components/IAgree.vue b/promo-ui2/src/views/Components/IAgree.vue new file mode 100644 index 0000000..947226b --- /dev/null +++ b/promo-ui2/src/views/Components/IAgree.vue @@ -0,0 +1,21 @@ + + + diff --git a/promo-ui2/src/views/Components/Icon.vue b/promo-ui2/src/views/Components/Icon.vue new file mode 100644 index 0000000..02da12a --- /dev/null +++ b/promo-ui2/src/views/Components/Icon.vue @@ -0,0 +1,61 @@ + + + diff --git a/promo-ui2/src/views/Components/IconPicker.vue b/promo-ui2/src/views/Components/IconPicker.vue new file mode 100644 index 0000000..6643f94 --- /dev/null +++ b/promo-ui2/src/views/Components/IconPicker.vue @@ -0,0 +1,16 @@ + + + diff --git a/promo-ui2/src/views/Components/ImageCropping.vue b/promo-ui2/src/views/Components/ImageCropping.vue new file mode 100644 index 0000000..9e69952 --- /dev/null +++ b/promo-ui2/src/views/Components/ImageCropping.vue @@ -0,0 +1,43 @@ + + + diff --git a/promo-ui2/src/views/Components/ImageViewer.vue b/promo-ui2/src/views/Components/ImageViewer.vue new file mode 100644 index 0000000..a20ce91 --- /dev/null +++ b/promo-ui2/src/views/Components/ImageViewer.vue @@ -0,0 +1,29 @@ + + + diff --git a/promo-ui2/src/views/Components/Infotip.vue b/promo-ui2/src/views/Components/Infotip.vue new file mode 100644 index 0000000..49cff12 --- /dev/null +++ b/promo-ui2/src/views/Components/Infotip.vue @@ -0,0 +1,33 @@ + + + diff --git a/promo-ui2/src/views/Components/InputPassword.vue b/promo-ui2/src/views/Components/InputPassword.vue new file mode 100644 index 0000000..bfae7e5 --- /dev/null +++ b/promo-ui2/src/views/Components/InputPassword.vue @@ -0,0 +1,21 @@ + + + diff --git a/promo-ui2/src/views/Components/Qrcode.vue b/promo-ui2/src/views/Components/Qrcode.vue new file mode 100644 index 0000000..6b7d792 --- /dev/null +++ b/promo-ui2/src/views/Components/Qrcode.vue @@ -0,0 +1,108 @@ + + + diff --git a/promo-ui2/src/views/Components/Search.vue b/promo-ui2/src/views/Components/Search.vue new file mode 100644 index 0000000..0cc9154 --- /dev/null +++ b/promo-ui2/src/views/Components/Search.vue @@ -0,0 +1,363 @@ + + + + + diff --git a/promo-ui2/src/views/Components/Table/CardTable.vue b/promo-ui2/src/views/Components/Table/CardTable.vue new file mode 100644 index 0000000..f07059d --- /dev/null +++ b/promo-ui2/src/views/Components/Table/CardTable.vue @@ -0,0 +1,80 @@ + + + diff --git a/promo-ui2/src/views/Components/Table/DefaultTable.vue b/promo-ui2/src/views/Components/Table/DefaultTable.vue new file mode 100644 index 0000000..1d9124a --- /dev/null +++ b/promo-ui2/src/views/Components/Table/DefaultTable.vue @@ -0,0 +1,105 @@ + + + diff --git a/promo-ui2/src/views/Components/Table/TableImagePreview.vue b/promo-ui2/src/views/Components/Table/TableImagePreview.vue new file mode 100644 index 0000000..ded2d6d --- /dev/null +++ b/promo-ui2/src/views/Components/Table/TableImagePreview.vue @@ -0,0 +1,87 @@ + + + diff --git a/promo-ui2/src/views/Components/Table/TableVideoPreview.vue b/promo-ui2/src/views/Components/Table/TableVideoPreview.vue new file mode 100644 index 0000000..a6607b0 --- /dev/null +++ b/promo-ui2/src/views/Components/Table/TableVideoPreview.vue @@ -0,0 +1,71 @@ + + + diff --git a/promo-ui2/src/views/Components/Table/TreeTable.vue b/promo-ui2/src/views/Components/Table/TreeTable.vue new file mode 100644 index 0000000..90a1dfb --- /dev/null +++ b/promo-ui2/src/views/Components/Table/TreeTable.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/promo-ui2/src/views/Components/Table/UseTableDemo.vue b/promo-ui2/src/views/Components/Table/UseTableDemo.vue new file mode 100644 index 0000000..62e9801 --- /dev/null +++ b/promo-ui2/src/views/Components/Table/UseTableDemo.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/promo-ui2/src/views/Components/Tree.vue b/promo-ui2/src/views/Components/Tree.vue new file mode 100644 index 0000000..661732e --- /dev/null +++ b/promo-ui2/src/views/Components/Tree.vue @@ -0,0 +1,252 @@ + + + + diff --git a/promo-ui2/src/views/Components/VideoPlayer.vue b/promo-ui2/src/views/Components/VideoPlayer.vue new file mode 100644 index 0000000..66dcad4 --- /dev/null +++ b/promo-ui2/src/views/Components/VideoPlayer.vue @@ -0,0 +1,24 @@ + + + diff --git a/promo-ui2/src/views/Components/Waterfall.vue b/promo-ui2/src/views/Components/Waterfall.vue new file mode 100644 index 0000000..c56f8f8 --- /dev/null +++ b/promo-ui2/src/views/Components/Waterfall.vue @@ -0,0 +1,62 @@ + + + diff --git a/promo-ui2/src/views/Dashboard/Analysis.vue b/promo-ui2/src/views/Dashboard/Analysis.vue new file mode 100644 index 0000000..3155f7d --- /dev/null +++ b/promo-ui2/src/views/Dashboard/Analysis.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/promo-ui2/src/views/Dashboard/Workplace.vue b/promo-ui2/src/views/Dashboard/Workplace.vue new file mode 100644 index 0000000..4e0bd80 --- /dev/null +++ b/promo-ui2/src/views/Dashboard/Workplace.vue @@ -0,0 +1,293 @@ + + + diff --git a/promo-ui2/src/views/Dashboard/components/PanelGroup.vue b/promo-ui2/src/views/Dashboard/components/PanelGroup.vue new file mode 100644 index 0000000..f10c935 --- /dev/null +++ b/promo-ui2/src/views/Dashboard/components/PanelGroup.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/promo-ui2/src/views/Dashboard/echarts-data.ts b/promo-ui2/src/views/Dashboard/echarts-data.ts new file mode 100644 index 0000000..aea1dbb --- /dev/null +++ b/promo-ui2/src/views/Dashboard/echarts-data.ts @@ -0,0 +1,296 @@ +import { EChartsOption } from 'echarts' +import { useI18n } from '@/hooks/web/useI18n' + +const { t } = useI18n() + +export const lineOptions: EChartsOption = { + xAxis: { + data: [], + boundaryGap: false, + axisTick: { + show: false + }, + axisLabel: { + // 首尾日期以刻度点居中,半个标签会溢出画布,靠 grid.right 留出空间 + hideOverlap: true, + margin: 12 + } + }, + grid: { + left: 20, + right: 48, + bottom: 20, + top: 30, + containLabel: true + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross' + }, + padding: [5, 10] + }, + yAxis: { + axisTick: { + show: false + } + }, + legend: { + data: ['日流水', '日邀请'] + }, + series: [ + { + name: '日流水', + smooth: true, + type: 'line', + data: [], + animationDuration: 2800, + animationEasing: 'cubicInOut' + }, + { + name: '日邀请', + smooth: true, + type: 'line', + itemStyle: {}, + data: [], + animationDuration: 2800, + animationEasing: 'quadraticOut' + } + ] +} + +export const pieOptions: EChartsOption = { + title: { + text: t('analysis.userAccessSource'), + left: 'center' + }, + tooltip: { + trigger: 'item', + formatter: '{a}
{b} : {c} ({d}%)' + }, + legend: { + orient: 'vertical', + left: 'left', + data: [ + t('analysis.directAccess'), + t('analysis.mailMarketing'), + t('analysis.allianceAdvertising'), + t('analysis.videoAdvertising'), + t('analysis.searchEngines') + ] + }, + series: [ + { + name: t('analysis.userAccessSource'), + type: 'pie', + radius: '55%', + center: ['50%', '60%'], + data: [ + { value: 335, name: t('analysis.directAccess') }, + { value: 310, name: t('analysis.mailMarketing') }, + { value: 234, name: t('analysis.allianceAdvertising') }, + { value: 135, name: t('analysis.videoAdvertising') }, + { value: 1548, name: t('analysis.searchEngines') } + ] + } + ] +} + +export const barOptions: EChartsOption = { + title: { + text: t('analysis.weeklyUserActivity'), + left: 'center' + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + } + }, + grid: { + left: 50, + right: 20, + bottom: 20 + }, + xAxis: { + type: 'category', + data: [ + t('analysis.monday'), + t('analysis.tuesday'), + t('analysis.wednesday'), + t('analysis.thursday'), + t('analysis.friday'), + t('analysis.saturday'), + t('analysis.sunday') + ], + axisTick: { + alignWithLabel: true + } + }, + yAxis: { + type: 'value' + }, + series: [ + { + name: t('analysis.activeQuantity'), + data: [13253, 34235, 26321, 12340, 24643, 1322, 1324], + type: 'bar' + } + ] +} + +export const radarOption: EChartsOption = { + legend: { + data: [t('workplace.personal'), t('workplace.team')] + }, + radar: { + // shape: 'circle', + indicator: [ + { name: t('workplace.quote'), max: 65 }, + { name: t('workplace.contribution'), max: 160 }, + { name: t('workplace.hot'), max: 300 }, + { name: t('workplace.yield'), max: 130 }, + { name: t('workplace.follow'), max: 100 } + ] + }, + series: [ + { + name: `xxx${t('workplace.index')}`, + type: 'radar', + data: [ + { + value: [42, 30, 20, 35, 80], + name: t('workplace.personal') + }, + { + value: [50, 140, 290, 100, 90], + name: t('workplace.team') + } + ] + } + ] +} + +export const wordOptions = { + series: [ + { + type: 'wordCloud', + gridSize: 2, + sizeRange: [12, 50], + rotationRange: [-90, 90], + shape: 'pentagon', + width: 600, + height: 400, + drawOutOfBound: true, + textStyle: { + color: function () { + return ( + 'rgb(' + + [ + Math.round(Math.random() * 160), + Math.round(Math.random() * 160), + Math.round(Math.random() * 160) + ].join(',') + + ')' + ) + } + }, + emphasis: { + textStyle: { + shadowBlur: 10, + shadowColor: '#333' + } + }, + data: [ + { + name: 'Sam S Club', + value: 10000, + textStyle: { + color: 'black' + }, + emphasis: { + textStyle: { + color: 'red' + } + } + }, + { + name: 'Macys', + value: 6181 + }, + { + name: 'Amy Schumer', + value: 4386 + }, + { + name: 'Jurassic World', + value: 4055 + }, + { + name: 'Charter Communications', + value: 2467 + }, + { + name: 'Chick Fil A', + value: 2244 + }, + { + name: 'Planet Fitness', + value: 1898 + }, + { + name: 'Pitch Perfect', + value: 1484 + }, + { + name: 'Express', + value: 1112 + }, + { + name: 'Home', + value: 965 + }, + { + name: 'Johnny Depp', + value: 847 + }, + { + name: 'Lena Dunham', + value: 582 + }, + { + name: 'Lewis Hamilton', + value: 555 + }, + { + name: 'KXAN', + value: 550 + }, + { + name: 'Mary Ellen Mark', + value: 462 + }, + { + name: 'Farrah Abraham', + value: 366 + }, + { + name: 'Rita Ora', + value: 360 + }, + { + name: 'Serena Williams', + value: 282 + }, + { + name: 'NCAA baseball tournament', + value: 273 + }, + { + name: 'Point Break', + value: 265 + } + ] + } + ] +} diff --git a/promo-ui2/src/views/Error/403.vue b/promo-ui2/src/views/Error/403.vue new file mode 100644 index 0000000..615c940 --- /dev/null +++ b/promo-ui2/src/views/Error/403.vue @@ -0,0 +1,17 @@ + + + diff --git a/promo-ui2/src/views/Error/404.vue b/promo-ui2/src/views/Error/404.vue new file mode 100644 index 0000000..0a3fbbe --- /dev/null +++ b/promo-ui2/src/views/Error/404.vue @@ -0,0 +1,17 @@ + + + diff --git a/promo-ui2/src/views/Error/500.vue b/promo-ui2/src/views/Error/500.vue new file mode 100644 index 0000000..ddb5097 --- /dev/null +++ b/promo-ui2/src/views/Error/500.vue @@ -0,0 +1,17 @@ + + + diff --git a/promo-ui2/src/views/Example/Dialog/ExampleDialog.vue b/promo-ui2/src/views/Example/Dialog/ExampleDialog.vue new file mode 100644 index 0000000..b93450d --- /dev/null +++ b/promo-ui2/src/views/Example/Dialog/ExampleDialog.vue @@ -0,0 +1,340 @@ + + + diff --git a/promo-ui2/src/views/Example/Dialog/components/Detail.vue b/promo-ui2/src/views/Example/Dialog/components/Detail.vue new file mode 100644 index 0000000..2496e75 --- /dev/null +++ b/promo-ui2/src/views/Example/Dialog/components/Detail.vue @@ -0,0 +1,20 @@ + + + diff --git a/promo-ui2/src/views/Example/Dialog/components/Write.vue b/promo-ui2/src/views/Example/Dialog/components/Write.vue new file mode 100644 index 0000000..2ea183a --- /dev/null +++ b/promo-ui2/src/views/Example/Dialog/components/Write.vue @@ -0,0 +1,63 @@ + + + diff --git a/promo-ui2/src/views/Example/Page/ExampleAdd.vue b/promo-ui2/src/views/Example/Page/ExampleAdd.vue new file mode 100644 index 0000000..fbe7460 --- /dev/null +++ b/promo-ui2/src/views/Example/Page/ExampleAdd.vue @@ -0,0 +1,51 @@ + + + diff --git a/promo-ui2/src/views/Example/Page/ExampleDetail.vue b/promo-ui2/src/views/Example/Page/ExampleDetail.vue new file mode 100644 index 0000000..6f7e44c --- /dev/null +++ b/promo-ui2/src/views/Example/Page/ExampleDetail.vue @@ -0,0 +1,37 @@ + + + diff --git a/promo-ui2/src/views/Example/Page/ExampleEdit.vue b/promo-ui2/src/views/Example/Page/ExampleEdit.vue new file mode 100644 index 0000000..eb65721 --- /dev/null +++ b/promo-ui2/src/views/Example/Page/ExampleEdit.vue @@ -0,0 +1,66 @@ + + + +@/hooks/event/useEventBus diff --git a/promo-ui2/src/views/Example/Page/ExamplePage.vue b/promo-ui2/src/views/Example/Page/ExamplePage.vue new file mode 100644 index 0000000..c13fe3c --- /dev/null +++ b/promo-ui2/src/views/Example/Page/ExamplePage.vue @@ -0,0 +1,297 @@ + + + +@/hooks/event/useEventBus diff --git a/promo-ui2/src/views/Example/Page/components/Detail.vue b/promo-ui2/src/views/Example/Page/components/Detail.vue new file mode 100644 index 0000000..11d4a94 --- /dev/null +++ b/promo-ui2/src/views/Example/Page/components/Detail.vue @@ -0,0 +1,69 @@ + + + diff --git a/promo-ui2/src/views/Example/Page/components/Write.vue b/promo-ui2/src/views/Example/Page/components/Write.vue new file mode 100644 index 0000000..c4f50ad --- /dev/null +++ b/promo-ui2/src/views/Example/Page/components/Write.vue @@ -0,0 +1,154 @@ + + + diff --git a/promo-ui2/src/views/FlowRecord/index.vue b/promo-ui2/src/views/FlowRecord/index.vue new file mode 100644 index 0000000..d4912e6 --- /dev/null +++ b/promo-ui2/src/views/FlowRecord/index.vue @@ -0,0 +1,495 @@ + + + + + diff --git a/promo-ui2/src/views/Function/MultipleTabs.vue b/promo-ui2/src/views/Function/MultipleTabs.vue new file mode 100644 index 0000000..4a623c8 --- /dev/null +++ b/promo-ui2/src/views/Function/MultipleTabs.vue @@ -0,0 +1,18 @@ + + + diff --git a/promo-ui2/src/views/Function/MultipleTabsDemo.vue b/promo-ui2/src/views/Function/MultipleTabsDemo.vue new file mode 100644 index 0000000..9421a9a --- /dev/null +++ b/promo-ui2/src/views/Function/MultipleTabsDemo.vue @@ -0,0 +1,19 @@ + + + diff --git a/promo-ui2/src/views/Function/Request.vue b/promo-ui2/src/views/Function/Request.vue new file mode 100644 index 0000000..b37e67b --- /dev/null +++ b/promo-ui2/src/views/Function/Request.vue @@ -0,0 +1,173 @@ + + + diff --git a/promo-ui2/src/views/Function/Test.vue b/promo-ui2/src/views/Function/Test.vue new file mode 100644 index 0000000..dbb3732 --- /dev/null +++ b/promo-ui2/src/views/Function/Test.vue @@ -0,0 +1,70 @@ + + + diff --git a/promo-ui2/src/views/Guide/Guide.vue b/promo-ui2/src/views/Guide/Guide.vue new file mode 100644 index 0000000..dbdc91b --- /dev/null +++ b/promo-ui2/src/views/Guide/Guide.vue @@ -0,0 +1,19 @@ + + + diff --git a/promo-ui2/src/views/Home/index.vue b/promo-ui2/src/views/Home/index.vue new file mode 100644 index 0000000..202bc5e --- /dev/null +++ b/promo-ui2/src/views/Home/index.vue @@ -0,0 +1,418 @@ + + + + + diff --git a/promo-ui2/src/views/Level/Menu111.vue b/promo-ui2/src/views/Level/Menu111.vue new file mode 100644 index 0000000..015d0f8 --- /dev/null +++ b/promo-ui2/src/views/Level/Menu111.vue @@ -0,0 +1,20 @@ + + + diff --git a/promo-ui2/src/views/Level/Menu12.vue b/promo-ui2/src/views/Level/Menu12.vue new file mode 100644 index 0000000..8e2725d --- /dev/null +++ b/promo-ui2/src/views/Level/Menu12.vue @@ -0,0 +1,20 @@ + + + diff --git a/promo-ui2/src/views/Level/Menu2.vue b/promo-ui2/src/views/Level/Menu2.vue new file mode 100644 index 0000000..72e2abb --- /dev/null +++ b/promo-ui2/src/views/Level/Menu2.vue @@ -0,0 +1,20 @@ + + + diff --git a/promo-ui2/src/views/Login/Login.vue b/promo-ui2/src/views/Login/Login.vue new file mode 100644 index 0000000..0e11729 --- /dev/null +++ b/promo-ui2/src/views/Login/Login.vue @@ -0,0 +1,445 @@ + + + + + diff --git a/promo-ui2/src/views/Login/completeLogin.ts b/promo-ui2/src/views/Login/completeLogin.ts new file mode 100644 index 0000000..1ae4626 --- /dev/null +++ b/promo-ui2/src/views/Login/completeLogin.ts @@ -0,0 +1,30 @@ +import { getInfo, getShareDomain } from '@/api/user' +import type { UserType } from '@/api/login/types' +import { useUserStore } from '@/store/modules/user' + +export const completeLogin = async (username: string, token: string) => { + const userStore = useUserStore() + userStore.setToken(token) + const info = await getInfo() + let shareLink = '' + let shareDomain = '' + + if (info.data.userType !== '99') { + const domains = await getShareDomain() + shareDomain = domains.data.shareDomain || '' + const template = + info.data.userType === '01' ? domains.data.zhuboShareDomain : domains.data.zhaoshaoShareDomain + shareLink = `${template || ''}`.replace('${code}', info.data.invitationCode) + } + + userStore.setUserInfo({ + uid: info.data.userId, + username, + password: '', + role: info.data.userType, + roleId: '1', + token, + shareLink, + shareDomain + } as UserType) +} diff --git a/promo-ui2/src/views/Login/components/LoginForm.vue b/promo-ui2/src/views/Login/components/LoginForm.vue new file mode 100644 index 0000000..736bcc8 --- /dev/null +++ b/promo-ui2/src/views/Login/components/LoginForm.vue @@ -0,0 +1,357 @@ + + + + + diff --git a/promo-ui2/src/views/Login/components/RegisterForm.vue b/promo-ui2/src/views/Login/components/RegisterForm.vue new file mode 100644 index 0000000..f274a59 --- /dev/null +++ b/promo-ui2/src/views/Login/components/RegisterForm.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/promo-ui2/src/views/Login/components/index.ts b/promo-ui2/src/views/Login/components/index.ts new file mode 100644 index 0000000..d08be96 --- /dev/null +++ b/promo-ui2/src/views/Login/components/index.ts @@ -0,0 +1,4 @@ +import LoginForm from './LoginForm.vue' +import RegisterForm from './RegisterForm.vue' + +export { LoginForm, RegisterForm } diff --git a/promo-ui2/src/views/MyMoney/index.vue b/promo-ui2/src/views/MyMoney/index.vue new file mode 100644 index 0000000..099b38a --- /dev/null +++ b/promo-ui2/src/views/MyMoney/index.vue @@ -0,0 +1,105 @@ + + + diff --git a/promo-ui2/src/views/MySubordinates/index.vue b/promo-ui2/src/views/MySubordinates/index.vue new file mode 100644 index 0000000..7b03a00 --- /dev/null +++ b/promo-ui2/src/views/MySubordinates/index.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/promo-ui2/src/views/Permission/index.vue b/promo-ui2/src/views/Permission/index.vue new file mode 100644 index 0000000..32c1617 --- /dev/null +++ b/promo-ui2/src/views/Permission/index.vue @@ -0,0 +1,384 @@ + + + + + diff --git a/promo-ui2/src/views/Personal/PersonalCenter/PersonalCenter.vue b/promo-ui2/src/views/Personal/PersonalCenter/PersonalCenter.vue new file mode 100644 index 0000000..04552b9 --- /dev/null +++ b/promo-ui2/src/views/Personal/PersonalCenter/PersonalCenter.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/promo-ui2/src/views/Personal/PersonalCenter/components/EditInfo.vue b/promo-ui2/src/views/Personal/PersonalCenter/components/EditInfo.vue new file mode 100644 index 0000000..505ff05 --- /dev/null +++ b/promo-ui2/src/views/Personal/PersonalCenter/components/EditInfo.vue @@ -0,0 +1,117 @@ + + + diff --git a/promo-ui2/src/views/Personal/PersonalCenter/components/EditPassword.vue b/promo-ui2/src/views/Personal/PersonalCenter/components/EditPassword.vue new file mode 100644 index 0000000..05b5dde --- /dev/null +++ b/promo-ui2/src/views/Personal/PersonalCenter/components/EditPassword.vue @@ -0,0 +1,117 @@ + + + diff --git a/promo-ui2/src/views/Personal/PersonalCenter/components/UploadAvatar.vue b/promo-ui2/src/views/Personal/PersonalCenter/components/UploadAvatar.vue new file mode 100644 index 0000000..2ec3332 --- /dev/null +++ b/promo-ui2/src/views/Personal/PersonalCenter/components/UploadAvatar.vue @@ -0,0 +1,34 @@ + + + diff --git a/promo-ui2/src/views/Redirect/Redirect.vue b/promo-ui2/src/views/Redirect/Redirect.vue new file mode 100644 index 0000000..0b7af7b --- /dev/null +++ b/promo-ui2/src/views/Redirect/Redirect.vue @@ -0,0 +1,30 @@ + + diff --git a/promo-ui2/src/views/RollRoom/index.vue b/promo-ui2/src/views/RollRoom/index.vue new file mode 100644 index 0000000..afd4298 --- /dev/null +++ b/promo-ui2/src/views/RollRoom/index.vue @@ -0,0 +1,900 @@ + + + + + diff --git a/promo-ui2/src/views/SubordinateRecharge/index.vue b/promo-ui2/src/views/SubordinateRecharge/index.vue new file mode 100644 index 0000000..43cfac0 --- /dev/null +++ b/promo-ui2/src/views/SubordinateRecharge/index.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/promo-ui2/src/views/Turnover/index.vue b/promo-ui2/src/views/Turnover/index.vue new file mode 100644 index 0000000..55c7f3e --- /dev/null +++ b/promo-ui2/src/views/Turnover/index.vue @@ -0,0 +1,89 @@ + + + diff --git a/promo-ui2/src/views/User/index.vue b/promo-ui2/src/views/User/index.vue new file mode 100644 index 0000000..6e81d3c --- /dev/null +++ b/promo-ui2/src/views/User/index.vue @@ -0,0 +1,198 @@ + + diff --git a/promo-ui2/src/views/hooks/useClipboard.vue b/promo-ui2/src/views/hooks/useClipboard.vue new file mode 100644 index 0000000..f49125e --- /dev/null +++ b/promo-ui2/src/views/hooks/useClipboard.vue @@ -0,0 +1,26 @@ + + + diff --git a/promo-ui2/src/views/hooks/useCrudSchemas.vue b/promo-ui2/src/views/hooks/useCrudSchemas.vue new file mode 100644 index 0000000..69a2d7f --- /dev/null +++ b/promo-ui2/src/views/hooks/useCrudSchemas.vue @@ -0,0 +1,186 @@ + + + diff --git a/promo-ui2/src/views/hooks/useNetwork.vue b/promo-ui2/src/views/hooks/useNetwork.vue new file mode 100644 index 0000000..dd6ed93 --- /dev/null +++ b/promo-ui2/src/views/hooks/useNetwork.vue @@ -0,0 +1,12 @@ + + + diff --git a/promo-ui2/src/views/hooks/useTagsView.vue b/promo-ui2/src/views/hooks/useTagsView.vue new file mode 100644 index 0000000..1bacedc --- /dev/null +++ b/promo-ui2/src/views/hooks/useTagsView.vue @@ -0,0 +1,59 @@ + + + diff --git a/promo-ui2/src/views/hooks/useValidator.vue b/promo-ui2/src/views/hooks/useValidator.vue new file mode 100644 index 0000000..c3d6131 --- /dev/null +++ b/promo-ui2/src/views/hooks/useValidator.vue @@ -0,0 +1,80 @@ + + + diff --git a/promo-ui2/src/views/hooks/useWatermark.vue b/promo-ui2/src/views/hooks/useWatermark.vue new file mode 100644 index 0000000..bd5cc84 --- /dev/null +++ b/promo-ui2/src/views/hooks/useWatermark.vue @@ -0,0 +1,31 @@ + + + diff --git a/promo-ui2/src/views/subFlow/index.vue b/promo-ui2/src/views/subFlow/index.vue new file mode 100644 index 0000000..f2a7178 --- /dev/null +++ b/promo-ui2/src/views/subFlow/index.vue @@ -0,0 +1,240 @@ + + diff --git a/promo-ui2/src/views/subFlow/transaction.vue b/promo-ui2/src/views/subFlow/transaction.vue new file mode 100644 index 0000000..4d60c46 --- /dev/null +++ b/promo-ui2/src/views/subFlow/transaction.vue @@ -0,0 +1,108 @@ + + + diff --git a/promo-ui2/stylelint.config.cjs b/promo-ui2/stylelint.config.cjs new file mode 100644 index 0000000..a50d202 --- /dev/null +++ b/promo-ui2/stylelint.config.cjs @@ -0,0 +1,231 @@ +module.exports = { + root: true, + plugins: ['stylelint-order'], + customSyntax: 'postcss-html', + extends: ['stylelint-config-standard'], + rules: { + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global', 'deep'] + } + ], + 'at-rule-no-unknown': [ + true, + { + ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin'] + } + ], + 'media-query-no-invalid': null, + 'function-no-unknown': null, + 'no-empty-source': null, + 'named-grid-areas-no-invalid': null, + 'no-descending-specificity': null, + 'font-family-no-missing-generic-family-keyword': null, + 'rule-empty-line-before': [ + 'always', + { + ignore: ['after-comment', 'first-nested'] + } + ], + 'unit-no-unknown': [ + true, + { + ignoreUnits: ['rpx'] + } + ], + 'order/order': [ + [ + 'dollar-variables', + 'custom-properties', + 'at-rules', + 'declarations', + { + type: 'at-rule', + name: 'supports' + }, + { + type: 'at-rule', + name: 'media' + }, + 'rules' + ], + { + severity: 'warning' + } + ], + // Specify the alphabetical order of the attributes in the declaration block + 'order/properties-order': [ + 'position', + 'top', + 'right', + 'bottom', + 'left', + 'z-index', + 'display', + 'float', + 'width', + 'height', + 'max-width', + 'max-height', + 'min-width', + 'min-height', + 'padding', + 'padding-top', + 'padding-right', + 'padding-bottom', + 'padding-left', + 'margin', + 'margin-top', + 'margin-right', + 'margin-bottom', + 'margin-left', + 'margin-collapse', + 'margin-top-collapse', + 'margin-right-collapse', + 'margin-bottom-collapse', + 'margin-left-collapse', + 'overflow', + 'overflow-x', + 'overflow-y', + 'clip', + 'clear', + 'font', + 'font-family', + 'font-size', + 'font-smoothing', + 'osx-font-smoothing', + 'font-style', + 'font-weight', + 'hyphens', + 'src', + 'line-height', + 'letter-spacing', + 'word-spacing', + 'color', + 'text-align', + 'text-decoration', + 'text-indent', + 'text-overflow', + 'text-rendering', + 'text-size-adjust', + 'text-shadow', + 'text-transform', + 'word-break', + 'word-wrap', + 'white-space', + 'vertical-align', + 'list-style', + 'list-style-type', + 'list-style-position', + 'list-style-image', + 'pointer-events', + 'cursor', + 'background', + 'background-attachment', + 'background-color', + 'background-image', + 'background-position', + 'background-repeat', + 'background-size', + 'border', + 'border-collapse', + 'border-top', + 'border-right', + 'border-bottom', + 'border-left', + 'border-color', + 'border-image', + 'border-top-color', + 'border-right-color', + 'border-bottom-color', + 'border-left-color', + 'border-spacing', + 'border-style', + 'border-top-style', + 'border-right-style', + 'border-bottom-style', + 'border-left-style', + 'border-width', + 'border-top-width', + 'border-right-width', + 'border-bottom-width', + 'border-left-width', + 'border-radius', + 'border-top-right-radius', + 'border-bottom-right-radius', + 'border-bottom-left-radius', + 'border-top-left-radius', + 'border-radius-topright', + 'border-radius-bottomright', + 'border-radius-bottomleft', + 'border-radius-topleft', + 'content', + 'quotes', + 'outline', + 'outline-offset', + 'opacity', + 'filter', + 'visibility', + 'size', + 'zoom', + 'transform', + 'box-align', + 'box-flex', + 'box-orient', + 'box-pack', + 'box-shadow', + 'box-sizing', + 'table-layout', + 'animation', + 'animation-delay', + 'animation-duration', + 'animation-iteration-count', + 'animation-name', + 'animation-play-state', + 'animation-timing-function', + 'animation-fill-mode', + 'transition', + 'transition-delay', + 'transition-duration', + 'transition-property', + 'transition-timing-function', + 'background-clip', + 'backface-visibility', + 'resize', + 'appearance', + 'user-select', + 'interpolation-mode', + 'direction', + 'marks', + 'page', + 'set-link-source', + 'unicode-bidi', + 'speak' + ] + }, + ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'], + overrides: [ + { + files: ['*.vue', '**/*.vue', '*.html', '**/*.html'], + extends: ['stylelint-config-recommended', 'stylelint-config-html'], + rules: { + 'keyframes-name-pattern': null, + 'selector-class-pattern': null, + 'no-duplicate-selectors': null, + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['deep', 'global'] + } + ], + 'selector-pseudo-element-no-unknown': [ + true, + { + ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'] + } + ] + } + } + ] +} diff --git a/promo-ui2/tsconfig.json b/promo-ui2/tsconfig.json new file mode 100644 index 0000000..fe67772 --- /dev/null +++ b/promo-ui2/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "jsxImportSource": "vue", + "lib": ["esnext", "dom"], + "baseUrl": "./", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "strictFunctionTypes": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "experimentalDecorators": true, + "noImplicitAny": false, + "skipLibCheck": true, + "paths": { + "@/*": ["src/*"] + }, + "types": [ + "@intlify/unplugin-vue-i18n/types", + "vite/client", + "element-plus/global", + "@types/qrcode", + "vite-plugin-svg-icons/client" + ] + }, + "include": ["src", "types/**/*.d.ts", "mock/**/*.ts"] + // "exclude": ["dist", "node_modules"] +} diff --git a/promo-ui2/types/components.d.ts b/promo-ui2/types/components.d.ts new file mode 100644 index 0000000..15b5a6b --- /dev/null +++ b/promo-ui2/types/components.d.ts @@ -0,0 +1,9 @@ +declare module 'vue' { + export interface GlobalComponents { + Icon: (typeof import('../src/components/Icon/index'))['Icon'] + Permission: (typeof import('../src/components/Permission/index'))['Permission'] + BaseButton: (typeof import('../src/components/Button/index'))['BaseButton'] + } +} + +export {} diff --git a/promo-ui2/types/env.d.ts b/promo-ui2/types/env.d.ts new file mode 100644 index 0000000..3811725 --- /dev/null +++ b/promo-ui2/types/env.d.ts @@ -0,0 +1,14 @@ +/// + +declare module '*.vue' { + import { DefineComponent } from 'vue' + + const component: DefineComponent<{}, {}, any> + export default component +} + +declare global { + interface ImportMeta { + readonly env: ImportMetaEnv + } +} diff --git a/promo-ui2/types/global.d.ts b/promo-ui2/types/global.d.ts new file mode 100644 index 0000000..467c662 --- /dev/null +++ b/promo-ui2/types/global.d.ts @@ -0,0 +1,89 @@ +import type { CSSProperties } from 'vue' +import { RawAxiosRequestHeaders } from 'axios' +declare global { + declare interface Fn { + (...arg: T[]): T + } + + declare type Nullable = T | null + + declare type ElRef = Nullable + + declare type Recordable = Record + + declare type RemoveReadonly = { + -readonly [P in keyof T]: T[P] + } + + declare type ComponentRef = InstanceType + + declare type LocaleType = 'zh-CN' | 'en' + + declare type TimeoutHandle = ReturnType + declare type IntervalHandle = ReturnType + + declare type ElementPlusInfoType = 'success' | 'info' | 'warning' | 'danger' + + declare type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu' + + declare type AxiosContentType = + | 'application/json' + | 'application/x-www-form-urlencoded' + | 'multipart/form-data' + | 'text/plain' + + declare type AxiosMethod = 'get' | 'post' | 'delete' | 'put' + + declare type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' + + declare interface AxiosConfig { + params?: any + data?: any + url?: string + method?: AxiosMethod + headers?: RawAxiosRequestHeaders + responseType?: AxiosResponseType + } + + declare interface IResponse { + code: number + data: T extends any ? T : T & any + rows: any + total: any + } + + declare interface ThemeTypes { + elColorPrimary?: string + leftMenuBorderColor?: string + leftMenuBgColor?: string + leftMenuBgLightColor?: string + leftMenuBgActiveColor?: string + leftMenuCollapseBgActiveColor?: string + leftMenuTextColor?: string + leftMenuTextActiveColor?: string + logoTitleTextColor?: string + logoBorderColor?: string + topHeaderBgColor?: string + topHeaderTextColor?: string + topHeaderHoverColor?: string + topToolBorderColor?: string + } + + declare interface ImportMetaEnv { + readonly VITE_NODE_ENV: string + readonly VITE_APP_TITLE: string + readonly VITE_API_BASE_PATH: string + readonly VITE_BASE_PATH: string + readonly VITE_DROP_DEBUGGER: string + readonly VITE_DROP_CONSOLE: string + readonly VITE_SOURCEMAP: string + readonly VITE_OUT_DIR: string + readonly VITE_USE_BUNDLE_ANALYZER: string + readonly VITE_USE_ALL_ELEMENT_PLUS_STYLE: string + readonly VITE_USE_MOCK: string + readonly VITE_USE_CSS_SPLIT: string + readonly VITE_USE_ONLINE_ICON: string + readonly VITE_ICON_PREFIX: string + readonly VITE_HIDE_GLOBAL_SETTING: string + } +} diff --git a/promo-ui2/types/router.d.ts b/promo-ui2/types/router.d.ts new file mode 100644 index 0000000..32d9447 --- /dev/null +++ b/promo-ui2/types/router.d.ts @@ -0,0 +1,78 @@ +import type { RouteRecordRaw } from 'vue-router' +import { defineComponent } from 'vue' + +/** +* redirect: noredirect 当设置 noredirect 的时候该路由在面包屑导航中不可被点击 +* name:'router-name' 设定路由的名字,一定要填写不然使用时会出现各种问题 +* meta : { + hidden: true 当设置 true 的时候该路由不会再侧边栏出现 如404,login等页面(默认 false) + + alwaysShow: true 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式, + 只有一个时,会将那个子路由当做根路由显示在侧边栏, + 若你想不管路由下面的 children 声明的个数都显示你的根路由, + 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则, + 一直显示根路由(默认 false) + + title: 'title' 设置该路由在侧边栏和面包屑中展示的名字 + + icon: 'svg-name' 设置该路由的图标 + + noCache: true 如果设置为true,则不会被 缓存(默认 false) + + breadcrumb: false 如果设置为false,则不会在breadcrumb面包屑中显示(默认 true) + + affix: true 如果设置为true,则会一直固定在tag项中(默认 false) + + noTagsView: true 如果设置为true,则不会出现在tag中(默认 false) + + activeMenu: '/dashboard' 显示高亮的路由路径 + + canTo: true 设置为true即使hidden为true,也依然可以进行路由跳转(默认 false) + + permission: ['edit','add', 'delete'] 设置该路由的权限 + } +**/ + +interface RouteMetaCustom extends Record { + hidden?: boolean + alwaysShow?: boolean + title?: string + icon?: string + noCache?: boolean + breadcrumb?: boolean + affix?: boolean + activeMenu?: string + noTagsView?: boolean + canTo?: boolean + permission?: string[] +} + +declare module 'vue-router' { + interface RouteMeta extends RouteMetaCustom {} +} + +type Component = + | ReturnType + | (() => Promise) + | (() => Promise) + +declare global { + declare interface AppRouteRecordRaw extends Omit { + name: string + meta: RouteMetaCustom + component?: Component | string + children?: AppRouteRecordRaw[] + props?: Recordable + fullPath?: string + } + + declare interface AppCustomRouteRecordRaw + extends Omit { + name: string + meta: RouteMetaCustom + component: string + path: string + redirect: string + children?: AppCustomRouteRecordRaw[] + } +} diff --git a/promo-ui2/uno.config.ts b/promo-ui2/uno.config.ts new file mode 100644 index 0000000..997f6c8 --- /dev/null +++ b/promo-ui2/uno.config.ts @@ -0,0 +1,146 @@ +import { defineConfig, toEscapedSelector as e, presetUno, presetIcons } from 'unocss' +import transformerVariantGroup from '@unocss/transformer-variant-group' +import { loadEnv } from 'vite' +import { ICON_PREFIX } from './src/constants' + +const root = process.cwd() + +const createPresetIcons = () => { + const isBuild = !!process.argv[4] + let env = {} as any + if (!isBuild) { + env = loadEnv(process.argv[3], root) + } else { + env = loadEnv(process.argv[4], root) + } + // @ts-ignore + if (env.VITE_USE_ONLINE_ICON === 'true') { + return [] + } else { + return [ + presetIcons({ + autoInstall: false, + prefix: ICON_PREFIX + }) + ] + } +} + +export default defineConfig({ + // ...UnoCSS options + rules: [ + [ + /^overflow-ellipsis$/, + ([], { rawSelector }) => { + const selector = e(rawSelector) + return ` +${selector} { + text-overflow: ellipsis; +} +` + } + ], + [ + /^custom-hover$/, + ([], { rawSelector }) => { + const selector = e(rawSelector) + return ` +${selector} { + display: flex; + height: 100%; + padding: 1px 10px 0; + cursor: pointer; + align-items: center; + transition: background var(--transition-time-02); +} +/* you can have multiple rules */ +${selector}:hover { + background-color: var(--top-header-hover-color); +} +.dark ${selector}:hover { + background-color: var(--el-bg-color-overlay); +} +` + } + ], + [ + /^layout-border__left$/, + ([], { rawSelector }) => { + const selector = e(rawSelector) + return ` +${selector}:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 1px; + height: 100%; + background-color: var(--el-border-color); + z-index: 3; +} +` + } + ], + [ + /^layout-border__right$/, + ([], { rawSelector }) => { + const selector = e(rawSelector) + return ` +${selector}:after { + content: ""; + position: absolute; + top: 0; + right: 0; + width: 1px; + height: 100%; + background-color: var(--el-border-color); + z-index: 3; +} +` + } + ], + [ + /^layout-border__top$/, + ([], { rawSelector }) => { + const selector = e(rawSelector) + return ` +${selector}:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 1px; + background-color: var(--el-border-color); + z-index: 3; +} +` + } + ], + [ + /^layout-border__bottom$/, + ([], { rawSelector }) => { + const selector = e(rawSelector) + return ` +${selector}:after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 1px; + background-color: var(--el-border-color); + z-index: 3; +} +` + } + ] + ], + presets: [presetUno({ dark: 'class', attributify: false }), ...createPresetIcons()], + transformers: [transformerVariantGroup()], + content: { + pipeline: { + include: [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html|ts)($|\?)/] + } + } +}) diff --git a/promo-ui2/vite.config.ts b/promo-ui2/vite.config.ts new file mode 100644 index 0000000..12c233e --- /dev/null +++ b/promo-ui2/vite.config.ts @@ -0,0 +1,184 @@ +import { resolve } from 'path' +import { loadEnv } from 'vite' +import type { UserConfig, ConfigEnv } from 'vite' +import Vue from '@vitejs/plugin-vue' +import VueJsx from '@vitejs/plugin-vue-jsx' +import progress from 'vite-plugin-progress' +import EslintPlugin from 'vite-plugin-eslint' +import { ViteEjsPlugin } from 'vite-plugin-ejs' +import { viteMockServe } from 'vite-plugin-mock' +import PurgeIcons from 'vite-plugin-purge-icons' +import ServerUrlCopy from 'vite-plugin-url-copy' +import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' +import { createStyleImportPlugin, ElementPlusResolve } from 'vite-plugin-style-import' +import UnoCSS from 'unocss/vite' +import { visualizer } from 'rollup-plugin-visualizer' + +// https://vitejs.dev/config/ +const root = process.cwd() + +function pathResolve(dir: string) { + return resolve(root, '.', dir) +} + +export default ({ command, mode }: ConfigEnv): UserConfig => { + let env = {} as any + const isBuild = command === 'build' + if (!isBuild) { + env = loadEnv(process.argv[3] === '--mode' ? process.argv[4] : process.argv[3], root) + } else { + env = loadEnv(mode, root) + } + return { + base: env.VITE_BASE_PATH, + plugins: [ + Vue({ + script: { + // 开启defineModel + defineModel: true + } + }), + VueJsx(), + ServerUrlCopy(), + progress(), + env.VITE_USE_ALL_ELEMENT_PLUS_STYLE === 'false' + ? createStyleImportPlugin({ + resolves: [ElementPlusResolve()], + libs: [ + { + libraryName: 'element-plus', + esModule: true, + resolveStyle: (name) => { + if (name === 'click-outside') { + return '' + } + return `element-plus/es/components/${name.replace(/^el-/, '')}/style/css` + } + } + ] + }) + : undefined, + // EslintPlugin({ + // cache: false, + // failOnWarning: false, + // failOnError: false, + // include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件 + // }), + VueI18nPlugin({ + runtimeOnly: true, + compositionOnly: true, + include: [resolve(__dirname, 'src/locales/**')] + }), + createSvgIconsPlugin({ + iconDirs: [pathResolve('src/assets/svgs')], + symbolId: 'icon-[dir]-[name]', + svgoOptions: true + }), + PurgeIcons(), + env.VITE_USE_MOCK === 'true' + ? viteMockServe({ + ignore: /^\_/, + mockPath: 'mock', + localEnabled: !isBuild, + prodEnabled: isBuild, + injectCode: ` + import { setupProdMockServer } from '../mock/_createProductionServer' + + setupProdMockServer() + ` + }) + : undefined, + ViteEjsPlugin({ + title: env.VITE_APP_TITLE + }), + UnoCSS() + ], + + css: { + preprocessorOptions: { + less: { + additionalData: '@import "./src/styles/variables.module.less";', + javascriptEnabled: true + } + } + }, + resolve: { + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.less', '.css'], + alias: [ + { + find: 'vue-i18n', + replacement: 'vue-i18n/dist/vue-i18n.cjs.js' + }, + { + find: /\@\//, + replacement: `${pathResolve('src')}/` + } + ] + }, + esbuild: { + pure: env.VITE_DROP_CONSOLE === 'true' ? ['console.log'] : undefined, + drop: env.VITE_DROP_DEBUGGER === 'true' ? ['debugger'] : undefined + }, + build: { + target: 'es2015', + outDir: env.VITE_OUT_DIR || 'dist', + sourcemap: env.VITE_SOURCEMAP === 'true', + // brotliSize: false, + rollupOptions: { + plugins: env.VITE_USE_BUNDLE_ANALYZER === 'true' ? [visualizer()] : undefined, + // 拆包 + output: { + manualChunks: { + 'vue-chunks': ['vue', 'vue-router', 'pinia', 'vue-i18n'], + 'element-plus': ['element-plus'], + 'wang-editor': ['@wangeditor/editor', '@wangeditor/editor-for-vue'], + echarts: ['echarts', 'echarts-wordcloud'] + } + } + }, + cssCodeSplit: !(env.VITE_USE_CSS_SPLIT === 'false'), + cssTarget: ['chrome31'] + }, + server: { + port: 4000, + proxy: { + // 选项写法 + '/api': { + target: 'http://127.0.0.1:28081', + changeOrigin: true + }, + '/promo': { + target: 'http://127.0.0.1:28081', + changeOrigin: true + } + }, + hmr: { + overlay: false + }, + host: '0.0.0.0' + }, + optimizeDeps: { + include: [ + 'vue', + 'vue-router', + 'vue-types', + 'element-plus/es/locale/lang/zh-cn', + 'element-plus/es/locale/lang/en', + '@iconify/iconify', + '@vueuse/core', + 'axios', + 'qs', + 'echarts', + 'echarts-wordcloud', + 'qrcode', + '@wangeditor/editor', + '@wangeditor/editor-for-vue', + 'vue-json-pretty', + '@zxcvbn-ts/core', + 'dayjs', + 'cropperjs' + ] + } + } +} diff --git a/publish.sh b/publish.sh new file mode 100644 index 0000000..a531c34 --- /dev/null +++ b/publish.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# ============================================================ +# 一键部署脚本(宝塔面板使用) +# 用法:bash publish.sh (推荐用 bash 调用,避免权限问题) +# ============================================================ + +set -e + +echo "=========================================" +echo " 开始部署..." +echo "=========================================" + +# 1. 拉取最新代码 +echo "" +echo ">>> [1/3] 拉取最新代码" +git checkout -- . +git fetch origin master +echo "" +echo "--- 本次提交的修改文件 ---" +git --no-pager log -1 --stat --pretty=format:"" FETCH_HEAD +echo "" +echo "---------------------------" +git reset --hard FETCH_HEAD --quiet +chmod +x *.sh +echo "✅ 代码已更新" + +# 2. 打包 +echo "" +echo ">>> [2/3] Maven 打包" +./package.sh +echo "✅ 打包完成" + +# 3. 重启服务 +echo "" +echo ">>> [3/3] 重启服务" +./ry.sh stop +sleep 2 +./ry.sh start + +echo "" +echo "=========================================" +echo " 部署完成!" +echo "=========================================" +echo "" + +echo "查看日志:tail -f /www/ruoyi/logs/sys-info.log" diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..4563cbf --- /dev/null +++ b/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# ============================================================ +# 快捷启动脚本(等价于 ./ry.sh start) +# ============================================================ + +DIR="$(cd "$(dirname "$0")" && pwd)" +exec "$DIR/ry.sh" start diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml new file mode 100644 index 0000000..9f9f389 --- /dev/null +++ b/ruoyi-admin/pom.xml @@ -0,0 +1,193 @@ + + + + ruoyi + com.ruoyi + 4.8.2 + + 4.0.0 + jar + ruoyi-admin + + + web服务入口 + + + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + org.springframework.boot + spring-boot-devtools + true + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + com.mysql + mysql-connector-j + + + + + com.ruoyi + ruoyi-framework + + + + + com.ruoyi + ruoyi-quartz + + + + + com.ruoyi + ruoyi-generator + + + + com.ruoyi + service-admin + ${ruoyi.version} + + + + com.ruoyi + service-playingmethod + ${ruoyi.version} + + + + com.ruoyi + service-thirdparty + ${ruoyi.version} + + + + com.ruoyi + service-user + ${ruoyi.version} + + + + com.ruoyi + service-task + 4.8.2 + + + + com.ruoyi + game-wheel-service + ${ruoyi.version} + + + + com.ruoyi + game-olympus-service + ${ruoyi.version} + + + + com.ruoyi + skins-promo + ${ruoyi.version} + + + + + cloud.tianai.captcha + tianai-captcha-springboot-starter + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.junit.platform + junit-platform-launcher + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + repackage + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + false + ${project.artifactId} + + + + + ${project.artifactId} + + + \ No newline at end of file diff --git a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java new file mode 100644 index 0000000..03c7799 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java @@ -0,0 +1,35 @@ +package com.ruoyi; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; + +import jakarta.annotation.PostConstruct; +import java.util.TimeZone; + +/** + * 启动程序 + * + * @author ruoyi + */ +@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) +public class RuoYiApplication +{ + // + public static void main(String[] args) + { + // System.setProperty("spring.devtools.restart.enabled", "false"); + SpringApplication.run(RuoYiApplication.class, args); + System.out.println("(♥◠‿◠)ノ゙ 若依启动成功了 ლ(´ڡ`ლ)゙ \n" + + " .-------. ____ __ \n" + + " | _ _ \\ \\ \\ / / \n" + + " | ( ' ) | \\ _. / ' \n" + + " |(_ o _) / _( )_ .' \n" + + " | (_,_).' __ ___(_ o _)' \n" + + " | |\\ \\ | || |(_,_)' \n" + + " | | \\ `' /| `-' / \n" + + " | | \\ / \\ / \n" + + " ''-' `'-' `-..-' "); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java new file mode 100644 index 0000000..6de67dc --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java @@ -0,0 +1,18 @@ +package com.ruoyi; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +/** + * web容器中进行部署 + * + * @author ruoyi + */ +public class RuoYiServletInitializer extends SpringBootServletInitializer +{ + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) + { + return application.sources(RuoYiApplication.class); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java new file mode 100644 index 0000000..d6dcf06 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java @@ -0,0 +1,94 @@ +package com.ruoyi.web.controller.common; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import jakarta.annotation.Resource; +import javax.imageio.ImageIO; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.FastByteArrayOutputStream; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import com.google.code.kaptcha.Producer; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.sign.Base64; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 验证码操作处理 + * + * @author ruoyi + */ +@RestController +public class CaptchaController +{ + @Resource(name = "captchaProducer") + private Producer captchaProducer; + + @Resource(name = "captchaProducerMath") + private Producer captchaProducerMath; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysConfigService configService; + /** + * 生成验证码 + */ + @GetMapping("/captchaImage") + public AjaxResult getCode(HttpServletResponse response) throws IOException + { + AjaxResult ajax = AjaxResult.success(); + boolean captchaEnabled = configService.selectCaptchaEnabled(); + ajax.put("captchaEnabled", captchaEnabled); + if (!captchaEnabled) + { + return ajax; + } + + // 保存验证码信息 + String uuid = IdUtils.simpleUUID(); + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; + + String capStr = null, code = null; + BufferedImage image = null; + + // 生成验证码 + String captchaType = RuoYiConfig.getCaptchaType(); + if ("math".equals(captchaType)) + { + String capText = captchaProducerMath.createText(); + capStr = capText.substring(0, capText.lastIndexOf("@")); + code = capText.substring(capText.lastIndexOf("@") + 1); + image = captchaProducerMath.createImage(capStr); + } + else if ("char".equals(captchaType)) + { + capStr = code = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + } + + redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + // 转换流信息写出 + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try + { + ImageIO.write(image, "jpg", os); + } + catch (IOException e) + { + return AjaxResult.error(e.getMessage()); + } + + ajax.put("uuid", uuid); + ajax.put("img", Base64.encode(os.toByteArray())); + return ajax; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java new file mode 100644 index 0000000..b873cbf --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java @@ -0,0 +1,164 @@ +package com.ruoyi.web.controller.common; + +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.framework.config.ServerConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; + +/** + * 通用请求处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/common") +public class CommonController +{ + private static final Logger log = LoggerFactory.getLogger(CommonController.class); + + @Autowired + private ServerConfig serverConfig; + + private static final String FILE_DELIMETER = ","; + + /** + * 通用下载请求 + * + * @param fileName 文件名称 + * @param delete 是否删除 + */ + @GetMapping("/download") + public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) + { + try + { + if (!FileUtils.checkAllowDownload(fileName)) + { + throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); + } + String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); + String filePath = RuoYiConfig.getDownloadPath() + "/" + fileName; + + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, realFileName); + FileUtils.writeBytes(filePath, response.getOutputStream()); + if (delete) + { + FileUtils.deleteFile(filePath); + } + } + catch (Exception e) + { + log.error("下载文件失败", e); + } + } + + /** + * 通用上传请求(单个) + */ + @PostMapping("/upload") + public AjaxResult uploadFile(MultipartFile file,HttpServletRequest request) throws Exception + { + try + { + // 上传文件路径 + String filePath = RuoYiConfig.getUploadPath(); + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + AjaxResult ajax = AjaxResult.success(); + ajax.put("url", url); + ajax.put("fileName", fileName); + ajax.put("newFileName", FileUtils.getName(fileName)); + ajax.put("originalFilename", file.getOriginalFilename()); + return ajax; + } + catch (Exception e) + { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 通用上传请求(多个) + */ + @PostMapping("/uploads") + public AjaxResult uploadFiles(List files) throws Exception + { + try + { + // 上传文件路径 + String filePath = RuoYiConfig.getUploadPath(); + List urls = new ArrayList(); + List fileNames = new ArrayList(); + List newFileNames = new ArrayList(); + List originalFilenames = new ArrayList(); + for (MultipartFile file : files) + { + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + urls.add(url); + fileNames.add(fileName); + newFileNames.add(FileUtils.getName(fileName)); + originalFilenames.add(file.getOriginalFilename()); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER)); + ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); + ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER)); + ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER)); + return ajax; + } + catch (Exception e) + { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 本地资源通用下载 + */ + @GetMapping("/download/resource") + public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) + throws Exception + { + try + { + if (!FileUtils.checkAllowDownload(resource)) + { + throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); + } + // 本地资源路径 + String localPath = RuoYiConfig.getProfile(); + // 数据库资源地址 + String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); + // 下载名称 + String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, downloadName); + FileUtils.writeBytes(downloadPath, response.getOutputStream()); + } + catch (Exception e) + { + log.error("下载文件失败", e); + } + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java new file mode 100644 index 0000000..69470d0 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java @@ -0,0 +1,120 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysCache; + +/** + * 缓存监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/cache") +public class CacheController +{ + @Autowired + private RedisTemplate redisTemplate; + + private final static List caches = new ArrayList(); + { + caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息")); + caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息")); + caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典")); + caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码")); + caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交")); + caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理")); + caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数")); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception + { + Properties info = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info()); + Properties commandStats = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info("commandstats")); + Object dbSize = redisTemplate.execute((RedisCallback) connection -> connection.dbSize()); + + Map result = new HashMap<>(3); + result.put("info", info); + result.put("dbSize", dbSize); + + List> pieList = new ArrayList<>(); + commandStats.stringPropertyNames().forEach(key -> { + Map data = new HashMap<>(2); + String property = commandStats.getProperty(key); + data.put("name", StringUtils.removeStart(key, "cmdstat_")); + data.put("value", StringUtils.substringBetween(property, "calls=", ",usec")); + pieList.add(data); + }); + result.put("commandStats", pieList); + return AjaxResult.success(result); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getNames") + public AjaxResult cache() + { + return AjaxResult.success(caches); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getKeys/{cacheName}") + public AjaxResult getCacheKeys(@PathVariable String cacheName) + { + Set cacheKeys = redisTemplate.keys(cacheName + "*"); + return AjaxResult.success(cacheKeys); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getValue/{cacheName}/{cacheKey}") + public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey) + { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue); + return AjaxResult.success(sysCache); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheName/{cacheName}") + public AjaxResult clearCacheName(@PathVariable String cacheName) + { + Collection cacheKeys = redisTemplate.keys(cacheName + "*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheKey/{cacheKey}") + public AjaxResult clearCacheKey(@PathVariable String cacheKey) + { + redisTemplate.delete(cacheKey); + return AjaxResult.success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheAll") + public AjaxResult clearCacheAll() + { + Collection cacheKeys = redisTemplate.keys("*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java new file mode 100644 index 0000000..cc805ad --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java @@ -0,0 +1,27 @@ +package com.ruoyi.web.controller.monitor; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.framework.web.domain.Server; + +/** + * 服务器监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/server") +public class ServerController +{ + @PreAuthorize("@ss.hasPermi('monitor:server:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception + { + Server server = new Server(); + server.copyTo(); + return AjaxResult.success(server); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java new file mode 100644 index 0000000..f6fac71 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java @@ -0,0 +1,82 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.framework.web.service.SysPasswordService; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.service.ISysLogininforService; + +/** + * 系统访问记录 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/logininfor") +public class SysLogininforController extends BaseController +{ + @Autowired + private ISysLogininforService logininforService; + + @Autowired + private SysPasswordService passwordService; + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')") + @GetMapping("/list") + public TableDataInfo list(SysLogininfor logininfor) + { + startPage(); + List list = logininforService.selectLogininforList(logininfor); + return getDataTable(list); + } + + @Log(title = "登录日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysLogininfor logininfor) + { + List list = logininforService.selectLogininforList(logininfor); + ExcelUtil util = new ExcelUtil(SysLogininfor.class); + util.exportExcel(response, list, "登录日志"); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{infoIds}") + public AjaxResult remove(@PathVariable Long[] infoIds) + { + return toAjax(logininforService.deleteLogininforByIds(infoIds)); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + logininforService.cleanLogininfor(); + return success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')") + @Log(title = "账户解锁", businessType = BusinessType.OTHER) + @GetMapping("/unlock/{userName}") + public AjaxResult unlock(@PathVariable("userName") String userName) + { + passwordService.clearLoginRecordCache(userName); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java new file mode 100644 index 0000000..f056d65 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java @@ -0,0 +1,69 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.service.ISysOperLogService; + +/** + * 操作日志记录 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/operlog") +public class SysOperlogController extends BaseController +{ + @Autowired + private ISysOperLogService operLogService; + + @PreAuthorize("@ss.hasPermi('monitor:operlog:list')") + @GetMapping("/list") + public TableDataInfo list(SysOperLog operLog) + { + startPage(); + List list = operLogService.selectOperLogList(operLog); + return getDataTable(list); + } + + @Log(title = "操作日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:operlog:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysOperLog operLog) + { + List list = operLogService.selectOperLogList(operLog); + ExcelUtil util = new ExcelUtil(SysOperLog.class); + util.exportExcel(response, list, "操作日志"); + } + + @Log(title = "操作日志", businessType = BusinessType.DELETE) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/{operIds}") + public AjaxResult remove(@PathVariable Long[] operIds) + { + return toAjax(operLogService.deleteOperLogByIds(operIds)); + } + + @Log(title = "操作日志", businessType = BusinessType.CLEAN) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/clean") + public AjaxResult clean() + { + operLogService.cleanOperLog(); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java new file mode 100644 index 0000000..9dea0e3 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java @@ -0,0 +1,105 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysUserOnline; +import com.ruoyi.system.service.ISysUserOnlineService; + +/** + * 在线用户监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/online") +public class SysUserOnlineController extends BaseController +{ + @Autowired + private ISysUserOnlineService userOnlineService; + + @Autowired + private RedisCache redisCache; + + /** + * 心跳接口,前端每1分钟调用一次,标记用户在线 + */ + @PostMapping("/heartbeat") + public AjaxResult heartbeat() + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + String tokenId = loginUser.getToken(); + // 心跳key,2分钟过期 + redisCache.setCacheObject(CacheConstants.ONLINE_HEARTBEAT_KEY + tokenId, "1", 2, TimeUnit.MINUTES); + return success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:online:list')") + @GetMapping("/list") + public TableDataInfo list(String ipaddr, String userName) + { + Collection keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); + List userOnlineList = new ArrayList(); + for (String key : keys) + { + LoginUser user = redisCache.getCacheObject(key); + // 只显示有心跳的用户 + String tokenId = user.getToken(); + if (!redisCache.hasKey(CacheConstants.ONLINE_HEARTBEAT_KEY + tokenId)) + { + continue; + } + if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) + { + userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user)); + } + else if (StringUtils.isNotEmpty(ipaddr)) + { + userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user)); + } + else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser())) + { + userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user)); + } + else + { + userOnlineList.add(userOnlineService.loginUserToUserOnline(user)); + } + } + Collections.reverse(userOnlineList); + userOnlineList.removeAll(Collections.singleton(null)); + return getDataTable(userOnlineList); + } + + /** + * 强退用户 + */ + @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')") + @Log(title = "在线用户", businessType = BusinessType.FORCE) + @DeleteMapping("/{tokenId}") + public AjaxResult forceLogout(@PathVariable String tokenId) + { + redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java new file mode 100644 index 0000000..91a854f --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java @@ -0,0 +1,133 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 参数配置 信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/config") +public class SysConfigController extends BaseController +{ + @Autowired + private ISysConfigService configService; + + /** + * 获取参数配置列表 + */ + @PreAuthorize("@ss.hasPermi('system:config:list')") + @GetMapping("/list") + public TableDataInfo list(SysConfig config) + { + startPage(); + List list = configService.selectConfigList(config); + return getDataTable(list); + } + + @Log(title = "参数管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:config:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysConfig config) + { + List list = configService.selectConfigList(config); + ExcelUtil util = new ExcelUtil(SysConfig.class); + util.exportExcel(response, list, "参数数据"); + } + + /** + * 根据参数编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:config:query')") + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long configId) + { + return success(configService.selectConfigById(configId)); + } + + /** + * 根据参数键名查询参数值 + */ + @GetMapping(value = "/configKey/{configKey}") + public AjaxResult getConfigKey(@PathVariable String configKey) + { + return success(configService.selectConfigByKey(configKey)); + } + + /** + * 新增参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:add')") + @Log(title = "参数管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysConfig config) + { + if (!configService.checkConfigKeyUnique(config)) + { + return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setCreateBy(getUsername()); + return toAjax(configService.insertConfig(config)); + } + + /** + * 修改参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:edit')") + @Log(title = "参数管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysConfig config) + { + if (!configService.checkConfigKeyUnique(config)) + { + return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setUpdateBy(getUsername()); + return toAjax(configService.updateConfig(config)); + } + + /** + * 删除参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{configIds}") + public AjaxResult remove(@PathVariable Long[] configIds) + { + configService.deleteConfigByIds(configIds); + return success(); + } + + /** + * 刷新参数缓存 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + configService.resetConfigCache(); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java new file mode 100644 index 0000000..59e7588 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java @@ -0,0 +1,132 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysDeptService; + +/** + * 部门信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/dept") +public class SysDeptController extends BaseController +{ + @Autowired + private ISysDeptService deptService; + + /** + * 获取部门列表 + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list") + public AjaxResult list(SysDept dept) + { + List depts = deptService.selectDeptList(dept); + return success(depts); + } + + /** + * 查询部门列表(排除节点) + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list/exclude/{deptId}") + public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) + { + List depts = deptService.selectDeptList(new SysDept()); + depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")); + return success(depts); + } + + /** + * 根据部门编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:dept:query')") + @GetMapping(value = "/{deptId}") + public AjaxResult getInfo(@PathVariable Long deptId) + { + deptService.checkDeptDataScope(deptId); + return success(deptService.selectDeptById(deptId)); + } + + /** + * 新增部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:add')") + @Log(title = "部门管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDept dept) + { + if (!deptService.checkDeptNameUnique(dept)) + { + return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + dept.setCreateBy(getUsername()); + return toAjax(deptService.insertDept(dept)); + } + + /** + * 修改部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:edit')") + @Log(title = "部门管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDept dept) + { + Long deptId = dept.getDeptId(); + deptService.checkDeptDataScope(deptId); + if (!deptService.checkDeptNameUnique(dept)) + { + return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + else if (dept.getParentId().equals(deptId)) + { + return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); + } + else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0) + { + return error("该部门包含未停用的子部门!"); + } + dept.setUpdateBy(getUsername()); + return toAjax(deptService.updateDept(dept)); + } + + /** + * 删除部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:remove')") + @Log(title = "部门管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{deptId}") + public AjaxResult remove(@PathVariable Long deptId) + { + if (deptService.hasChildByDeptId(deptId)) + { + return warn("存在下级部门,不允许删除"); + } + if (deptService.checkDeptExistUser(deptId)) + { + return warn("部门存在用户,不允许删除"); + } + deptService.checkDeptDataScope(deptId); + return toAjax(deptService.deleteDeptById(deptId)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java new file mode 100644 index 0000000..325cf56 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java @@ -0,0 +1,128 @@ +package com.ruoyi.web.controller.system; + +import java.util.ArrayList; +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.service.ISysDictDataService; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@Api(tags = "管理端 字段值管理") +@RestController +@RequestMapping("/system/dict/data") +public class SysDictDataController extends BaseController +{ + @Autowired + private ISysDictDataService dictDataService; + + @Autowired + private ISysDictTypeService dictTypeService; + + @ApiOperation("获取字典值") + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictData dictData) + { + startPage(); + List list = dictDataService.selectDictDataList(dictData); + return getDataTable(list); + } + + @Log(title = "字典数据", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictData dictData) + { + List list = dictDataService.selectDictDataList(dictData); + ExcelUtil util = new ExcelUtil(SysDictData.class); + util.exportExcel(response, list, "字典数据"); + } + + /** + * 查询字典数据详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictCode}") + public AjaxResult getInfo(@PathVariable Long dictCode) + { + return success(dictDataService.selectDictDataById(dictCode)); + } + + /** + * 根据字典类型查询字典数据信息 + */ + @GetMapping(value = "/type/{dictType}") + public AjaxResult dictType(@PathVariable String dictType) + { + List data = dictTypeService.selectDictDataByType(dictType); + if (StringUtils.isNull(data)) + { + data = new ArrayList(); + } + return success(data); + } + + /** + * 新增字典类型 + */ + @ApiOperation("新增字典值") + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典数据", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictData dict) + { + dict.setCreateBy(getUsername()); + return toAjax(dictDataService.insertDictData(dict)); + } + + /** + * 修改保存字典类型 + */ + @ApiOperation("修改字典值") + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典数据", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictData dict) + { + dict.setUpdateBy(getUsername()); + return toAjax(dictDataService.updateDictData(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictCodes}") + public AjaxResult remove(@PathVariable Long[] dictCodes) + { + dictDataService.deleteDictDataByIds(dictCodes); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java new file mode 100644 index 0000000..3ff314c --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java @@ -0,0 +1,131 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDictType; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/dict/type") +public class SysDictTypeController extends BaseController +{ + @Autowired + private ISysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictType dictType) + { + startPage(); + List list = dictTypeService.selectDictTypeList(dictType); + return getDataTable(list); + } + + @Log(title = "字典类型", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictType dictType) + { + List list = dictTypeService.selectDictTypeList(dictType); + ExcelUtil util = new ExcelUtil(SysDictType.class); + util.exportExcel(response, list, "字典类型"); + } + + /** + * 查询字典类型详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictId}") + public AjaxResult getInfo(@PathVariable Long dictId) + { + return success(dictTypeService.selectDictTypeById(dictId)); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典类型", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictType dict) + { + if (!dictTypeService.checkDictTypeUnique(dict)) + { + return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setCreateBy(getUsername()); + return toAjax(dictTypeService.insertDictType(dict)); + } + + /** + * 修改字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典类型", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictType dict) + { + if (!dictTypeService.checkDictTypeUnique(dict)) + { + return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setUpdateBy(getUsername()); + return toAjax(dictTypeService.updateDictType(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictIds}") + public AjaxResult remove(@PathVariable Long[] dictIds) + { + dictTypeService.deleteDictTypeByIds(dictIds); + return success(); + } + + /** + * 刷新字典缓存 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + dictTypeService.resetDictCache(); + return success(); + } + + /** + * 获取字典选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List dictTypes = dictTypeService.selectDictTypeAll(); + return success(dictTypes); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java new file mode 100644 index 0000000..13007eb --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java @@ -0,0 +1,29 @@ +package com.ruoyi.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.utils.StringUtils; + +/** + * 首页 + * + * @author ruoyi + */ +@RestController +public class SysIndexController +{ + /** 系统基础配置 */ + @Autowired + private RuoYiConfig ruoyiConfig; + + /** + * 访问首页,提示语 + */ + @RequestMapping("/") + public String index() + { + return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion()); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java new file mode 100644 index 0000000..9c30493 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -0,0 +1,88 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import java.util.Set; + +import io.swagger.annotations.Api; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginBody; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.framework.web.service.SysLoginService; +import com.ruoyi.framework.web.service.SysPermissionService; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 登录验证 + * + * @author ruoyi + */ +@Api(tags = "管理端认证api") +@RestController +public class SysLoginController +{ + @Autowired + private SysLoginService loginService; + + @Autowired + private ISysMenuService menuService; + + @Autowired + private SysPermissionService permissionService; + + /** + * 登录方法 + * + * @param loginBody 登录信息 + * @return 结果 + */ + @PostMapping("/login") + public AjaxResult login(@RequestBody LoginBody loginBody) + { + AjaxResult ajax = AjaxResult.success(); + // 生成令牌 + String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid()); + ajax.put(Constants.TOKEN, token); + return ajax; + } + + /** + * 获取用户信息 + * + * @return 用户信息 + */ + @GetMapping("getInfo") + public AjaxResult getInfo() + { + SysUser user = SecurityUtils.getLoginUser().getUser(); + // 角色集合 + Set roles = permissionService.getRolePermission(user); + // 权限集合 + Set permissions = permissionService.getMenuPermission(user); + AjaxResult ajax = AjaxResult.success(); + ajax.put("user", user); + ajax.put("roles", roles); + ajax.put("permissions", permissions); + return ajax; + } + + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @GetMapping("getRouters") + public AjaxResult getRouters() + { + Long userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuTreeByUserId(userId); + return AjaxResult.success(menuService.buildMenus(menus)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java new file mode 100644 index 0000000..0a1f908 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java @@ -0,0 +1,138 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 菜单信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/menu") +public class SysMenuController extends BaseController +{ + @Autowired + private ISysMenuService menuService; + + /** + * 获取菜单列表 + */ + @PreAuthorize("@ss.hasPermi('system:menu:list')") + @GetMapping("/list") + public AjaxResult list(SysMenu menu) + { + List menus = menuService.selectMenuList(menu, getUserId()); + return success(menus); + } + + /** + * 根据菜单编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:menu:query')") + @GetMapping(value = "/{menuId}") + public AjaxResult getInfo(@PathVariable Long menuId) + { + return success(menuService.selectMenuById(menuId)); + } + + /** + * 获取菜单下拉树列表 + */ + @GetMapping("/treeselect") + public AjaxResult treeselect(SysMenu menu) + { + List menus = menuService.selectMenuList(menu, getUserId()); + return success(menuService.buildMenuTreeSelect(menus)); + } + + /** + * 加载对应角色菜单列表树 + */ + @GetMapping(value = "/roleMenuTreeselect/{roleId}") + public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) + { + List menus = menuService.selectMenuList(getUserId()); + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); + ajax.put("menus", menuService.buildMenuTreeSelect(menus)); + return ajax; + } + + /** + * 新增菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:add')") + @Log(title = "菜单管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysMenu menu) + { + if (!menuService.checkMenuNameUnique(menu)) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + menu.setCreateBy(getUsername()); + return toAjax(menuService.insertMenu(menu)); + } + + /** + * 修改菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:edit')") + @Log(title = "菜单管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysMenu menu) + { + if (!menuService.checkMenuNameUnique(menu)) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + else if (menu.getMenuId().equals(menu.getParentId())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } + menu.setUpdateBy(getUsername()); + return toAjax(menuService.updateMenu(menu)); + } + + /** + * 删除菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:remove')") + @Log(title = "菜单管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{menuId}") + public AjaxResult remove(@PathVariable("menuId") Long menuId) + { + if (menuService.hasChildByMenuId(menuId)) + { + return warn("存在子菜单,不允许删除"); + } + return toAjax(menuService.deleteMenuById(menuId)); + } +} \ No newline at end of file diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java new file mode 100644 index 0000000..8622828 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java @@ -0,0 +1,91 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.system.domain.SysNotice; +import com.ruoyi.system.service.ISysNoticeService; + +/** + * 公告 信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/notice") +public class SysNoticeController extends BaseController +{ + @Autowired + private ISysNoticeService noticeService; + + /** + * 获取通知公告列表 + */ + @PreAuthorize("@ss.hasPermi('system:notice:list')") + @GetMapping("/list") + public TableDataInfo list(SysNotice notice) + { + startPage(); + List list = noticeService.selectNoticeList(notice); + return getDataTable(list); + } + + /** + * 根据通知公告编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:notice:query')") + @GetMapping(value = "/{noticeId}") + public AjaxResult getInfo(@PathVariable Long noticeId) + { + return success(noticeService.selectNoticeById(noticeId)); + } + + /** + * 新增通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:add')") + @Log(title = "通知公告", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysNotice notice) + { + notice.setCreateBy(getUsername()); + return toAjax(noticeService.insertNotice(notice)); + } + + /** + * 修改通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:edit')") + @Log(title = "通知公告", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysNotice notice) + { + notice.setUpdateBy(getUsername()); + return toAjax(noticeService.updateNotice(notice)); + } + + /** + * 删除通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:remove')") + @Log(title = "通知公告", businessType = BusinessType.DELETE) + @DeleteMapping("/{noticeIds}") + public AjaxResult remove(@PathVariable Long[] noticeIds) + { + return toAjax(noticeService.deleteNoticeByIds(noticeIds)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java new file mode 100644 index 0000000..5b604cf --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java @@ -0,0 +1,129 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.service.ISysPostService; + +/** + * 岗位信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/post") +public class SysPostController extends BaseController +{ + @Autowired + private ISysPostService postService; + + /** + * 获取岗位列表 + */ + @PreAuthorize("@ss.hasPermi('system:post:list')") + @GetMapping("/list") + public TableDataInfo list(SysPost post) + { + startPage(); + List list = postService.selectPostList(post); + return getDataTable(list); + } + + @Log(title = "岗位管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:post:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysPost post) + { + List list = postService.selectPostList(post); + ExcelUtil util = new ExcelUtil(SysPost.class); + util.exportExcel(response, list, "岗位数据"); + } + + /** + * 根据岗位编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:post:query')") + @GetMapping(value = "/{postId}") + public AjaxResult getInfo(@PathVariable Long postId) + { + return success(postService.selectPostById(postId)); + } + + /** + * 新增岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:add')") + @Log(title = "岗位管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysPost post) + { + if (!postService.checkPostNameUnique(post)) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (!postService.checkPostCodeUnique(post)) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setCreateBy(getUsername()); + return toAjax(postService.insertPost(post)); + } + + /** + * 修改岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:edit')") + @Log(title = "岗位管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysPost post) + { + if (!postService.checkPostNameUnique(post)) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (!postService.checkPostCodeUnique(post)) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setUpdateBy(getUsername()); + return toAjax(postService.updatePost(post)); + } + + /** + * 删除岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:remove')") + @Log(title = "岗位管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{postIds}") + public AjaxResult remove(@PathVariable Long[] postIds) + { + return toAjax(postService.deletePostByIds(postIds)); + } + + /** + * 获取岗位选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List posts = postService.selectPostAll(); + return success(posts); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java new file mode 100644 index 0000000..be5af6a --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java @@ -0,0 +1,136 @@ +package com.ruoyi.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.utils.file.MimeTypeUtils; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 个人信息 业务处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/user/profile") +public class SysProfileController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private TokenService tokenService; + + /** + * 个人信息 + */ + @GetMapping + public AjaxResult profile() + { + LoginUser loginUser = getLoginUser(); + SysUser user = loginUser.getUser(); + AjaxResult ajax = AjaxResult.success(user); + ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername())); + ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername())); + return ajax; + } + + /** + * 修改用户 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult updateProfile(@RequestBody SysUser user) + { + LoginUser loginUser = getLoginUser(); + SysUser currentUser = loginUser.getUser(); + currentUser.setNickName(user.getNickName()); + currentUser.setEmail(user.getEmail()); + currentUser.setPhonenumber(user.getPhonenumber()); + currentUser.setSex(user.getSex()); + if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser)) + { + return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser)) + { + return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + if (userService.updateUserProfile(currentUser) > 0) + { + // 更新缓存用户信息 + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改个人信息异常,请联系管理员"); + } + + /** + * 重置密码 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping("/updatePwd") + public AjaxResult updatePwd(String oldPassword, String newPassword) + { + LoginUser loginUser = getLoginUser(); + String userName = loginUser.getUsername(); + String password = loginUser.getPassword(); + if (!SecurityUtils.matchesPassword(oldPassword, password)) + { + return error("修改密码失败,旧密码错误"); + } + if (SecurityUtils.matchesPassword(newPassword, password)) + { + return error("新密码不能与旧密码相同"); + } + if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0) + { + // 更新缓存用户密码 + loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword)); + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改密码异常,请联系管理员"); + } + + /** + * 头像上传 + */ + @Log(title = "用户头像", businessType = BusinessType.UPDATE) + @PostMapping("/avatar") + public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception + { + if (!file.isEmpty()) + { + LoginUser loginUser = getLoginUser(); + String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION); + if (userService.updateUserAvatar(loginUser.getUsername(), avatar)) + { + AjaxResult ajax = AjaxResult.success(); + ajax.put("imgUrl", avatar); + // 更新缓存用户头像 + loginUser.getUser().setAvatar(avatar); + tokenService.setLoginUser(loginUser); + return ajax; + } + } + return error("上传图片异常,请联系管理员"); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java new file mode 100644 index 0000000..fe19249 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java @@ -0,0 +1,38 @@ +package com.ruoyi.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.RegisterBody; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.SysRegisterService; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 注册验证 + * + * @author ruoyi + */ +@RestController +public class SysRegisterController extends BaseController +{ + @Autowired + private SysRegisterService registerService; + + @Autowired + private ISysConfigService configService; + + @PostMapping("/register") + public AjaxResult register(@RequestBody RegisterBody user) + { + if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) + { + return error("当前系统没有开启注册功能!"); + } + String msg = registerService.register(user); + return StringUtils.isEmpty(msg) ? success() : error(msg); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java new file mode 100644 index 0000000..4ba3d1b --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java @@ -0,0 +1,262 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.framework.web.service.SysPermissionService; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.service.ISysDeptService; +import com.ruoyi.system.service.ISysRoleService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 角色信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/role") +public class SysRoleController extends BaseController +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private TokenService tokenService; + + @Autowired + private SysPermissionService permissionService; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysDeptService deptService; + + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/list") + public TableDataInfo list(SysRole role) + { + startPage(); + List list = roleService.selectRoleList(role); + return getDataTable(list); + } + + @Log(title = "角色管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:role:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysRole role) + { + List list = roleService.selectRoleList(role); + ExcelUtil util = new ExcelUtil(SysRole.class); + util.exportExcel(response, list, "角色数据"); + } + + /** + * 根据角色编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/{roleId}") + public AjaxResult getInfo(@PathVariable Long roleId) + { + roleService.checkRoleDataScope(roleId); + return success(roleService.selectRoleById(roleId)); + } + + /** + * 新增角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:add')") + @Log(title = "角色管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysRole role) + { + if (!roleService.checkRoleNameUnique(role)) + { + return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (!roleService.checkRoleKeyUnique(role)) + { + return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setCreateBy(getUsername()); + return toAjax(roleService.insertRole(role)); + + } + + /** + * 修改保存角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + if (!roleService.checkRoleNameUnique(role)) + { + return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (!roleService.checkRoleKeyUnique(role)) + { + return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setUpdateBy(getUsername()); + + if (roleService.updateRole(role) > 0) + { + // 更新缓存用户权限 + LoginUser loginUser = getLoginUser(); + if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin()) + { + loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser())); + loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName())); + tokenService.setLoginUser(loginUser); + } + return success(); + } + return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员"); + } + + /** + * 修改保存数据权限 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/dataScope") + public AjaxResult dataScope(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + return toAjax(roleService.authDataScope(role)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + role.setUpdateBy(getUsername()); + return toAjax(roleService.updateRoleStatus(role)); + } + + /** + * 删除角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:remove')") + @Log(title = "角色管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{roleIds}") + public AjaxResult remove(@PathVariable Long[] roleIds) + { + return toAjax(roleService.deleteRoleByIds(roleIds)); + } + + /** + * 获取角色选择框列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + return success(roleService.selectRoleAll()); + } + + /** + * 查询已分配用户角色列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/allocatedList") + public TableDataInfo allocatedList(SysUser user) + { + startPage(); + List list = userService.selectAllocatedList(user); + return getDataTable(list); + } + + /** + * 查询未分配用户角色列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/unallocatedList") + public TableDataInfo unallocatedList(SysUser user) + { + startPage(); + List list = userService.selectUnallocatedList(user); + return getDataTable(list); + } + + /** + * 取消授权用户 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancel") + public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) + { + return toAjax(roleService.deleteAuthUser(userRole)); + } + + /** + * 批量取消授权用户 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancelAll") + public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds) + { + return toAjax(roleService.deleteAuthUsers(roleId, userIds)); + } + + /** + * 批量选择用户授权 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/selectAll") + public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds) + { + roleService.checkRoleDataScope(roleId); + return toAjax(roleService.insertAuthUsers(roleId, userIds)); + } + + /** + * 获取对应角色部门树列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/deptTree/{roleId}") + public AjaxResult deptTree(@PathVariable("roleId") Long roleId) + { + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId)); + ajax.put("depts", deptService.selectDeptTreeList(new SysDept())); + return ajax; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java new file mode 100644 index 0000000..b006012 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java @@ -0,0 +1,251 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import java.util.stream.Collectors; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.service.ISysDeptService; +import com.ruoyi.system.service.ISysPostService; +import com.ruoyi.system.service.ISysRoleService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/user") +public class SysUserController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + private ISysPostService postService; + + /** + * 获取用户列表 + */ + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/list") + public TableDataInfo list(SysUser user) + { + startPage(); + List list = userService.selectUserList(user); + return getDataTable(list); + } + + @Log(title = "用户管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:user:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysUser user) + { + List list = userService.selectUserList(user); + ExcelUtil util = new ExcelUtil(SysUser.class); + util.exportExcel(response, list, "用户数据"); + } + + @Log(title = "用户管理", businessType = BusinessType.IMPORT) + @PreAuthorize("@ss.hasPermi('system:user:import')") + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception + { + ExcelUtil util = new ExcelUtil(SysUser.class); + List userList = util.importExcel(file.getInputStream()); + String operName = getUsername(); + String message = userService.importUser(userList, updateSupport, operName); + return success(message); + } + + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) + { + ExcelUtil util = new ExcelUtil(SysUser.class); + util.importTemplateExcel(response, "用户数据"); + } + + /** + * 根据用户编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping(value = { "/", "/{userId}" }) + public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) + { + userService.checkUserDataScope(userId); + AjaxResult ajax = AjaxResult.success(); + List roles = roleService.selectRoleAll(); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + ajax.put("posts", postService.selectPostAll()); + if (StringUtils.isNotNull(userId)) + { + SysUser sysUser = userService.selectUserById(userId); + ajax.put(AjaxResult.DATA_TAG, sysUser); + ajax.put("postIds", postService.selectPostListByUserId(userId)); + ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList())); + } + return ajax; + } + + /** + * 新增用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:add')") + @Log(title = "用户管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysUser user) + { + if (!userService.checkUserNameUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setCreateBy(getUsername()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + return toAjax(userService.insertUser(user)); + } + + /** + * 修改用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + if (!userService.checkUserNameUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUser(user)); + } + + /** + * 删除用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:remove')") + @Log(title = "用户管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{userIds}") + public AjaxResult remove(@PathVariable Long[] userIds) + { + if (ArrayUtils.contains(userIds, getUserId())) + { + return error("当前用户不能删除"); + } + return toAjax(userService.deleteUserByIds(userIds)); + } + + /** + * 重置密码 + */ + @PreAuthorize("@ss.hasPermi('system:user:resetPwd')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/resetPwd") + public AjaxResult resetPwd(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + user.setUpdateBy(getUsername()); + return toAjax(userService.resetPwd(user)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUserStatus(user)); + } + + /** + * 根据用户编号获取授权角色 + */ + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping("/authRole/{userId}") + public AjaxResult authRole(@PathVariable("userId") Long userId) + { + AjaxResult ajax = AjaxResult.success(); + SysUser user = userService.selectUserById(userId); + List roles = roleService.selectRolesByUserId(userId); + ajax.put("user", user); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + return ajax; + } + + /** + * 用户授权角色 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.GRANT) + @PutMapping("/authRole") + public AjaxResult insertAuthRole(Long userId, Long[] roleIds) + { + userService.checkUserDataScope(userId); + userService.insertUserAuth(userId, roleIds); + return success(); + } + + /** + * 获取部门树列表 + */ + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/deptTree") + public AjaxResult deptTree(SysDept dept) + { + return success(deptService.selectDeptTreeList(dept)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java new file mode 100644 index 0000000..a54b3af --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java @@ -0,0 +1,171 @@ +package com.ruoyi.web.controller.tool; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * swagger 用户测试方法 + * + * @author ruoyi + */ +//@Api(tags = "用户信息管理") +@RestController +@RequestMapping("/test/user") +public class TestController extends BaseController +{ + private final static Map users = new LinkedHashMap(); + { + users.put(1, new UserEntity(1, "admin", "admin123", "15888888888")); + users.put(2, new UserEntity(2, "ry", "admin123", "15666666666")); + } + + // @ApiOperation("获取用户列表") + @GetMapping("/list") + public R> userList() + { + List userList = new ArrayList(users.values()); + return R.ok(userList); + } + + // @ApiOperation("获取用户详细") +// @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class) + @GetMapping("/{userId}") + public R getUser(@PathVariable Integer userId) + { + if (!users.isEmpty() && users.containsKey(userId)) + { + return R.ok(users.get(userId)); + } + else + { + return R.fail("用户不存在"); + } + } + + // @ApiOperation("新增用户") +// @ApiImplicitParams({ +// @ApiImplicitParam(name = "userId", value = "用户id", dataType = "Integer", dataTypeClass = Integer.class), +// @ApiImplicitParam(name = "username", value = "用户名称", dataType = "String", dataTypeClass = String.class), +// @ApiImplicitParam(name = "password", value = "用户密码", dataType = "String", dataTypeClass = String.class), +// @ApiImplicitParam(name = "mobile", value = "用户手机", dataType = "String", dataTypeClass = String.class) +// }) + @PostMapping("/save") + public R save(UserEntity user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) + { + return R.fail("用户ID不能为空"); + } + users.put(user.getUserId(), user); + return R.ok(); + } + + // @ApiOperation("更新用户") + @PutMapping("/update") + public R update(@RequestBody UserEntity user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) + { + return R.fail("用户ID不能为空"); + } + if (users.isEmpty() || !users.containsKey(user.getUserId())) + { + return R.fail("用户不存在"); + } + users.remove(user.getUserId()); + users.put(user.getUserId(), user); + return R.ok(); + } + + // @ApiOperation("删除用户信息") +// @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class) + @DeleteMapping("/{userId}") + public R delete(@PathVariable Integer userId) + { + if (!users.isEmpty() && users.containsKey(userId)) + { + users.remove(userId); + return R.ok(); + } + else + { + return R.fail("用户不存在"); + } + } +} + +//@ApiModel(value = "UserEntity", description = "用户实体") +class UserEntity +{ + // @ApiModelProperty("用户ID") + private Integer userId; + + // @ApiModelProperty("用户名称") + private String username; + + // @ApiModelProperty("用户密码") + private String password; + + // @ApiModelProperty("用户手机") + private String mobile; + + public UserEntity() + { + + } + + public UserEntity(Integer userId, String username, String password, String mobile) + { + this.userId = userId; + this.username = username; + this.password = password; + this.mobile = mobile; + } + + public Integer getUserId() + { + return userId; + } + + public void setUserId(Integer userId) + { + this.userId = userId; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getMobile() + { + return mobile; + } + + public void setMobile(String mobile) + { + this.mobile = mobile; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java b/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java new file mode 100644 index 0000000..e0a8d1a --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java @@ -0,0 +1,170 @@ +package com.ruoyi.web.core.config; + +import io.swagger.annotations.ApiOperation; +import io.swagger.models.auth.In; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiKey; +import springfox.documentation.service.AuthorizationScope; +import springfox.documentation.service.SecurityReference; +import springfox.documentation.service.SecurityScheme; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +public class SwaggerConfig { + + /** + * 后台接口分组 + */ + // @Bean + public Docket adminDocket() { + return new Docket(DocumentationType.OAS_30) + .select() + // 扫描注解 + .apis(RequestHandlerSelectors.basePackage("com.ruoyi.admin.controller")) + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + // 扫描所有.apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build() + .groupName("admin") + // 设置安全模式,访问token + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()); + } + + /** + * 用户业务接口分组 + */ + @Bean + public Docket userDocket() { + return new Docket(DocumentationType.OAS_30) + .select() + .apis(RequestHandlerSelectors.basePackage("com.ruoyi.user.controller")) + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + .paths(PathSelectors.any()) + .build() + .groupName("user") + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()); + } + + /** + * 第三方业务接口分组 + */ + @Bean + public Docket thirdPartyDocket() { + return new Docket(DocumentationType.OAS_30) + .select() + .apis(RequestHandlerSelectors.basePackage("com.ruoyi.thirdparty")) + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + .paths(PathSelectors.any()) + .build() + .groupName("thirdParty") + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()); + } + + /** + * 玩法业务接口分组 + */ + @Bean + public Docket playingMethodDocket() { + return new Docket(DocumentationType.OAS_30) + .select() + .apis(RequestHandlerSelectors.basePackage("com.ruoyi.playingmethod.controller")) + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + .paths(PathSelectors.any()) + .build() + .groupName("playingMethod") + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()); + } + + /** + * 糖果玩法业务接口分组 + */ + @Bean + public Docket gameSugarMethodDocket() { + return new Docket(DocumentationType.OAS_30) + .select() + .apis(RequestHandlerSelectors.basePackage("com.ruoyi.game.sugar.controller")) + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + .paths(PathSelectors.any()) + .build() + .groupName("gameSugarMethod") + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()); + } + + /** + * 任务业务接口分组 + */ + @Bean + public Docket taskDocket() { + return new Docket(DocumentationType.OAS_30) + .select() + .apis(RequestHandlerSelectors.basePackage("com.ruoyi.task.controller")) + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + .paths(PathSelectors.any()) + .build() + .groupName("task") + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()); + } + + /** + * 推广后台接口分组 + */ + @Bean + public Docket promoDocket() { + return new Docket(DocumentationType.OAS_30) + .select() + .apis(RequestHandlerSelectors.basePackage("com.ruoyi.promo.controller")) + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + .paths(PathSelectors.any()) + .build() + .groupName("promo") + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()); + } + + /** + * 安全模式,这里指定token通过Authorization请求头传递 + */ + private List securitySchemes() { + List apiKeyList = new ArrayList(); + apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue())); + return apiKeyList; + } + + /** + * 安全上下文 + */ + private List securityContexts() { + List securityContexts = new ArrayList<>(); + securityContexts.add(SecurityContext.builder() + .securityReferences(defaultAuth()) + .operationSelector(o -> o.requestMappingPattern().matches("/.*")) + .build()); + return securityContexts; + } + + /** + * 默认的安全引用 + */ + private List defaultAuth() { + AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + List securityReferences = new ArrayList<>(); + securityReferences.add(new SecurityReference("Authorization", authorizationScopes)); + return securityReferences; + } +} diff --git a/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties b/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..2b23f85 --- /dev/null +++ b/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.json=/com.alibaba.fastjson.*.jar \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml new file mode 100644 index 0000000..d6e5d42 --- /dev/null +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -0,0 +1,261 @@ +# 日志配置 +logging: + level: + com.ruoyi: info + org.springframework: warn +# 项目相关配置 +ruoyi: + # 名称 + name: Skins + # 版本 + version: 3.8.6 + # 版权年份 + copyrightYear: 2023 + # 实例演示开关 + demoEnabled: true + # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) +# profile: D:/ruoyi/uploadPath + profile: /www/static + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数字计算 char 字符验证 + captchaType: math + # 项目全局域名配置 + # domainName: http://192.168.3.25:${server.port} + # domainName: http://api.mkcsgo.com/prod-api + domainName: "" + +# 数据源配置 +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + # 主库数据源 + master: + url: jdbc:mysql://127.0.0.1:23306/changzhou?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: changzhou + password: iTSJSPPZM3LSGAPC + # 从库数据源 + slave: + # 从数据源开关/默认关闭 + enabled: false + url: + username: + password: + # 初始连接数 + initialSize: 20 + # 最小连接池数量 + minIdle: 20 + # 最大连接池数量 + maxActive: 80 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置连接超时时间 + connectTimeout: 30000 + # 配置网络超时时间 + socketTimeout: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + # 配置一个连接在池中最大生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 + # 配置检测连接是否有效 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: ruoyi + login-password: 123456 + filter: + stat: + enabled: true + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + # redis 配置 + data: + redis: + host: 127.0.0.1 + port: 26379 + password: FireSkins@99999 + lettuce: + # 关键配置:指定协议版本 + client-options: + protocol: RESP2 + redis: + # 地址 +# host: 47.106.150.25 + host: 127.0.0.1 + # 端口,默认为6379 + port: 26379 + # 数据库索引 + database: 4 + # 密码 + password: FireSkins@99999 + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + rabbitmq: +# host: 47.106.150.25 + host: 127.0.0.1 + port: 25672 + username: skins + password: mk + virtual-host: /skins + listener: + simple: + acknowledge-mode: MANUAL +# prefetch: 1 #每次从队列中取一个,轮询分发,默认是公平分发 +# retry: +# max-attempts: 5 # 重试次数 +# enabled: true # 开启重试 +# rabbitmqclt change_password admin admin + +#smsbao: +# # 万能接口:p 参数直接传 API Key(无需 MD5),文档:https://www.smsbao.com +# httpUrl: https://api.smsbao.com/sms +# username: jiong9871 +# apiKey: d519fa84ccda45259f5d652631b3822d +# +## 4SMS 短信服务,Header 签名鉴权(HMAC-SHA256),文档:https://4sms.cn +##Key ID: bk_5d01b8d4950317d7 +##Key Secret: 2618cd0a4cf7e0cb19cc81d58556a9b61b71928a139e5046a5ba6024da1708cd +#foursms: +# baseUrl: https://4sms.cn +# keyId: bk_e9f29678e8c1403d +# keySecret: 515d20443502fc62c413cf51be00d649724a197291768e29d835869236d38b8a +# # 默认模板 ID(发送验证码使用),可调用 /admin/foursms/templates 查询 +# templateId: 4 +# # 默认签名 ID(可选,不填则使用模板默认签名) +# signatureId: 4 +# +# 阿里云短信服务,文档:https://help.aliyun.com/document_detail/101414.html +aliyun: + sms: + accessKeyId: LTAI5t5erzmPYVz9Uo89znAx + accessKeySecret: PxiarD7QzFsim7v5ETmDFpY9Cffd0b + signName: 马鞍山市凯科佰娱技术服务 + templateCode: SMS_489940205 + regionId: cn-hangzhou + endpoint: dysmsapi.aliyuncs.com +mkcsgo: + startLoadOrnaments: false +# startLoadOrnaments: true + fight: + roundTime: 6000 + startingDuration: 3000 + revealDuration: 4000 + settledDuration: 1000 + upgrade: + defaultRequired: 160 + anchorDefaultRequired: 100 + +cs340: + baseUrl: https://open.cs2pifa.com + appKey: open_f30b02888ec744529f6e3d3b585d8be6 + privateKey: MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCu7WP8JV18X31Qy4aKiBGZjwGoQY4GPuE82ooEMvwSx6xFaceESxa+ElnMp/V7lRR7hDBBy4EY+X+veavoKarfEou2+f6r4z+exaxHiBiW87mqpV8W63/TsZ2oV03dwaIm3sEXlK+hxdpGhhwVeXSrkkdxSanSFbt/vby5h7kNkh2gJxOB0G9IwRGtyW3UJr9i/trDh+sl9KIafh/Fitv1NgAITu69BCOYr/MfP3O6yY+gzz6+f7RAXVNbe3tYbIZzIZZ022G4L8SyZvoygKgpCC+KdBCzEkFQxHDVOQdPjKgmpSKimU0KTLrEgfdGvE7aVbJv8N8gNKWz1i8a19QJAgMBAAECggEAAhO4hbRt3+hZr71oQLnX4lJj4HsSyXkyqcsl1gyYQ019T/+vCkpCoAl4vn4vYy3Ee1Ojp7cCYT5efpdma/serR0RF/WaSVvelASLL02Vjn6gsZQwGltmflLUE+fMJA9vXPXKe3yyTyyZvbpMQrxZScuSr0ItfYdGMtXuaTzhfMs+f03FtvAWLe4oaL6glpAYJL633UdV5VQIbXDNyMWSgBekxGCoc/HzIVcTB2ZK+t4OV1qqNGc8gOONtdtut96wvxn0VdxDtE6gcIBHSW4tLQDCyaQY5w97LvOspZNJQMoxkCXu7C3tpPkWDsvCaiS9gn5xgM7H3iQiapuKhrl7AQKBgQDTbaWGNpEV70FEyq9WI1uWY4xfY960dwZxaGLUp2tb9dvzhFmAOkzxMBPHn77SGNRq6RgVv20rEoXMBfbdy3vM0letrB2fyEdTAYl535SKwPdVeqFZRcf2tugaLSG7k8MgC9UsohD5/pTtr7eIvsgBymOVHvaLbAQJ+22sWz62yQKBgQDTzdwR25/roTcYFzumBVX516GngzwLiISrI6NAgrpb+Zoi4QeGxCjVipeNbrp2J57h6OVOsmKQJ/TUdDsCVng51Wn6Y/x2I47vibMwChDQ3ClD3FIuaQ09ktWd6G2nflc0ipyB4/XgmAGXfxxjRKCUl8ZAbzSNoTfTrTyoYkaTQQKBgBNtcdZBUVeYooC1tyLNIO9SumVRgfdHMJpfIyWzNJRApTNdJ/Q2ZzeKWWbwcNJRYcNpI8Nr0ZYxn4W9zo9W0Uti0JeuxxIVjyg7bex69OVDEfcSy5yuYs9ywyUUnJLl4vog3De7tJ+kGv80NEMmdK5lcH0VWUY6aKyaI9vAjqSxAoGAXXHrb+2HprufxP8rD9o2zl22MmOR8Dyw25iDhAipE78Ro+XIHksZAO1+Y6XAmjRswpqv4IKPPa6DM+B70FI6BLoFYbUPQ0f43fHQnpcySBN6GThxAYzTjKDloh9TkZwJ+WJWHnBmoEowGZi4+4rhle1ISFFHzFyedsNtiZSiMoECgYAiHkVGEIXe3Af0sPzirWecLHi9vslB/daEzCLhtphQUMff1Eb5oBp3szYAK5Tjyb+PT1g6F3yaNF1iRizkQ6ZshITNO2gZYglQjZNkVytl/dU2X895GzVoQnuyHLEp6GP7itf+sCYo7ySTyyZy0KTQIowAANL9MNohiTmXk7jx5A== + publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAru1j/CVdfF99UMuGiogRmY8BqEGOBj7hPNqKBDL8EsesRWnHhEsWvhJZzKf1e5UUe4QwQcuBGPl/r3mr6Cmq3xKLtvn+q+M/nsWsR4gYlvO5qqVfFut/07GdqFdN3cGiJt7BF5SvocXaRoYcFXl0q5JHcUmp0hW7f728uYe5DZIdoCcTgdBvSMERrclt1Ca/Yv7aw4frJfSiGn4fxYrb9TYACE7uvQQjmK/zHz9zusmPoM8+vn+0QF1TW3t7WGyGcyGWdNthuC/Esmb6MoCoKQgvinQQsxJBUMRw1TkHT4yoJqUioplNCky6xIH3RrxO2lWyb/DfIDSls9YvGtfUCQIDAQAB + callbackPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgtsGLRuPjoc2vi6/Wf8oSW0rJ+nwCEadEBq+IlrE9hGVZCZ2eL+f1x10eDJMQjOG+AdnFnnFr/4CIhBs7JN4fGBR7wqqa4xrSl22WvkhVIQ8fp1EcSJhlSfxuUA+lpoE0GOPXyCi3zf9RgSlWJk9rqaTlTq66xGHFP2pgg/w2zCUT0SVzo6H/TM6cTbYkCQS8n/jxBAQiAbh4osKYYFwkWJgH4qDajWJH1ikEutis+p43xi4rvOSqRtU+Dltq971nuGhz/fujcKcVrxgDbGx6oygaIU+hsvUFLAKxq1cr5YZV205/HVniFz3OYC2UBYOaUuLLDv7ALcZBYf7g9OxRQIDAQAB +# privateKey: MIIEvAIBADANBgkqhkiG9w0BAQEFAA +# publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCA +# callbackPublicKey: MIIBIjANBgkqhkiG9w0BAQ +alipay: + serverUrl: https://openapi.alipay.com/gateway.do + format: JSON + charset: UTF-8 + signType: RSA2 + merchants: + - appId: 2021004130630119 + privateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS + alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC + - appId: 2021003192641091 + privateKey: MIIEvgIBADANB + alipayPublicKey: MIIBIjANBgkqhkiG9w0BA + notifyUrl: http://4qmsbk.natappfree.cc/api/alipay/callBack + realNameAuthenticationReturnUrl: http://xggcsgo.com + authenticationHost: https://jmidcardv1.market.alicloudapi.com + authenticationPath: /idcard/validate + authenticationAppcode: 123 +apispace: + carriersAuthToken: rcpg373exsddqb8p24nixfo6zgdzw35c + carriersAuthUrl: https://eolink.o.apispace.com/carriers/carriers-auth +# +#hipay: +# payUrl: https://xahnob7yqke3.hipay.one/pay/pay/createpay +# recvId: e42835c685f0712b19f27d693ee11f42 +# apiKey: aa1818c04f964043900d21bf38a83768 +# payChannelId1: "610" +# payChannelId2: "616" +# notifyUrl: http://154.12.94.98:8081/api/hipay/notify +# returnUrl: "" +# +#chengdu-hipay: +# baseUrl: https://xahnob7yqke3.hipay.one +# recvId: e7c0f86eff4296c636b8d290300efd29 +# apiKey: 922297c5e1dc44c78e8b4afac7315b16 +# payChannelId: "601" +# notifyUrl: http://191.223.40.222:28081/api/chengduhipay/notify +# returnUrl: "" +#richku: +# baseUrl: http://owsc.jiuwanpay.ggbmxta.com +# memberId: "260662300" +# apiKey: 1wx8i39jeb8yjcpn1t60oltmhezhbxle +# alipayBankcode: "8026" +# unionpayBankcode: "8026" +# notifyUrl: http://191.223.40.222:28081/api/richku/notify +# returnUrl: "" +#itpay: +# baseUrl: http://QCMMUcGCaQ7Y5HobKF4.itjqr.xyz +# mid: "M200022" # 商户号,部署时填写 +# secret: "ccefe5bcd8badde5250b322e7b97fee1" # 商户密钥,部署时填写 +# channelCode: "9999" # 通道编码,测试用9999 +# clientIp: "34.92.5.26" # 会员/回调IP,部署时填写 +# notifyUrl: http://191.223.40.222:28081/api/itpay/notify +# returnUrl: "" +# +#abkpay: +# # 代收API基础地址(下单/查单/余额分别拼接 /Pay_SG.html、/Pay_Query.html、/Pay_Querybalance.html) +# baseUrl: https://t5556qpd.ukudjggujdheja.com +# mchId: "M1784915684" # 商户号 +# secret: "jxmj30j0qx4erqexinnl6dnzty4s0vec" # 商户密钥 +# wayCode: "8012" # 默认产品编码:300<=金额<800(<300 默认路由到 itpay) +# wayCodeHigh: "8003" # 高额产品编码:金额>=800;留空则统一用 wayCode +# clientIp: "43.198.71.126" # 平台白名单IP(服务器出口IP) +# notifyUrl: http://191.223.40.222:28081/api/abkpay/notify +# returnUrl: "" +# +#snappay: +# # 代收API基础地址(下单/查单/余额分别拼接 /api/v2/transactions/deposit、/api/v2/transactions/query、/api/v2/balances) +# baseUrl: https://api.snappay168.store +# merchantId: "m01KYH91E7WXQFASDWNB7YQYRZP" # 商户ID(m 开头字符串),来自 https://web.snappay168.store 商户后台 +# secret: "0c17c0c7986231f31720d20b5ba4b5b1" # 商户密钥(secretKey) +# channel: 900 # 代收通道编码:900=支付宝(number 型别) +# minAmount: 1000 # 充值下限(含) +# maxAmount: 50000 # 充值上限(含) +# emailDomain: "snap-user.com" # 无邮箱用户补全 email 用的默认域名 +# # 回调通知地址;平台白名单/回调IP:136.119.21.224 +# notifyUrl: http://191.223.40.222:28081/api/snappay/notify +# redirectUrl: "" # 付款完成跳转(可空) diff --git a/ruoyi-admin/src/main/resources/application-pro.yml b/ruoyi-admin/src/main/resources/application-pro.yml new file mode 100644 index 0000000..a9f1bd9 --- /dev/null +++ b/ruoyi-admin/src/main/resources/application-pro.yml @@ -0,0 +1,262 @@ +# 日志配置 +logging: + level: + com.ruoyi: info + org.springframework: warn +# 项目相关配置 +ruoyi: + # 名称 + name: Skins + # 版本 + version: 3.8.6 + # 版权年份 + copyrightYear: 2023 + # 实例演示开关 + demoEnabled: true + # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) + # profile: D:/ruoyi/uploadPath + profile: /www/static + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数字计算 char 字符验证 + captchaType: math + # 项目全局域名配置 + # domainName: http://192.168.3.25:${server.port} + # domainName: http://api.mkcsgo.com/prod-api + domainName: + +# 数据源配置 +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + # 主库数据源 + master: + url: jdbc:mysql://127.0.0.1:23306/changzhou?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: changzhou + password: iTSJSPPZM3LSGAPC + # 从库数据源 + slave: + # 从数据源开关/默认关闭 + enabled: false + url: + username: + password: + # 初始连接数 + initialSize: 20 + # 最小连接池数量 + minIdle: 20 + # 最大连接池数量 + maxActive: 80 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置连接超时时间 + connectTimeout: 30000 + # 配置网络超时时间 + socketTimeout: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + # 配置一个连接在池中最大生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 + # 配置检测连接是否有效 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: ruoyi + login-password: 123456 + filter: + stat: + enabled: true + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + # redis 配置 + data: + redis: + host: 127.0.0.1 + port: 26379 + password: FireSkins@99999 + lettuce: + # 关键配置:指定协议版本 + client-options: + protocol: RESP2 + redis: + # 地址 + # host: 47.106.150.25 + host: 127.0.0.1 + # 端口,默认为6379 + port: 26379 + # 数据库索引 + database: 4 + # 密码 + password: FireSkins@99999 + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + rabbitmq: + # host: 47.106.150.25 + host: 127.0.0.1 + port: 25672 + username: skins + password: mk + virtual-host: /skins + listener: + simple: + acknowledge-mode: MANUAL +# prefetch: 1 #每次从队列中取一个,轮询分发,默认是公平分发 +# retry: +# max-attempts: 5 # 重试次数 +# enabled: true # 开启重试 +# rabbitmqclt change_password admin admin + +#smsbao: +# # 万能接口:p 参数直接传 API Key(无需 MD5),文档:https://www.smsbao.com +# httpUrl: https://api.smsbao.com/sms +# username: jiong9871 +# apiKey: d519fa84ccda45259f5d652631b3822d +# +## 4SMS 短信服务,Header 签名鉴权(HMAC-SHA256),文档:https://4sms.cn +##Key ID: bk_5d01b8d4950317d7 +##Key Secret: 2618cd0a4cf7e0cb19cc81d58556a9b61b71928a139e5046a5ba6024da1708cd +#foursms: +# baseUrl: https://4sms.cn +# keyId: bk_e9f29678e8c1403d +# keySecret: 515d20443502fc62c413cf51be00d649724a197291768e29d835869236d38b8a +# # 默认模板 ID(发送验证码使用),可调用 /admin/foursms/templates 查询 +# templateId: 4 +# # 默认签名 ID(可选,不填则使用模板默认签名) +# signatureId: 4 +# +# 阿里云短信服务,文档:https://help.aliyun.com/document_detail/101414.html +aliyun: + sms: + accessKeyId: LTAI5t5erzmPYVz9Uo89znAx + accessKeySecret: PxiarD7QzFsim7v5ETmDFpY9Cffd0b + signName: 马鞍山市凯科佰娱技术服务 + templateCode: SMS_489940205 + regionId: cn-hangzhou + endpoint: dysmsapi.aliyuncs.com + +mkcsgo: + startLoadOrnaments: false + # startLoadOrnaments: true + fight: + roundTime: 6000 + startingDuration: 3000 + revealDuration: 4000 + settledDuration: 1000 + upgrade: + defaultRequired: 160 + anchorDefaultRequired: 100 + +cs340: + baseUrl: https://open.cs2pifa.com + appKey: open_f30b02888ec744529f6e3d3b585d8be6 +# privateKey: MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCu7WP8JV18X31Qy4aKiBGZjwGoQY4GPuE82ooEMvwSx6xFaceESxa+ElnMp/V7lRR7hDBBy4EY+X+veavoKarfEou2+f6r4z+exaxHiBiW87mqpV8W63/TsZ2oV03dwaIm3sEXlK+hxdpGhhwVeXSrkkdxSanSFbt/vby5h7kNkh2gJxOB0G9IwRGtyW3UJr9i/trDh+sl9KIafh/Fitv1NgAITu69BCOYr/MfP3O6yY+gzz6+f7RAXVNbe3tYbIZzIZZ022G4L8SyZvoygKgpCC+KdBCzEkFQxHDVOQdPjKgmpSKimU0KTLrEgfdGvE7aVbJv8N8gNKWz1i8a19QJAgMBAAECggEAAhO4hbRt3+hZr71oQLnX4lJj4HsSyXkyqcsl1gyYQ019T/+vCkpCoAl4vn4vYy3Ee1Ojp7cCYT5efpdma/serR0RF/WaSVvelASLL02Vjn6gsZQwGltmflLUE+fMJA9vXPXKe3yyTyyZvbpMQrxZScuSr0ItfYdGMtXuaTzhfMs+f03FtvAWLe4oaL6glpAYJL633UdV5VQIbXDNyMWSgBekxGCoc/HzIVcTB2ZK+t4OV1qqNGc8gOONtdtut96wvxn0VdxDtE6gcIBHSW4tLQDCyaQY5w97LvOspZNJQMoxkCXu7C3tpPkWDsvCaiS9gn5xgM7H3iQiapuKhrl7AQKBgQDTbaWGNpEV70FEyq9WI1uWY4xfY960dwZxaGLUp2tb9dvzhFmAOkzxMBPHn77SGNRq6RgVv20rEoXMBfbdy3vM0letrB2fyEdTAYl535SKwPdVeqFZRcf2tugaLSG7k8MgC9UsohD5/pTtr7eIvsgBymOVHvaLbAQJ+22sWz62yQKBgQDTzdwR25/roTcYFzumBVX516GngzwLiISrI6NAgrpb+Zoi4QeGxCjVipeNbrp2J57h6OVOsmKQJ/TUdDsCVng51Wn6Y/x2I47vibMwChDQ3ClD3FIuaQ09ktWd6G2nflc0ipyB4/XgmAGXfxxjRKCUl8ZAbzSNoTfTrTyoYkaTQQKBgBNtcdZBUVeYooC1tyLNIO9SumVRgfdHMJpfIyWzNJRApTNdJ/Q2ZzeKWWbwcNJRYcNpI8Nr0ZYxn4W9zo9W0Uti0JeuxxIVjyg7bex69OVDEfcSy5yuYs9ywyUUnJLl4vog3De7tJ+kGv80NEMmdK5lcH0VWUY6aKyaI9vAjqSxAoGAXXHrb+2HprufxP8rD9o2zl22MmOR8Dyw25iDhAipE78Ro+XIHksZAO1+Y6XAmjRswpqv4IKPPa6DM+B70FI6BLoFYbUPQ0f43fHQnpcySBN6GThxAYzTjKDloh9TkZwJ+WJWHnBmoEowGZi4+4rhle1ISFFHzFyedsNtiZSiMoECgYAiHkVGEIXe3Af0sPzirWecLHi9vslB/daEzCLhtphQUMff1Eb5oBp3szYAK5Tjyb+PT1g6F3yaNF1iRizkQ6ZshITNO2gZYglQjZNkVytl/dU2X895GzVoQnuyHLEp6GP7itf+sCYo7ySTyyZy0KTQIowAANL9MNohiTmXk7jx5A== +# publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAru1j/CVdfF99UMuGiogRmY8BqEGOBj7hPNqKBDL8EsesRWnHhEsWvhJZzKf1e5UUe4QwQcuBGPl/r3mr6Cmq3xKLtvn+q+M/nsWsR4gYlvO5qqVfFut/07GdqFdN3cGiJt7BF5SvocXaRoYcFXl0q5JHcUmp0hW7f728uYe5DZIdoCcTgdBvSMERrclt1Ca/Yv7aw4frJfSiGn4fxYrb9TYACE7uvQQjmK/zHz9zusmPoM8+vn+0QF1TW3t7WGyGcyGWdNthuC/Esmb6MoCoKQgvinQQsxJBUMRw1TkHT4yoJqUioplNCky6xIH3RrxO2lWyb/DfIDSls9YvGtfUCQIDAQAB +# callbackPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgtsGLRuPjoc2vi6/Wf8oSW0rJ+nwCEadEBq+IlrE9hGVZCZ2eL+f1x10eDJMQjOG+AdnFnnFr/4CIhBs7JN4fGBR7wqqa4xrSl22WvkhVIQ8fp1EcSJhlSfxuUA+lpoE0GOPXyCi3zf9RgSlWJk9rqaTlTq66xGHFP2pgg/w2zCUT0SVzo6H/TM6cTbYkCQS8n/jxBAQiAbh4osKYYFwkWJgH4qDajWJH1ikEutis+p43xi4rvOSqRtU+Dltq971nuGhz/fujcKcVrxgDbGx6oygaIU+hsvUFLAKxq1cr5YZV205/HVniFz3OYC2UBYOaUuLLDv7ALcZBYf7g9OxRQIDAQAB + privateKey: MIIEvAIBADANBgkqhkiG9w0BAQEFAA + publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCA + callbackPublicKey: MIIBIjANBgkqhkiG9w0BAQ +alipay: + serverUrl: https://openapi.alipay.com/gateway.do + format: JSON + charset: UTF-8 + signType: RSA2 + merchants: + - appId: 2021004130630119 + privateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS + alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC + - appId: 2021003192641091 + privateKey: MIIEvgIBADANB + alipayPublicKey: MIIBIjANBgkqhkiG9w0BA + notifyUrl: http://4qmsbk.natappfree.cc/api/alipay/callBack + realNameAuthenticationReturnUrl: http://xggcsgo.com + authenticationHost: https://jmidcardv1.market.alicloudapi.com + authenticationPath: /idcard/validate + authenticationAppcode: 123 +apispace: + carriersAuthToken: rcpg373exsddqb8p24nixfo6zgdzw35c + carriersAuthUrl: https://eolink.o.apispace.com/carriers/carriers-auth +# +#hipay: +# payUrl: https://xahnob7yqke3.hipay.one/pay/pay/createpay +# recvId: e42835c685f0712b19f27d693ee11f42 +# apiKey: aa1818c04f964043900d21bf38a83768 +# payChannelId1: "610" +# payChannelId2: "616" +# notifyUrl: https://api.c1cscs2026.com/api/hipay/notify +# returnUrl: "" +# +#chengdu-hipay: +# baseUrl: https://xahnob7yqke3.hipay.one +# recvId: e7c0f86eff4296c636b8d290300efd29 +# apiKey: 922297c5e1dc44c78e8b4afac7315b16 +# payChannelId: "601" +# notifyUrl: https://api.c1cscs2026.com/api/chengduhipay/notify +# returnUrl: "" +#richku: +# baseUrl: http://owsc.jiuwanpay.ggbmxta.com +# memberId: "260662300" +# apiKey: 1wx8i39jeb8yjcpn1t60oltmhezhbxle +# alipayBankcode: "8026" +# unionpayBankcode: "8026" +# notifyUrl: https://api.c1cscs2026.com/api/richku/notify +# returnUrl: "" +#itpay: +# baseUrl: http://QCMMUcGCaQ7Y5HobKF4.itjqr.xyz +# mid: "M200022" # 商户号,部署时填写 +# secret: "ccefe5bcd8badde5250b322e7b97fee1" # 商户密钥,部署时填写 +# channelCode: "9999" # 通道编码,测试用9999 +# clientIp: "34.92.5.26" # 会员/回调IP,部署时填写 +# notifyUrl: https://api.c1cscs2026.com/api/itpay/notify +# returnUrl: "" +# +#abkpay: +# # 代收API基础地址(下单/查单/余额分别拼接 /Pay_SG.html、/Pay_Query.html、/Pay_Querybalance.html) +# baseUrl: https://t5556qpd.ukudjggujdheja.com +# mchId: "M1784915684" # 商户号 +# secret: "jxmj30j0qx4erqexinnl6dnzty4s0vec" # 商户密钥 +# wayCode: "8012" # 默认产品编码:300<=金额<800(<300 默认路由到 itpay) +# wayCodeHigh: "8003" # 高额产品编码:金额>=800;留空则统一用 wayCode +# clientIp: "43.198.71.126" # 平台白名单IP(服务器出口IP) +# notifyUrl: https://api.c1cscs2026.com/api/abkpay/notify +# returnUrl: "" +# +#snappay: +# # 代收API基础地址(下单/查单/余额分别拼接 /api/v2/transactions/deposit、/api/v2/transactions/query、/api/v2/balances) +# baseUrl: https://api.snappay168.store +# merchantId: "m01KYH91E7WXQFASDWNB7YQYRZP" # 商户ID(m 开头字符串),来自 https://web.snappay168.store 商户后台 +# secret: "0c17c0c7986231f31720d20b5ba4b5b1" # 商户密钥(secretKey) +# channel: 900 # 代收通道编码:900=支付宝(number 型别) +# minAmount: 1000 # 充值下限(含) +# maxAmount: 50000 # 充值上限(含) +# emailDomain: "snap-user.com" # 无邮箱用户补全 email 用的默认域名 +# # 回调通知地址;平台白名单/回调IP:136.119.21.224(需确保该域名解析/路由到我方服务器) +# notifyUrl: https://api.c1cscs2026.com/api/snappay/notify +# redirectUrl: "" # 付款完成跳转(可空) diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml new file mode 100644 index 0000000..22adf6c --- /dev/null +++ b/ruoyi-admin/src/main/resources/application.yml @@ -0,0 +1,122 @@ +# 开发环境配置 +server: + # 服务器的HTTP端口,默认为8080 + port: 28081 + address: 0.0.0.0 + servlet: + # 应用的访问路径 + context-path: / + tomcat: + # tomcat的URI编码 + uri-encoding: UTF-8 + # 连接数满后的排队数,默认为100 + accept-count: 1000 + threads: + # tomcat最大线程数,默认为200 + max: 800 + # Tomcat启动初始化的线程数,默认值10 + min-spare: 100 + +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 + +# Spring配置 +spring: + threads: + virtual: + enabled: true + jackson: + time-zone: "Asia/Shanghai" + date-format: yyyy-MM-dd HH:mm:ss + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + profiles: + # 开发环境配置 dev test pro ---------------------------------------------------------------------------------------- + active: dev + # 文件上传 + servlet: + multipart: + # 单个文件大小 + max-file-size: 10MB + # 设置总上传的文件大小 + max-request-size: 20MB + # 服务模块 + devtools: + restart: + # 热部署开关 ------------------------------------------------------------------------------------------------------- + enabled: false + +# token配置 +token: + # 令牌自定义标识 + header: Authorization + # 令牌密钥 + secret: abcdefghijklmnopqrstuvwzxy + # 玩家端令牌有效期(30天) + expireTime: 43200 + # 后台管理员令牌有效期(24小时,每次请求自动续期) + adminExpireTime: 1440 + +# MyBatis/mybatis-plus 配置 +mybatis-plus: + # 搜索指定包别名 + typeAliasesPackage: com.ruoyi.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 加载全局的配置文件 + configLocation: classpath:mybatis/mybatis-config.xml + global-config: + db-config: + logic-delete-field: delFlag + logic-delete-value: 2 + logic-not-delete-value: 0 + id-type: auto + +# PageHelper分页插件 +pagehelper: + helperDialect: mysql + supportMethodsArguments: true + params: count=countSql + +# AES 加解密配置 +aes: + # 全局加解密开关(true=开启,false=关闭;生产环境建议开启) + enabled: false + # AES 密鑰(16位,与前端 AES_KEY 保持一致) + key: ruoyi@aes@202416 + # AES 向量(16位,与前端 AES_IV 保持一致) + iv: ruoyi@aes@iv2024 + +# 防止XSS攻击 +xss: + # 过滤开关 + enabled: true + # 排除链接(多个用逗号分隔) + excludes: /system/notice + # 匹配链接 + urlPatterns: /system/*,/monitor/*,/tool/* + +# 行为验证码配置 +captcha: + # Redis key前缀 + prefix: captcha + # 验证码过期时间(毫秒) + expire: + default: 120000 + # 使用系统自带资源 + init-default-resource: true + # 本地预生成缓存:后台线程提前生成验证码,请求时直接返回缓存数据,避免实时图像处理 + local-cache-enabled: true + # 预生成缓存数量 + local-cache-size: 20 + # 缓存为空时等待时间(毫秒),超过此时间仍未拿到缓存则直接实时生成 + local-cache-wait-time: 3000 + # 缓存补充检查间隔(毫秒),后台线程定期检查并补充缓存 + local-cache-period: 2000 diff --git a/ruoyi-admin/src/main/resources/banner.txt b/ruoyi-admin/src/main/resources/banner.txt new file mode 100644 index 0000000..fee1ce3 --- /dev/null +++ b/ruoyi-admin/src/main/resources/banner.txt @@ -0,0 +1,2 @@ +Application Version: ${ruoyi.version} +Spring Boot Version: ${spring-boot.version} \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/i18n/messages.properties b/ruoyi-admin/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..93de005 --- /dev/null +++ b/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -0,0 +1,38 @@ +#错误消息 +not.null=* 必须填写 +user.jcaptcha.error=验证码错误 +user.jcaptcha.expire=验证码已失效 +user.not.exists=用户不存在/密码错误 +user.password.not.match=用户不存在/密码错误 +user.password.retry.limit.count=密码输入错误{0}次 +user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.password.delete=对不起,您的账号已被删除 +user.blocked=用户已封禁,请联系管理员 +role.blocked=角色已封禁,请联系管理员 +login.blocked=很遗憾,访问IP已被列入系统黑名单 +user.logout.success=退出成功 + +length.not.valid=长度必须在{min}到{max}个字符之间 + +user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头 +user.password.not.valid=* 5-50个字符 + +user.email.not.valid=邮箱格式错误 +user.mobile.phone.number.not.valid=手机号格式错误 +user.login.success=登录成功 +user.register.success=注册成功 +user.notfound=请重新登录 +user.forcelogout=管理员强制退出,请重新登录 +user.unknown.error=未知错误,请重新登录 + +##文件上传消息 +upload.exceed.maxSize=上传的文件大小超出限制的文件大小!
允许的文件最大大小是:{0}MB! +upload.filename.exceed.length=上传的文件名最长{0}个字符 + +##权限 +no.permission=您没有数据的权限,请联系管理员添加权限 [{0}] +no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}] +no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}] +no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] +no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] +no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] diff --git a/ruoyi-admin/src/main/resources/logback.xml b/ruoyi-admin/src/main/resources/logback.xml new file mode 100644 index 0000000..9a5e8e4 --- /dev/null +++ b/ruoyi-admin/src/main/resources/logback.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/sys-info.log + + + + ${log.path}/sys-info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/sys-error.log + + + + ${log.path}/sys-error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + ${log.path}/sys-user.log + + + ${log.path}/sys-user.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml b/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml new file mode 100644 index 0000000..ad337e9 --- /dev/null +++ b/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ruoyi-admin/src/test/java/com/ruoyi/audit/UserOperationAuditTest.java b/ruoyi-admin/src/test/java/com/ruoyi/audit/UserOperationAuditTest.java new file mode 100644 index 0000000..c368410 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/audit/UserOperationAuditTest.java @@ -0,0 +1,291 @@ +package com.ruoyi.audit; + +import java.lang.reflect.Proxy; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.admin.controller.AdminBlankCardKeyController; +import com.ruoyi.admin.controller.LossRebateController; +import com.ruoyi.admin.controller.TtCardMerchantController; +import com.ruoyi.admin.controller.TtUserController; +import com.ruoyi.admin.mapper.TtLossRebateMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.domain.entity.TtLossRebate; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.framework.aspectj.LogAspect; +import com.ruoyi.system.domain.SysOperLog; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.Signature; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import static org.junit.jupiter.api.Assertions.*; + +/** No application boot, live DB, scheduler or real writes; actual advice + concrete mapper proxy + BeanFactory. */ +public class UserOperationAuditTest +{ + public static void main(String[] args) throws Exception { + new UserOperationAuditTest().adviceCapturesPersistedBeforeAfterAndCleansUp(); + new UserOperationAuditTest().idFieldResolvesIdWhenPrimaryKeyIsNotAnEntityArgument(); + new UserOperationAuditTest().scalarParamsAreCapturedForCreateActions(); + System.out.println("UserOperationAuditTest PASSED (actual advice + BeanFactory + mapper proxy)"); + } + static class RecordingAspect extends LogAspect { + List rows = new ArrayList<>(); + @Override protected void saveLog(SysOperLog row) { rows.add(row); } + } + + @Test + public void adviceCapturesPersistedBeforeAfterAndCleansUp() throws Exception + { + DefaultListableBeanFactory beans = new DefaultListableBeanFactory(); + TtUser[] database = { user("100.00", "50") }; + boolean[] unavailable = { false }; + TtUserMapper mapper = (TtUserMapper) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{TtUserMapper.class}, + (proxy, method, args) -> { + if (method.getName().equals("selectById")) { + if (unavailable[0]) throw new IllegalStateException("secret-error"); + return database[0]; + } + return null; + }); + beans.registerSingleton("userMapper", mapper); + // A second BaseMapper bean makes the old getBean(BaseMapper) implementation fail. + beans.registerSingleton("otherMapper", Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[]{com.baomidou.mybatisplus.core.mapper.BaseMapper.class}, (p,m,a) -> null)); + new SpringUtils().postProcessBeanFactory(beans); + LoginUser principal = new LoginUser(); + com.ruoyi.common.core.domain.entity.SysUser admin = new com.ruoyi.common.core.domain.entity.SysUser(); + admin.setUserName("audit-admin"); principal.setUser(admin); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(principal, null)); + MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/admin/user"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + try { + RecordingAspect aspect = new RecordingAspect(); + Log annotation = TtUserController.class.getMethod("edit", TtUser.class).getAnnotation(Log.class); + assertEquals(TtUserMapper.class, annotation.mapperClass()); + TtUser partial = new TtUser(); partial.setUserId(18); partial.setAccountAmount(new BigDecimal("999")); + JoinPoint point = point(partial); + aspect.boBefore(point, annotation); + // Persisted state differs from submitted 999; same mutable row proves snapshot is detached. + database[0].setAccountAmount(new BigDecimal("200")); + database[0].setAccountCredits(new BigDecimal("80")); + database[0].setRemark("new-plain-password"); + database[0].setUpdateTime(new java.util.Date()); + aspect.doAfterReturning(point, annotation, AjaxResult.success()); + JSONObject result = payload(aspect); + assertEquals("changed", result.getString("state")); + assertEquals(2, result.getJSONArray("changes").size()); + assertEquals("100", result.getJSONArray("changes").getJSONObject(0).getString("before")); + assertEquals("200", result.getJSONArray("changes").getJSONObject(0).getString("after")); + assertEquals("50", result.getJSONArray("changes").getJSONObject(1).getString("before")); + assertEquals("80", result.getJSONArray("changes").getJSONObject(1).getString("after")); + assertEquals(18, result.getJSONObject("target").getIntValue("id")); + assertEquals("测试用户", result.getJSONObject("target").getString("name")); + assertFalse(result.toJSONString().contains("password")); + assertFalse(result.toJSONString().contains("999")); + assertFalse(result.toJSONString().contains("updateTime")); + + Log invite = TtUserController.class.getMethod("editInvitationCode", TtUser.class).getAnnotation(Log.class); + aspect.boBefore(point, invite); + database[0].setInvitationCode("NEW"); + aspect.doAfterReturning(point, invite, AjaxResult.success()); + assertEquals(1, payload(aspect).getJSONArray("changes").size()); + assertEquals("invitationCode", payload(aspect).getJSONArray("changes").getJSONObject(0).getString("field")); + + aspect.boBefore(point, annotation); + aspect.doAfterReturning(point, annotation, AjaxResult.success()); + assertEquals("unchanged", payload(aspect).getString("state")); + for (Object failure : new Object[]{AjaxResult.error("plain-secret"), R.fail("plain-secret")}) { + aspect.boBefore(point, annotation); + aspect.doAfterReturning(point, annotation, failure); + assertEquals(1, aspect.rows.get(aspect.rows.size()-1).getStatus()); + assertEquals("failed", payload(aspect).getString("state")); + assertEquals(0, payload(aspect).getJSONArray("changes").size()); + } + aspect.boBefore(point, annotation); + aspect.doAfterThrowing(point, annotation, new IllegalStateException("secret-error")); + assertEquals("failed", payload(aspect).getString("state")); + // No @Before means no snapshot: previous thread state must not leak. + aspect.doAfterReturning(point, annotation, AjaxResult.success()); + assertEquals("unavailable", payload(aspect).getString("state")); + assertTrue(payload(aspect).getJSONObject("target").isEmpty()); + unavailable[0] = true; + aspect.boBefore(point, annotation); + aspect.doAfterReturning(point, annotation, AjaxResult.success()); + assertEquals("unavailable", payload(aspect).getString("state")); + unavailable[0] = false; + aspect.boBefore(point, annotation); + unavailable[0] = true; + aspect.doAfterReturning(point, annotation, AjaxResult.success()); + assertEquals("unavailable", payload(aspect).getString("state")); + unavailable[0] = false; + aspect.boBefore(point, annotation); + database[0].setInvitationCode(null); + aspect.doAfterReturning(point, annotation, AjaxResult.success()); + assertEquals("空", payload(aspect).getJSONArray("changes").getJSONObject(0).getString("after")); + aspect.boBefore(point, annotation); + database[0].setAccountAmount(new BigDecimal("200.00")); + aspect.doAfterReturning(point, annotation, AjaxResult.success()); + assertEquals("unchanged", payload(aspect).getString("state")); + aspect.boBefore(point, annotation); + database[0].setNickName("长".repeat(3000)); + aspect.doAfterReturning(point, annotation, AjaxResult.success()); + assertTrue(payload(aspect).getBooleanValue("truncated")); + for (SysOperLog row : aspect.rows) { + assertTrue(row.getOperParam().length() <= 2000); + assertFalse(row.getOperParam().contains("plain-secret")); + assertFalse(row.getOperParam().contains("secret-error")); + assertNull(row.getJsonResult()); + } + } finally { SecurityContextHolder.clearContext(); RequestContextHolder.resetRequestAttributes(); } + } + + /** 主键不在实体参数里时(请求体 Map / DTO getter),@Log(idField) 也要能取到改前数据 */ + @Test + public void idFieldResolvesIdWhenPrimaryKeyIsNotAnEntityArgument() throws Exception + { + DefaultListableBeanFactory beans = new DefaultListableBeanFactory(); + TtUser[] userDb = { user("100.00", "50") }; + userDb[0].setMerchantBalance(new BigDecimal("200")); + TtLossRebate[] rebateDb = { new TtLossRebate() }; + rebateDb[0].setId(7L); + registerMapper(beans, TtUserMapper.class, "userMapper", userDb); + registerMapper(beans, TtLossRebateMapper.class, "lossRebateMapper", rebateDb); + new SpringUtils().postProcessBeanFactory(beans); + prepareContext("POST", "/admin/cardMerchant/recharge"); + try + { + RecordingAspect aspect = new RecordingAspect(); + + // 请求体是 Map,userId 藏在 key 里(卡商充值) + Log recharge = TtCardMerchantController.class + .getMethod("cardMerchantRecharge", Map.class).getAnnotation(Log.class); + assertEquals("userId", recharge.idField()); + Map body = new HashMap<>(); + body.put("userId", 18); + body.put("amount", 100); + JoinPoint mapPoint = pointWithArgs(body); + aspect.boBefore(mapPoint, recharge); + userDb[0].setMerchantBalance(new BigDecimal("300")); + aspect.doAfterReturning(mapPoint, recharge, AjaxResult.success()); + JSONObject mapResult = payload(aspect); + assertEquals(18, mapResult.getJSONObject("target").getIntValue("id")); + assertEquals("changed", mapResult.getString("state")); + assertEquals("200", mapResult.getJSONArray("changes").getJSONObject(0).getString("before")); + assertEquals("300", mapResult.getJSONArray("changes").getJSONObject(0).getString("after")); + + // 主键在 DTO 的 getter 上(亏损返水人工发放) + Log grant = LossRebateController.class + .getMethod("grant", LossRebateController.GrantRequest.class).getAnnotation(Log.class); + assertEquals("id", grant.idField()); + LossRebateController.GrantRequest request = new LossRebateController.GrantRequest(); + request.setId(7L); + request.setAmount(new BigDecimal("66")); + JoinPoint dtoPoint = pointWithArgs(request); + aspect.boBefore(dtoPoint, grant); + aspect.doAfterReturning(dtoPoint, grant, R.ok("已发放")); + assertEquals(7, payload(aspect).getJSONObject("target").getIntValue("id")); + } + finally { SecurityContextHolder.clearContext(); RequestContextHolder.resetRequestAttributes(); } + } + + /** 只有标量入参、没有可比字段的创建类操作,也要记下"生成了几张/哪个产品" */ + @Test + public void scalarParamsAreCapturedForCreateActions() throws Exception + { + prepareContext("POST", "/admin/blankCardKey/generate"); + try + { + RecordingAspect aspect = new RecordingAspect(); + Log generate = AdminBlankCardKeyController.class + .getMethod("generate", Map.class).getAnnotation(Log.class); + assertEquals("生成空白卡密", generate.action()); + Map body = new HashMap<>(); + body.put("rechargeListId", 5); + body.put("num", 100); + body.put("password", "must-not-appear"); + JoinPoint point = pointWithArgs(body); + aspect.boBefore(point, generate); + aspect.doAfterReturning(point, generate, R.ok("ok")); + JSONObject result = payload(aspect); + assertEquals("notCaptured", result.getString("state")); + JSONObject params = result.getJSONObject("params"); + assertNotNull(params, result.toJSONString()); + assertEquals("5", params.getString("rechargeListId")); + assertEquals("100", params.getString("num")); + assertFalse(params.containsKey("password")); + assertFalse(result.toJSONString().contains("must-not-appear")); + } + finally { SecurityContextHolder.clearContext(); RequestContextHolder.resetRequestAttributes(); } + } + + /** 注册一个 selectById 恒返回 db[0] 的 Mapper 代理 */ + private static void registerMapper(DefaultListableBeanFactory beans, Class type, String name, Object[] db) + { + beans.registerSingleton(name, Proxy.newProxyInstance(UserOperationAuditTest.class.getClassLoader(), + new Class[]{type}, (p, m, a) -> m.getName().equals("selectById") ? db[0] : null)); + } + + private static void prepareContext(String httpMethod, String uri) + { + LoginUser principal = new LoginUser(); + com.ruoyi.common.core.domain.entity.SysUser admin = new com.ruoyi.common.core.domain.entity.SysUser(); + admin.setUserName("audit-admin"); + principal.setUser(admin); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(principal, null)); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest(httpMethod, uri))); + } + + private static JoinPoint pointWithArgs(Object... args) + { + Signature signature = (Signature) Proxy.newProxyInstance(UserOperationAuditTest.class.getClassLoader(), + new Class[]{Signature.class}, (p,m,a) -> m.getName().equals("getName") ? "call" : null); + return (JoinPoint) Proxy.newProxyInstance(UserOperationAuditTest.class.getClassLoader(), new Class[]{JoinPoint.class}, + (p,m,a) -> { + switch (m.getName()) { + case "getArgs": return args; + case "getTarget": return UserOperationAuditTest.class; + case "getSignature": return signature; + default: return null; + } + }); + } + + private static TtUser user(String amount, String credits) { + TtUser user = new TtUser(); user.setUserId(18); user.setNickName("测试用户"); + user.setAccountAmount(new BigDecimal(amount)); user.setAccountCredits(new BigDecimal(credits)); + user.setPassword("hash-secret"); user.setSafePasswordPlain("plain-secret"); + user.setRemark("plain-password"); user.setTransactionLink("https://example/?token=secret"); + user.setInvitationCode("OLD"); return user; + } + private static JSONObject payload(RecordingAspect aspect) { + return JSON.parseObject(aspect.rows.get(aspect.rows.size()-1).getOperParam()); + } + private static JoinPoint point(TtUser request) { + Signature signature = (Signature) Proxy.newProxyInstance(UserOperationAuditTest.class.getClassLoader(), + new Class[]{Signature.class}, (p,m,a) -> m.getName().equals("getName") ? "edit" : null); + return (JoinPoint) Proxy.newProxyInstance(UserOperationAuditTest.class.getClassLoader(), new Class[]{JoinPoint.class}, + (p,m,a) -> { + switch (m.getName()) { + case "getArgs": return new Object[]{request}; + case "getTarget": return request; + case "getSignature": return signature; + default: return null; + } + }); + } +} diff --git a/ruoyi-admin/src/test/java/com/ruoyi/thirdparty/cs340/task/Cs340ItemPullTaskRunner.java b/ruoyi-admin/src/test/java/com/ruoyi/thirdparty/cs340/task/Cs340ItemPullTaskRunner.java new file mode 100644 index 0000000..5584995 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/thirdparty/cs340/task/Cs340ItemPullTaskRunner.java @@ -0,0 +1,32 @@ +package com.ruoyi.thirdparty.cs340.task; + +import com.ruoyi.RuoYiApplication; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * CS340 拉取饰品任务本地手动验证(main 方式启动) + *

+ * 由于 IDEA 2025.1.3 与 Spring Boot 4.0 自带的 JUnit 6 存在兼容问题 + * (NoSuchMethodError: MethodSelector.getMethodParameterTypes), + * 这里改用 main 方法启动 Spring 容器后取 Bean 调用任务,规避 IDEA 的 JUnit Runner。 + *

+ * 用法:在 IDEA 里右键此类的 main 方法点 Run 即可。 + * 注意:会真实读 / 写 application-dev.yml 对应数据库的 tt_ornament 表, + * 请确认连的是 dev 库(不是生产库)后再执行。 + */ +public class Cs340ItemPullTaskRunner { + + public static void main(String[] args) { + // 加载 application-dev.yml + System.setProperty("spring.profiles.active", "dev"); + + ConfigurableApplicationContext ctx = SpringApplication.run(RuoYiApplication.class, args); + try { + Cs340ItemPullTask task = ctx.getBean(Cs340ItemPullTask.class); + task.getCs340OrnamentsData(); + } finally { + ctx.close(); + } + } +} diff --git a/ruoyi-admin/src/test/java/com/ruoyi/thirdparty/cs340/task/Cs340PullAllOrnamentsRunner.java b/ruoyi-admin/src/test/java/com/ruoyi/thirdparty/cs340/task/Cs340PullAllOrnamentsRunner.java new file mode 100644 index 0000000..f92b611 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/thirdparty/cs340/task/Cs340PullAllOrnamentsRunner.java @@ -0,0 +1,179 @@ +package com.ruoyi.thirdparty.cs340.task; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.RuoYiApplication; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.cs340.domain.result.BatchGetOnSaleCommodityInfoResult; +import com.ruoyi.thirdparty.cs340.domain.result.Cs340TemplateItem; +import com.ruoyi.thirdparty.cs340.service.Cs340Service; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * CS340「清空并全量拉取第三方饰品」本地手动验证(main 方式启动)。 + *

+ * 背景:既有定时任务 {@link Cs340ItemPullTask#getCs340OrnamentsData()} 只会“根据本地 tt_ornament + * 已有饰品的 hashName 去第三方刷新价格/上下架”,并不会把第三方“全部饰品”拉过来(本地库为空时直接返回)。 + *

+ * 本类的流程(先拉取、后清空,避免拉取失败却清空了本地表): + *

    + *
  1. 调用 {@link Cs340Service#queryAllTemplates()} 拉取第三方全部模板(饰品);
  2. + *
  3. 分批调用 batchGetOnSaleCommodityInfo 查询在售价格/库存;
  4. + *
  5. 组装完整饰品记录(在售→上架并计算商城价,未在售→下架);
  6. + *
  7. 清空原有饰品后批量入库。
  8. + *
+ *

+ * 说明:与既有 Runner 一致,采用 main 方法启动 Spring 容器后取 Bean 调用,规避 IDEA 与 JUnit 6 的兼容问题。 + * 用法:在 IDEA 里右键此类的 main 方法点 Run 即可。 + * 注意:会真实读/写 application-dev.yml 对应数据库的 tt_ornament 表,并且会**清空**该表后重新导入, + * 请务必确认连接的是 dev 库(不是生产库)后再执行。 + */ +public class Cs340PullAllOrnamentsRunner { + + /** 商城价上限,超过则视为异常价格,不上架(与既有同步任务保持一致) */ + private static final BigDecimal STORE_PRICE_MAX = new BigDecimal("99999999.99"); + + /** batchGetOnSaleCommodityInfo 单次最多 200 个 hashName */ + private static final int QUERY_BATCH_SIZE = 200; + + /** 批量入库分片大小 */ + private static final int SAVE_BATCH_SIZE = 500; + + public static void main(String[] args) { + System.setProperty("spring.profiles.active", "dev"); + ConfigurableApplicationContext ctx = SpringApplication.run(RuoYiApplication.class, args); + try { + Cs340Service cs340Service = ctx.getBean(Cs340Service.class); + TtOrnamentService ornamentService = ctx.getBean(TtOrnamentService.class); + ISysConfigService configService = ctx.getBean(ISysConfigService.class); + + BigDecimal premium = new BigDecimal(configService.selectConfigByKey("usePricePremiumRate")); + BigDecimal parities = new BigDecimal(configService.selectConfigByKey("ZBTParities")); + + // 1. 先拉取第三方全部模板;失败则不清空本地表 + List templates = cs340Service.queryAllTemplates(); + if (templates == null || templates.isEmpty()) { + System.err.println("[cs340-pull-all] 未拉取到任何模板,已终止(不清空本地饰品表)"); + return; + } + System.out.println("[cs340-pull-all] 第三方模板总数:" + templates.size()); + + // 2. 去重(按 hashName 规范化后去重,避免大小写/空格导致主键冲突) + LinkedHashMap templateMap = new LinkedHashMap<>(); + for (Cs340TemplateItem t : templates) { + if (t == null || t.getId() == null) { + continue; + } + String hash = t.getHashName(); + if (hash == null || hash.trim().isEmpty()) { + continue; + } + String normalized = hash.trim().toLowerCase(); + templateMap.putIfAbsent(normalized, t); + } + System.out.println("[cs340-pull-all] 去重后有效模板数:" + templateMap.size()); + + // 3. 分批查询在售价格/库存 + List hashNames = new ArrayList<>(templateMap.keySet()); + Map onSaleMap = new HashMap<>(); + for (int i = 0; i < hashNames.size(); i += QUERY_BATCH_SIZE) { + int end = Math.min(i + QUERY_BATCH_SIZE, hashNames.size()); + List batch = hashNames.subList(i, end); + List infos; + try { + infos = cs340Service.batchGetOnSaleCommodityInfoResult(batch); + } catch (Exception e) { + System.err.println("[cs340-pull-all] 批次查询在售信息异常,跳过, size=" + batch.size() + ", err=" + e.getMessage()); + continue; + } + if (infos == null) { + continue; + } + for (BatchGetOnSaleCommodityInfoResult info : infos) { + if (info == null || info.getSaleTemplateResponse() == null || info.getSaleCommodityResponse() == null) { + continue; + } + onSaleMap.put(info.getSaleTemplateResponse().getTemplateHashName(), info); + } + System.out.println("[cs340-pull-all] 已查询在售信息 " + end + "/" + hashNames.size()); + } + System.out.println("[cs340-pull-all] 第三方在售饰品数:" + onSaleMap.size()); + + // 4. 组装完整饰品记录 + Date now = DateUtils.getNowDate(); + int listed = 0; + List toSave = new ArrayList<>(templateMap.size()); + for (Cs340TemplateItem t : templateMap.values()) { + TtOrnament o = TtOrnament.builder().build(); + o.setId(t.getId()); + String hashName = t.getHashName().trim(); + o.setMarketHashName(hashName); + o.setName(t.getName() != null ? t.getName().trim() : null); + o.setTypeName(t.getTypeName()); + o.setTypeHashName(t.getTypeHashName()); + o.setExteriorName(Cs340ItemPullTask.extractExteriorName(t.getName())); // 先从 name 提取磨损度 + o.setIsProprietaryProperty("1"); // 非本网站自定义道具 + o.setCreateTime(now); + o.setUpdateTime(now); + + BatchGetOnSaleCommodityInfoResult info = onSaleMap.get(t.getHashName()); + boolean onSale = false; + if (info != null && info.getSaleCommodityResponse() != null + && info.getSaleCommodityResponse().getMinSellPrice() != null) { + BigDecimal price = new BigDecimal(info.getSaleCommodityResponse().getMinSellPrice()); + BigDecimal storePrice = price.multiply(parities).multiply(premium); + if (storePrice.compareTo(STORE_PRICE_MAX) <= 0) { + int quantity = info.getSaleCommodityResponse().getSellNum() != null + ? Integer.parseInt(info.getSaleCommodityResponse().getSellNum()) : 0; + o.setPrice(price); + o.setQuantity(quantity); + o.setStorePrice(storePrice); + o.setUsePrice(storePrice); + o.setImageUrl(info.getSaleTemplateResponse().getIconUrl()); + // 只有第三方真的返回了磨损度才覆盖:cs340 这里常返回空串, + // 用 != null 判断会把上面从 name 切好的磨损度覆盖成空值。 + String thirdExterior = info.getSaleTemplateResponse().getExteriorName(); + if (thirdExterior != null && !thirdExterior.trim().isEmpty()) { + o.setExteriorName(thirdExterior.trim()); + } + o.setRarityName(info.getSaleTemplateResponse().getRarityName()); + o.setQualityName(info.getSaleTemplateResponse().getQualityName()); + o.setIsPutaway("0"); // 在售 -> 上架 + onSale = true; + listed++; + } + } + if (!onSale) { + o.setPrice(BigDecimal.ZERO); + o.setQuantity(0); + o.setIsPutaway("1"); // 未在售 -> 下架 + } + toSave.add(o); + } + + // 5. 清空原有饰品(market_hash_name 为主键且非空,带 where 条件绕过全表删除拦截),再批量入库 + long before = ornamentService.count(); + ornamentService.remove(new LambdaQueryWrapper().isNotNull(TtOrnament::getMarketHashName)); + System.out.println("[cs340-pull-all] 已清空原有饰品:" + before + " 条"); + + boolean saved = ornamentService.saveBatch(toSave, SAVE_BATCH_SIZE); + System.out.println("[cs340-pull-all] 批量入库结果=" + saved + + ",入库总数=" + toSave.size() + + ",其中在售(上架)=" + listed + + ",当前表内饰品数=" + ornamentService.count()); + } finally { + ctx.close(); + } + } +} diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml new file mode 100644 index 0000000..57eb5f1 --- /dev/null +++ b/ruoyi-common/pom.xml @@ -0,0 +1,204 @@ + + + + ruoyi + com.ruoyi + 4.8.2 + + 4.0.0 + + ruoyi-common + + + common通用工具 + + + + + io.micrometer + micrometer-core + + + + + org.springframework + spring-context-support + + + + + org.springframework + spring-web + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.baomidou + dynamic-datasource-spring-boot-starter + 3.5.2 + + + + + com.alibaba.fastjson2 + fastjson2 + + + + + commons-io + commons-io + + + + + org.apache.poi + poi-ooxml + + + + + org.yaml + snakeyaml + + + + + io.jsonwebtoken + jjwt + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + org.glassfish.jaxb + jaxb-runtime + 2.3.1 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + + + + + + nl.basjes.parse.useragent + yauaa + + + eu.bitwalker + UserAgentUtils + + + + + jakarta.servlet + jakarta.servlet-api + + + + + com.baomidou + mybatis-plus-spring-boot4-starter + 3.5.15 + + + com.baomidou + mybatis-plus-jsqlparser + 3.5.15 + + + + org.projectlombok + lombok + + + + + cn.hutool + hutool-all + 5.8.23 + + + + org.springframework.boot + spring-boot-starter-amqp + + + + org.redisson + redisson-spring-boot-starter + 3.16.2 + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + io.springfox + springfox-boot-starter + ${swagger.version} + + + + + \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java new file mode 100644 index 0000000..1d6d4f4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 匿名访问不鉴权注解 + * + * @author ruoyi + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Anonymous +{ +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java new file mode 100644 index 0000000..be49c80 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java @@ -0,0 +1,33 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 数据权限过滤注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope +{ + /** + * 部门表的别名 + */ + public String deptAlias() default ""; + + /** + * 用户表的别名 + */ + public String userAlias() default ""; + + /** + * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来 + */ + public String permission() default ""; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java new file mode 100644 index 0000000..79cd191 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java @@ -0,0 +1,28 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.ruoyi.common.enums.DataSourceType; + +/** + * 自定义多数据源切换注解 + * + * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 + * + * @author ruoyi + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface DataSource +{ + /** + * 切换数据源名称 + */ + public DataSourceType value() default DataSourceType.MASTER; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java new file mode 100644 index 0000000..58a4bc6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java @@ -0,0 +1,187 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigDecimal; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import com.ruoyi.common.utils.poi.ExcelHandlerAdapter; + +/** + * 自定义导出Excel数据注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Excel +{ + /** + * 导出时在excel中排序 + */ + public int sort() default Integer.MAX_VALUE; + + /** + * 导出到Excel中的名字. + */ + public String name() default ""; + + /** + * 日期格式, 如: yyyy-MM-dd + */ + public String dateFormat() default ""; + + /** + * 如果是字典类型,请设置字典的type值 (如: sys_user_sex) + */ + public String dictType() default ""; + + /** + * 读取内容转表达式 (如: 0=男,1=女,2=未知) + */ + public String readConverterExp() default ""; + + /** + * 分隔符,读取字符串组内容 + */ + public String separator() default ","; + + /** + * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化) + */ + public int scale() default -1; + + /** + * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN + */ + public int roundingMode() default BigDecimal.ROUND_HALF_EVEN; + + /** + * 导出时在excel中每个列的高度 + */ + public double height() default 14; + + /** + * 导出时在excel中每个列的宽度 + */ + public double width() default 16; + + /** + * 文字后缀,如% 90 变成90% + */ + public String suffix() default ""; + + /** + * 当值为空时,字段的默认值 + */ + public String defaultValue() default ""; + + /** + * 提示信息 + */ + public String prompt() default ""; + + /** + * 设置只能选择不能输入的列内容. + */ + public String[] combo() default {}; + + /** + * 是否需要纵向合并单元格,应对需求:含有list集合单元格) + */ + public boolean needMerge() default false; + + /** + * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写. + */ + public boolean isExport() default true; + + /** + * 另一个类中的属性名称,支持多级获取,以小数点隔开 + */ + public String targetAttr() default ""; + + /** + * 是否自动统计数据,在最后追加一行统计数据总和 + */ + public boolean isStatistics() default false; + + /** + * 导出类型(0数字 1字符串 2图片) + */ + public ColumnType cellType() default ColumnType.STRING; + + /** + * 导出列头背景颜色 + */ + public IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT; + + /** + * 导出列头字体颜色 + */ + public IndexedColors headerColor() default IndexedColors.WHITE; + + /** + * 导出单元格背景颜色 + */ + public IndexedColors backgroundColor() default IndexedColors.WHITE; + + /** + * 导出单元格字体颜色 + */ + public IndexedColors color() default IndexedColors.BLACK; + + /** + * 导出字段对齐方式 + */ + public HorizontalAlignment align() default HorizontalAlignment.CENTER; + + /** + * 自定义数据处理器 + */ + public Class handler() default ExcelHandlerAdapter.class; + + /** + * 自定义数据处理器参数 + */ + public String[] args() default {}; + + /** + * 字段类型(0:导出导入;1:仅导出;2:仅导入) + */ + Type type() default Type.ALL; + + public enum Type + { + ALL(0), EXPORT(1), IMPORT(2); + private final int value; + + Type(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } + + public enum ColumnType + { + NUMERIC(0), STRING(1), IMAGE(2); + private final int value; + + ColumnType(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java new file mode 100644 index 0000000..1f1cc81 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Excel注解集 + * + * @author ruoyi + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Excels +{ + public Excel[] value(); +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java new file mode 100644 index 0000000..c747cfd --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java @@ -0,0 +1,75 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.enums.OperatorType; + +/** + * 自定义操作日志记录注解 + * + * @author ruoyi + * + */ +@Target({ ElementType.PARAMETER, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log +{ + /** + * 模块 + */ + public String title() default ""; + + /** + * 操作位置:运营看到的页面名(菜单名),如"用户列表"。填了才记"人话"日志 + */ + public String menu() default ""; + + /** + * 操作动作:运营点的那颗按钮上的字,如"修改用户"。取值看前端页面按钮文案,别自己编 + */ + public String action() default ""; + + /** + * 功能 + */ + public BusinessType businessType() default BusinessType.OTHER; + + /** + * 操作人类别 + */ + public OperatorType operatorType() default OperatorType.MANAGE; + + /** + * 是否保存请求的参数 + */ + public boolean isSaveRequestData() default true; + + /** + * 是否保存响应的参数 + */ + public boolean isSaveResponseData() default true; + + /** + * 排除指定的请求参数 + */ + public String[] excludeParamNames() default {}; + + /** + * 要比对的实体类:记"字段:改前 → 改后"。不填则按原样记请求参数 + */ + public Class entityClass() default Void.class; + + /** Explicit concrete mapper interface; never resolve an inherited BaseMapper method by declaring class. */ + public Class mapperClass() default Void.class; + + /** + * 主键不在实体参数里时,按这个名字从入参里找:请求体 Map 取 key、DTO 取 getXxx()、或入参本身就是主键。 + * 例:@Log(entityClass = TtUser.class, mapperClass = TtUserMapper.class, idField = "userId") + */ + public String idField() default ""; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/NewUserInfo.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/NewUserInfo.java new file mode 100644 index 0000000..fa12300 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/NewUserInfo.java @@ -0,0 +1,12 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.*; + +/** + * 方法之前更新用户缓存 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NewUserInfo { +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/NoRepeatSubmit.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/NoRepeatSubmit.java new file mode 100644 index 0000000..d7a3042 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/NoRepeatSubmit.java @@ -0,0 +1,4 @@ +package com.ruoyi.common.annotation; + +public @interface NoRepeatSubmit { +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java new file mode 100644 index 0000000..0f024c7 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java @@ -0,0 +1,40 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.enums.LimitType; + +/** + * 限流注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimiter +{ + /** + * 限流key + */ + public String key() default CacheConstants.RATE_LIMIT_KEY; + + /** + * 限流时间,单位秒 + */ + public int time() default 60; + + /** + * 限流次数 + */ + public int count() default 100; + + /** + * 限流类型 + */ + public LimitType limitType() default LimitType.DEFAULT; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java new file mode 100644 index 0000000..b769748 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java @@ -0,0 +1,31 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义注解防止表单重复提交 + * + * @author ruoyi + * + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatSubmit +{ + /** + * 间隔时间(ms),小于此时间视为重复提交 + */ + public int interval() default 5000; + + /** + * 提示消息 + */ + public String message() default "不允许重复提交,请稍候再试"; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/UpdateUserCache.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/UpdateUserCache.java new file mode 100644 index 0000000..fe2231f --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/UpdateUserCache.java @@ -0,0 +1,12 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.*; + +/** + * 自定义更新用户缓存注解 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface UpdateUserCache { +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/UserPermission.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/UserPermission.java new file mode 100644 index 0000000..50404d2 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/UserPermission.java @@ -0,0 +1,35 @@ +package com.ruoyi.common.annotation; /** + * Copyright 2018-2020 stylefeng & fengshuonan (https://gitee.com/stylefeng) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.lang.annotation.*; + +/** + * 权限注解 用于检查权限 规定访问权限 + * + * @example @Permission({role1,role2}) + * @example @Permission + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface UserPermission { + + /** + *

角色英文名称

+ *

使用注解时加上这个值表示限制只有某个角色的才可以访问对应的资源

+ *

常用在某些资源限制只有超级管理员角色才可访问

+ */ + String[] value() default {}; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/ResTemplateConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/ResTemplateConfig.java new file mode 100644 index 0000000..3e54966 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/ResTemplateConfig.java @@ -0,0 +1,25 @@ +package com.ruoyi.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class ResTemplateConfig { + + @Bean + public RestTemplate restTemplate(ClientHttpRequestFactory factory) { + return new RestTemplate(factory); + } + + @Bean + public ClientHttpRequestFactory simpleClientHttpRequestFactory() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + // 超时设置 + factory.setReadTimeout(5000); // ms + factory.setConnectTimeout(15000); // ms + return factory; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java new file mode 100644 index 0000000..0ef2860 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java @@ -0,0 +1,112 @@ +package com.ruoyi.common.config; + +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 读取项目相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "ruoyi") +public class RuoYiConfig +{ + /** 上传路径 */ + @Getter + private static String profile; + /** 获取地址开关 */ + @Getter + private static boolean addressEnabled; + /** 验证码类型 */ + @Getter + private static String captchaType; + /** + * 项目全局域名配置 + */ + @Getter + private static String domainName; + /** 项目名称 */ + @Getter + private String name; + /** 版本 */ + @Getter + private String version; + /** 版权年份 */ + @Getter + private String copyrightYear; + /** 实例演示开关 */ + @Getter + private boolean demoEnabled; + + public void setName(String name) + { + this.name = name; + } + + public void setVersion(String version) + { + this.version = version; + } + + public void setCopyrightYear(String copyrightYear) + { + this.copyrightYear = copyrightYear; + } + + public void setDemoEnabled(boolean demoEnabled) + { + this.demoEnabled = demoEnabled; + } + + public void setProfile(String profile) + { + RuoYiConfig.profile = profile; + } + + public void setAddressEnabled(boolean addressEnabled) + { + RuoYiConfig.addressEnabled = addressEnabled; + } + + public void setCaptchaType(String captchaType) { + RuoYiConfig.captchaType = captchaType; + } + + public void setDomainName(String domainName) { + RuoYiConfig.domainName = domainName; + } + + /** + * 获取导入上传路径 + */ + public static String getImportPath() + { + return getProfile() + "/import"; + } + + /** + * 获取头像上传路径 + */ + public static String getAvatarPath() + { + return getProfile() + "/avatar"; + } + + /** + * 获取下载路径 + */ + public static String getDownloadPath() + { + return getProfile() + "/download"; + } + + /** + * 获取上传路径 + */ + public static String getUploadPath() + { + return getProfile() + "/upload"; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java new file mode 100644 index 0000000..3fd9210 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java @@ -0,0 +1,54 @@ +package com.ruoyi.common.constant; + +/** + * 缓存的key 常量 + * + * @author ruoyi + */ +public class CacheConstants +{ + /** + * 登录用户 redis key + */ + public static final String LOGIN_TOKEN_KEY = "login_tokens:"; + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + /** + * 字典管理 cache key + */ + public static final String SYS_DICT_KEY = "sys_dict:"; + + /** + * 防重提交 redis key + */ + public static final String REPEAT_SUBMIT_KEY = "repeat_submit:"; + + /** + * 限流 redis key + */ + public static final String RATE_LIMIT_KEY = "rate_limit:"; + + /** + * 登录账户密码错误次数 redis key + */ + public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:"; + + /** + * 在线用户心跳 redis key + */ + public static final String ONLINE_HEARTBEAT_KEY = "online_heartbeat:"; + + /** + * 在线用户心跳有效期(分钟):HTTP 请求或 WebSocket PING 后此时长内视为在线 + */ + public static final int ONLINE_HEARTBEAT_EXPIRE_MINUTES = 3; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java new file mode 100644 index 0000000..355851f --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java @@ -0,0 +1,167 @@ +package com.ruoyi.common.constant; + +import io.jsonwebtoken.Claims; + +/** + * 通用常量信息 + * + * @author ruoyi + */ +public class Constants +{ + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * www主域 + */ + public static final String WWW = "www."; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 通用成功标识 + */ + public static final String SUCCESS = "0"; + + /** + * 通用失败标识 + */ + public static final String FAIL = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 所有权限标识 + */ + public static final String ALL_PERMISSION = "*:*:*"; + + /** + * 管理员角色权限标识 + */ + public static final String SUPER_ADMIN = "admin"; + + /** + * 角色权限分隔符 + */ + public static final String ROLE_DELIMETER = ","; + + /** + * 权限标识分隔符 + */ + public static final String PERMISSION_DELIMETER = ","; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** + * 令牌 + */ + public static final String TOKEN = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * 令牌前缀 + */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** + * 用户ID + */ + public static final String JWT_USERID = "userid"; + + /** + * 用户名称 + */ + public static final String JWT_USERNAME = Claims.SUBJECT; + + /** + * 用户头像 + */ + public static final String JWT_AVATAR = "avatar"; + + /** + * 创建时间 + */ + public static final String JWT_CREATED = "created"; + + /** + * 用户权限 + */ + public static final String JWT_AUTHORITIES = "authorities"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全) + */ + public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.ruoyi" }; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = { "com.ruoyi" }; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config" }; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/ErrorCode.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ErrorCode.java new file mode 100644 index 0000000..a77260b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ErrorCode.java @@ -0,0 +1,54 @@ +package com.ruoyi.common.constant; + +/** + * 业务错误码常量类 + * + * 编码规则(千位分段,每段 1000 个码位): + * 1xxx — 用户 / 背包 (service-user) + * 2xxx — 对战 / 玩法 (service-playingmethod) + * 3xxx — 商城 / 交易 (service-shopping) + * 4xxx — 后台管理 (service-admin) + * 5xxx — 第三方服务 (service-thirdparty) + * 6xxx — 促销 / 推广 (skins-promo) + * 7xxx-8xxx — 预留 + * 9xxx — 框架 / 基础设施 (ruoyi-framework, ruoyi-common) + * + *

HTTP 标准码 (200, 401, 403, 404, 500) 沿用 {@link HttpStatus},不在此重复定义。

+ * + * @see HttpStatus + */ +public class ErrorCode { + + // ===================== 1xxx: 用户 / 背包 ===================== + /** 饰品已下架,不可用于提货或转赠 */ + public static final int ORNAMENT_NOT_PUTAWAY = 1001; + /** 未绑定 Steam 交易链接 */ + public static final int TRANSACTION_LINK_NOT_SET = 1002; + + // ===================== 2xxx: 对战 / 玩法 ===================== + /** 不可重复加入对局 */ + public static final int FIGHT_DUPLICATE_JOIN = 2001; + /** 对局尚未开始 */ + public static final int FIGHT_NOT_STARTED = 2002; + /** 对局已结束 */ + public static final int FIGHT_ALREADY_ENDED = 2003; + + // ===================== 3xxx: 商城 / 交易 ===================== + // 预留 + + // ===================== 4xxx: 后台管理 ===================== + // 预留 + + // ===================== 5xxx: 第三方服务 ===================== + // 预留 + + // ===================== 9xxx: 框架 / 基础设施 ===================== + /** 登录过期 */ + public static final int LOGIN_EXPIRED = 9001; + /** 账号被踢下线(别处登录) */ + public static final int SESSION_KICKED = 9002; + /** WebSocket 连接数超限 */ + public static final int WS_MAX_CONNECTIONS = 9003; + + private ErrorCode() {} +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java new file mode 100644 index 0000000..7d899d4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java @@ -0,0 +1,117 @@ +package com.ruoyi.common.constant; + +/** + * 代码生成通用常量 + * + * @author ruoyi + */ +public class GenConstants +{ + /** 单表(增删改查) */ + public static final String TPL_CRUD = "crud"; + + /** 树表(增删改查) */ + public static final String TPL_TREE = "tree"; + + /** 主子表(增删改查) */ + public static final String TPL_SUB = "sub"; + + /** 树编码字段 */ + public static final String TREE_CODE = "treeCode"; + + /** 树父编码字段 */ + public static final String TREE_PARENT_CODE = "treeParentCode"; + + /** 树名称字段 */ + public static final String TREE_NAME = "treeName"; + + /** 上级菜单ID字段 */ + public static final String PARENT_MENU_ID = "parentMenuId"; + + /** 上级菜单名称字段 */ + public static final String PARENT_MENU_NAME = "parentMenuName"; + + /** 数据库字符串类型 */ + public static final String[] COLUMNTYPE_STR = { "char", "varchar", "nvarchar", "varchar2" }; + + /** 数据库文本类型 */ + public static final String[] COLUMNTYPE_TEXT = { "tinytext", "text", "mediumtext", "longtext" }; + + /** 数据库时间类型 */ + public static final String[] COLUMNTYPE_TIME = { "datetime", "time", "date", "timestamp" }; + + /** 数据库数字类型 */ + public static final String[] COLUMNTYPE_NUMBER = { "tinyint", "smallint", "mediumint", "int", "number", "integer", + "bit", "bigint", "float", "double", "decimal" }; + + /** 页面不需要编辑字段 */ + public static final String[] COLUMNNAME_NOT_EDIT = { "id", "create_by", "create_time", "del_flag" }; + + /** 页面不需要显示的列表字段 */ + public static final String[] COLUMNNAME_NOT_LIST = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time" }; + + /** 页面不需要查询字段 */ + public static final String[] COLUMNNAME_NOT_QUERY = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time", "remark" }; + + /** Entity基类字段 */ + public static final String[] BASE_ENTITY = { "createBy", "createTime", "updateBy", "updateTime", "remark" }; + + /** Tree基类字段 */ + public static final String[] TREE_ENTITY = { "parentName", "parentId", "orderNum", "ancestors", "children" }; + + /** 文本框 */ + public static final String HTML_INPUT = "input"; + + /** 文本域 */ + public static final String HTML_TEXTAREA = "textarea"; + + /** 下拉框 */ + public static final String HTML_SELECT = "select"; + + /** 单选框 */ + public static final String HTML_RADIO = "radio"; + + /** 复选框 */ + public static final String HTML_CHECKBOX = "checkbox"; + + /** 日期控件 */ + public static final String HTML_DATETIME = "datetime"; + + /** 图片上传控件 */ + public static final String HTML_IMAGE_UPLOAD = "imageUpload"; + + /** 文件上传控件 */ + public static final String HTML_FILE_UPLOAD = "fileUpload"; + + /** 富文本控件 */ + public static final String HTML_EDITOR = "editor"; + + /** 字符串类型 */ + public static final String TYPE_STRING = "String"; + + /** 整型 */ + public static final String TYPE_INTEGER = "Integer"; + + /** 长整型 */ + public static final String TYPE_LONG = "Long"; + + /** 浮点型 */ + public static final String TYPE_DOUBLE = "Double"; + + /** 高精度计算类型 */ + public static final String TYPE_BIGDECIMAL = "BigDecimal"; + + /** 时间类型 */ + public static final String TYPE_DATE = "Date"; + + /** 模糊查询 */ + public static final String QUERY_LIKE = "LIKE"; + + /** 相等查询 */ + public static final String QUERY_EQ = "EQ"; + + /** 需要 */ + public static final String REQUIRE = "1"; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java new file mode 100644 index 0000000..3a69b89 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java @@ -0,0 +1,99 @@ +package com.ruoyi.common.constant; + +/** + * 返回状态码 + * + * @author ruoyi + */ +public class HttpStatus +{ + /** + * 操作成功 + */ + public static final int SUCCESS = 200; + + /** + * 对象创建成功 + */ + public static final int CREATED = 201; + + /** + * 请求已经被接受 + */ + public static final int ACCEPTED = 202; + + /** + * 操作已经执行成功,但是没有返回数据 + */ + public static final int NO_CONTENT = 204; + + /** + * 资源已被移除 + */ + public static final int MOVED_PERM = 301; + + /** + * 重定向 + */ + public static final int SEE_OTHER = 303; + + /** + * 资源没有被修改 + */ + public static final int NOT_MODIFIED = 304; + + /** + * 参数列表错误(缺少,格式不匹配) + */ + public static final int BAD_REQUEST = 400; + + /** + * 未授权 + */ + public static final int UNAUTHORIZED = 401; + + /** + * 未实名认证(前端收到该错误码统一弹出实名认证窗口) + */ + public static final int REAL_NAME_REQUIRED = 701; + + /** + * 访问受限,授权过期 + */ + public static final int FORBIDDEN = 403; + + /** + * 资源,服务未找到 + */ + public static final int NOT_FOUND = 404; + + /** + * 不允许的http方法 + */ + public static final int BAD_METHOD = 405; + + /** + * 资源冲突,或者资源被锁 + */ + public static final int CONFLICT = 409; + + /** + * 不支持的数据,媒体类型 + */ + public static final int UNSUPPORTED_TYPE = 415; + + /** + * 系统内部错误 + */ + public static final int ERROR = 500; + + /** + * 接口未实现 + */ + public static final int NOT_IMPLEMENTED = 501; + + /** + * 系统警告消息 + */ + public static final int WARN = 601; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java new file mode 100644 index 0000000..62ad815 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java @@ -0,0 +1,50 @@ +package com.ruoyi.common.constant; + +/** + * 任务调度通用常量 + * + * @author ruoyi + */ +public class ScheduleConstants +{ + public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME"; + + /** 执行目标key */ + public static final String TASK_PROPERTIES = "TASK_PROPERTIES"; + + /** 默认 */ + public static final String MISFIRE_DEFAULT = "0"; + + /** 立即触发执行 */ + public static final String MISFIRE_IGNORE_MISFIRES = "1"; + + /** 触发一次执行 */ + public static final String MISFIRE_FIRE_AND_PROCEED = "2"; + + /** 不触发立即执行 */ + public static final String MISFIRE_DO_NOTHING = "3"; + + public enum Status + { + /** + * 正常 + */ + NORMAL("0"), + /** + * 暂停 + */ + PAUSE("1"); + + private String value; + + private Status(String value) + { + this.value = value; + } + + public String getValue() + { + return value; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/ThirdParty/Platform.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ThirdParty/Platform.java new file mode 100644 index 0000000..b8fb406 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ThirdParty/Platform.java @@ -0,0 +1,17 @@ +package com.ruoyi.common.constant.ThirdParty; + +public enum Platform { + + ZBT(1), + YY(2); + + private Integer code; + + Platform(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java new file mode 100644 index 0000000..c44941b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java @@ -0,0 +1,83 @@ +package com.ruoyi.common.constant; + +/** + * 用户常量信息 + * + * @author ruoyi + */ +public class UserConstants +{ + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + public static final String NORMAL = "0"; + + /** 异常状态 */ + public static final String EXCEPTION = "1"; + + /** 用户封禁状态 */ + public static final String USER_DISABLE = "1"; + + /** 角色封禁状态 */ + public static final String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + public static final String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + public static final String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + public static final String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + public static final String YES = "Y"; + + /** 是否菜单外链(是) */ + public static final String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + public static final String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + public static final String TYPE_DIR = "M"; + + /** 菜单类型(菜单) */ + public static final String TYPE_MENU = "C"; + + /** 菜单类型(按钮) */ + public static final String TYPE_BUTTON = "F"; + + /** Layout组件标识 */ + public final static String LAYOUT = "Layout"; + + /** ParentView组件标识 */ + public final static String PARENT_VIEW = "ParentView"; + + /** InnerLink组件标识 */ + public final static String INNER_LINK = "InnerLink"; + + /** 校验是否唯一的返回标识 */ + public final static boolean UNIQUE = true; + public final static boolean NOT_UNIQUE = false; + + /** + * 用户名长度限制 + */ + public static final int USERNAME_MIN_LENGTH = 2; + public static final int USERNAME_MAX_LENGTH = 20; + /** + * 用户名长度限制 + */ + public static final int NICKNAME_MIN_LENGTH = 1; + public static final int NICKNAME_MAX_LENGTH = 7; + + /** + * 密码长度限制 + */ + public static final int PASSWORD_MIN_LENGTH = 5; + public static final int PASSWORD_MAX_LENGTH = 20; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java new file mode 100644 index 0000000..b5346ee --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java @@ -0,0 +1,223 @@ +package com.ruoyi.common.core.controller; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.core.page.PageDomain; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.page.TableSupport; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.PageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.sql.SqlUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; + +import java.beans.PropertyEditorSupport; +import java.util.Date; +import java.util.List; + +/** + * web层通用数据处理 + * + * @author ruoyi + */ +public class BaseController +{ + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * 将前台传递过来的日期格式的字符串,自动转化为Date类型 + */ + @InitBinder + public void initBinder(WebDataBinder binder) + { + // Date 类型转换 + binder.registerCustomEditor(Date.class, new PropertyEditorSupport() + { + @Override + public void setAsText(String text) + { + setValue(DateUtils.parseDate(text)); + } + }); + } + + /** + * 设置请求分页数据 + */ + protected void startPage() + { + PageUtils.startPage(); + } + + /** + * 设置请求排序数据 + */ + protected void startOrderBy() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) + { + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + PageHelper.orderBy(orderBy); + } + } + + /** + * 清理分页的线程变量 + */ + protected void clearPage() + { + PageUtils.clearPage(); + } + + /** + * 响应请求分页数据 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected TableDataInfo getDataTable(List list) + { + TableDataInfo rspData = new TableDataInfo(); + rspData.setCode(HttpStatus.SUCCESS); + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal(new PageInfo(list).getTotal()); + return rspData; + } + + /** + * 响应请求分页数据 + */ + protected PageDataInfo getPageData(List list) { + PageDataInfo pageDataInfo = new PageDataInfo(); + pageDataInfo.setCode(HttpStatus.SUCCESS); + pageDataInfo.setMsg("查询成功"); + pageDataInfo.setRows(list); + pageDataInfo.setTotal(new PageInfo(list).getTotal()); + return pageDataInfo; + } + + /** + * 返回成功 + */ + public AjaxResult success() + { + return AjaxResult.success(); + } + + /** + * 返回失败消息 + */ + public AjaxResult error() + { + return AjaxResult.error(); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(String message) + { + return AjaxResult.success(message); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(Object data) + { + return AjaxResult.success(data); + } + + /** + * 返回失败消息 + */ + public AjaxResult error(String message) + { + return AjaxResult.error(message); + } + + /** + * 返回警告消息 + */ + public AjaxResult warn(String message) + { + return AjaxResult.warn(message); + } + + /** + * 响应返回结果 + * + * @param rows 影响行数 + * @return 操作结果 + */ + protected AjaxResult toAjax(int rows) + { + return rows > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 响应返回结果 + * + * @param result 结果 + * @return 操作结果 + */ + protected AjaxResult toAjax(boolean result) + { + return result ? success() : error(); + } + + /** + * 页面跳转 + */ + public String redirect(String url) + { + return StringUtils.format("redirect:{}", url); + } + + /** + * 获取用户缓存信息 + */ + public LoginUser getLoginUser() + { + return SecurityUtils.getLoginUser(); + } + + /** + * 获取登录用户id + */ + public Long getUserId() + { + return getLoginUser().getUserId(); + } + + /** + * 获取登录部门id + */ + public Long getDeptId() + { + return getLoginUser().getDeptId(); + } + + /** + * 获取登录用户名 + */ + public String getUsername() + { + return getLoginUser().getUsername(); + } + + /** + * 获取登录用户Token + */ + public String getToken() { + return getLoginUser().getToken(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java new file mode 100644 index 0000000..285dd96 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java @@ -0,0 +1,237 @@ +package com.ruoyi.common.core.domain; + +import java.util.HashMap; +import java.util.Objects; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.utils.StringUtils; + +/** + * 操作消息提醒 + * + * @author ruoyi + */ +public class AjaxResult extends HashMap +{ + private static final long serialVersionUID = 1L; + + /** 状态码 */ + public static final String CODE_TAG = "code"; + + /** 返回内容 */ + public static final String MSG_TAG = "msg"; + + /** 数据对象 */ + public static final String DATA_TAG = "data"; + + /** + * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 + */ + public AjaxResult() + { + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (StringUtils.isNotNull(data)) + { + super.put(DATA_TAG, data); + } + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() + { + return AjaxResult.success("操作成功"); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data) + { + return AjaxResult.success("操作成功", data); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @return 成功消息 + */ + public static AjaxResult success(String msg) + { + return AjaxResult.success(msg, null); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, Object data) + { + return new AjaxResult(HttpStatus.SUCCESS, msg, data); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult warn(String msg) + { + return AjaxResult.warn(msg, null); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 警告消息 + */ + public static AjaxResult warn(String msg, Object data) + { + return new AjaxResult(HttpStatus.WARN, msg, data); + } + + /** + * 返回错误消息 + * + * @return 错误消息 + */ + public static AjaxResult error() + { + return AjaxResult.error("操作失败"); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(String msg) + { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 错误消息 + */ + public static AjaxResult error(String msg, Object data) + { + return new AjaxResult(HttpStatus.ERROR, msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(int code, String msg) + { + return new AjaxResult(code, msg, null); + } + + /** + * 返回未实名认证错误。前端收到 code=4001 时统一弹出实名认证窗口。 + * + * @return 错误消息 + */ + public static AjaxResult realNameRequired() + { + return new AjaxResult(HttpStatus.REAL_NAME_REQUIRED, "您的账户未进行实名认证,请先进行实名认证!", null); + } + + /** + * 返回未实名认证错误(自定义提示语)。前端收到 code=4001 时统一弹出实名认证窗口。 + * + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult realNameRequired(String msg) + { + return new AjaxResult(HttpStatus.REAL_NAME_REQUIRED, msg, null); + } + + /** + * 是否为成功消息 + * + * @return 结果 + */ + public boolean isSuccess() + { + return Objects.equals(HttpStatus.SUCCESS, this.get(CODE_TAG)); + } + + /** + * 是否为警告消息 + * + * @return 结果 + */ + public boolean isWarn() + { + return Objects.equals(HttpStatus.WARN, this.get(CODE_TAG)); + } + + /** + * 是否为错误消息 + * + * @return 结果 + */ + public boolean isError() + { + return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG)); + } + + /** + * 方便链式调用 + * + * @param key 键 + * @param value 值 + * @return 数据对象 + */ + @Override + public AjaxResult put(String key, Object value) + { + super.put(key, value); + return this; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java new file mode 100644 index 0000000..15bf66b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java @@ -0,0 +1,118 @@ +package com.ruoyi.common.core.domain; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Entity基类 + * + * @author ruoyi + */ +public class BaseEntity implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 搜索值 */ + @JsonIgnore + private String searchValue; + + /** 创建者 */ + private String createBy; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + private String updateBy; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** 请求参数 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map params; + + public String getSearchValue() + { + return searchValue; + } + + public void setSearchValue(String searchValue) + { + this.searchValue = searchValue; + } + + public String getCreateBy() + { + return createBy; + } + + public void setCreateBy(String createBy) + { + this.createBy = createBy; + } + + public Date getCreateTime() + { + return createTime; + } + + public void setCreateTime(Date createTime) + { + this.createTime = createTime; + } + + public String getUpdateBy() + { + return updateBy; + } + + public void setUpdateBy(String updateBy) + { + this.updateBy = updateBy; + } + + public Date getUpdateTime() + { + return updateTime; + } + + public void setUpdateTime(Date updateTime) + { + this.updateTime = updateTime; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } + + public Map getParams() + { + if (params == null) + { + params = new HashMap<>(); + } + return params; + } + + public void setParams(Map params) + { + this.params = params; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/EntityChange.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/EntityChange.java new file mode 100644 index 0000000..78db338 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/EntityChange.java @@ -0,0 +1,67 @@ +package com.ruoyi.common.core.domain; + +/** + * 实体字段改动记录(操作日志用) + * + * @author ruoyi + */ +public class EntityChange +{ + /** 字段名 */ + private final String field; + + /** 中文名:优先取字段上的 @Excel(name),没有就用字段名 */ + private final String label; + + /** 改前(已格式化,可能脱敏/截断) */ + private final String before; + + /** 改后(已格式化,可能脱敏/截断) */ + private final String after; + + public EntityChange(String field, String label, String before, String after) + { + this.field = field; + this.label = label; + this.before = before; + this.after = after; + } + + public String getField() + { + return field; + } + + public String getLabel() + { + return label; + } + + public String getBefore() + { + return before; + } + + public String getAfter() + { + return after; + } + + /** 给运营看的一行:账户金额:100 → 200 */ + public String toLine() + { + return label + ":" + before + " → " + after; + } + + /** 给程序解析的一行,前端用 tab 分隔好还原成表格 */ + public String toTabLine() + { + return label + "\t" + before + "\t" + after; + } + + @Override + public String toString() + { + return toLine(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java new file mode 100644 index 0000000..10c1c89 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java @@ -0,0 +1,134 @@ +package com.ruoyi.common.core.domain; + +import java.io.Serializable; +import com.ruoyi.common.constant.HttpStatus; + +/** + * 响应信息主体 + * + * @author ruoyi + */ +public class R implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 成功 */ + public static final int SUCCESS = HttpStatus.SUCCESS; + + /** 失败 */ + public static final int FAIL = HttpStatus.ERROR; + + /** 未实名认证(前端收到该错误码统一弹出实名认证窗口) */ + public static final int REAL_NAME_REQUIRED = HttpStatus.REAL_NAME_REQUIRED; + + private Integer code; + + private String msg; + + private T data; + + public static R ok() + { + return restResult(null, SUCCESS, "操作成功"); + } + + public static R ok(T data) + { + return restResult(data, SUCCESS, "操作成功"); + } + + public static R ok(T data, String msg) + { + return restResult(data, SUCCESS, msg); + } + + public static R fail() + { + return restResult(null, FAIL, "操作失败"); + } + + public static R fail(String msg) + { + return restResult(null, FAIL, msg); + } + + public static R fail(T data) + { + return restResult(data, FAIL, "操作失败"); + } + + public static R fail(T data, String msg) + { + return restResult(data, FAIL, msg); + } + + public static R fail(int code, String msg) + { + return restResult(null, code, msg); + } + + /** + * 未实名认证。前端收到 code=701 时统一弹出实名认证窗口。 + */ + public static R realNameRequired() + { + return restResult(null, REAL_NAME_REQUIRED, "您的账户未进行实名认证,请先进行实名认证!"); + } + + /** + * 未实名认证(自定义提示语)。前端收到 code=701 时统一弹出实名认证窗口。 + */ + public static R realNameRequired(String msg) + { + return restResult(null, REAL_NAME_REQUIRED, msg); + } + + private static R restResult(T data, int code, String msg) + { + R apiResult = new R<>(); + apiResult.setCode(code); + apiResult.setData(data); + apiResult.setMsg(msg); + return apiResult; + } + + public Integer getCode() + { + return code; + } + + public void setCode(Integer code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public T getData() + { + return data; + } + + public void setData(T data) + { + this.data = data; + } + + public static Boolean isError(R ret) + { + return !isSuccess(ret); + } + + public static Boolean isSuccess(R ret) + { + return R.SUCCESS == ret.getCode(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java new file mode 100644 index 0000000..a180a18 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java @@ -0,0 +1,79 @@ +package com.ruoyi.common.core.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tree基类 + * + * @author ruoyi + */ +public class TreeEntity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 祖级列表 */ + private String ancestors; + + /** 子部门 */ + private List children = new ArrayList<>(); + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java new file mode 100644 index 0000000..bd835db --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java @@ -0,0 +1,77 @@ +package com.ruoyi.common.core.domain; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysMenu; + +/** + * Treeselect树结构实体类 + * + * @author ruoyi + */ +public class TreeSelect implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点ID */ + private Long id; + + /** 节点名称 */ + private String label; + + /** 子节点 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect() + { + + } + + public TreeSelect(SysDept dept) + { + this.id = dept.getDeptId(); + this.label = dept.getDeptName(); + this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public TreeSelect(SysMenu menu) + { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java new file mode 100644 index 0000000..37236db --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java @@ -0,0 +1,203 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.ArrayList; +import java.util.List; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 部门表 sys_dept + * + * @author ruoyi + */ +public class SysDept extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 部门ID */ + private Long deptId; + + /** 父部门ID */ + private Long parentId; + + /** 祖级列表 */ + private String ancestors; + + /** 部门名称 */ + private String deptName; + + /** 显示顺序 */ + private Integer orderNum; + + /** 负责人 */ + private String leader; + + /** 联系电话 */ + private String phone; + + /** 邮箱 */ + private String email; + + /** 部门状态:0正常,1停用 */ + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 父部门名称 */ + private String parentName; + + /** 子部门 */ + private List children = new ArrayList(); + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + @NotBlank(message = "部门名称不能为空") + @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符") + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getLeader() + { + return leader; + } + + public void setLeader(String leader) + { + this.leader = leader; + } + + @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符") + public String getPhone() + { + return phone; + } + + public void setPhone(String phone) + { + this.phone = phone; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("deptId", getDeptId()) + .append("parentId", getParentId()) + .append("ancestors", getAncestors()) + .append("deptName", getDeptName()) + .append("orderNum", getOrderNum()) + .append("leader", getLeader()) + .append("phone", getPhone()) + .append("email", getEmail()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java new file mode 100644 index 0000000..6b035a6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java @@ -0,0 +1,176 @@ +package com.ruoyi.common.core.domain.entity; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 字典数据表 sys_dict_data + * + * @author ruoyi + */ +public class SysDictData extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典编码 */ + @Excel(name = "字典编码", cellType = ColumnType.NUMERIC) + private Long dictCode; + + /** 字典排序 */ + @Excel(name = "字典排序", cellType = ColumnType.NUMERIC) + private Long dictSort; + + /** 字典标签 */ + @Excel(name = "字典标签") + private String dictLabel; + + /** 字典键值 */ + @Excel(name = "字典键值") + private String dictValue; + + /** 字典类型 */ + @Excel(name = "字典类型") + private String dictType; + + /** 样式属性(其他样式扩展) */ + private String cssClass; + + /** 表格字典样式 */ + private String listClass; + + /** 是否默认(Y是 N否) */ + @Excel(name = "是否默认", readConverterExp = "Y=是,N=否") + private String isDefault; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictCode() + { + return dictCode; + } + + public void setDictCode(Long dictCode) + { + this.dictCode = dictCode; + } + + public Long getDictSort() + { + return dictSort; + } + + public void setDictSort(Long dictSort) + { + this.dictSort = dictSort; + } + + @NotBlank(message = "字典标签不能为空") + @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符") + public String getDictLabel() + { + return dictLabel; + } + + public void setDictLabel(String dictLabel) + { + this.dictLabel = dictLabel; + } + + @NotBlank(message = "字典键值不能为空") + @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符") + public String getDictValue() + { + return dictValue; + } + + public void setDictValue(String dictValue) + { + this.dictValue = dictValue; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符") + public String getCssClass() + { + return cssClass; + } + + public void setCssClass(String cssClass) + { + this.cssClass = cssClass; + } + + public String getListClass() + { + return listClass; + } + + public void setListClass(String listClass) + { + this.listClass = listClass; + } + + public boolean getDefault() + { + return UserConstants.YES.equals(this.isDefault); + } + + public String getIsDefault() + { + return isDefault; + } + + public void setIsDefault(String isDefault) + { + this.isDefault = isDefault; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictCode", getDictCode()) + .append("dictSort", getDictSort()) + .append("dictLabel", getDictLabel()) + .append("dictValue", getDictValue()) + .append("dictType", getDictType()) + .append("cssClass", getCssClass()) + .append("listClass", getListClass()) + .append("isDefault", getIsDefault()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java new file mode 100644 index 0000000..ac5d290 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java @@ -0,0 +1,96 @@ +package com.ruoyi.common.core.domain.entity; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 字典类型表 sys_dict_type + * + * @author ruoyi + */ +public class SysDictType extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典主键 */ + @Excel(name = "字典主键", cellType = ColumnType.NUMERIC) + private Long dictId; + + /** 字典名称 */ + @Excel(name = "字典名称") + private String dictName; + + /** 字典类型 */ + @Excel(name = "字典类型") + private String dictType; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictId() + { + return dictId; + } + + public void setDictId(Long dictId) + { + this.dictId = dictId; + } + + @NotBlank(message = "字典名称不能为空") + @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") + public String getDictName() + { + return dictName; + } + + public void setDictName(String dictName) + { + this.dictName = dictName; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符") + @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictId", getDictId()) + .append("dictName", getDictName()) + .append("dictType", getDictType()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java new file mode 100644 index 0000000..581f5eb --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java @@ -0,0 +1,259 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.ArrayList; +import java.util.List; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 菜单权限表 sys_menu + * + * @author ruoyi + */ +public class SysMenu extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 菜单ID */ + private Long menuId; + + /** 菜单名称 */ + private String menuName; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 路由地址 */ + private String path; + + /** 组件路径 */ + private String component; + + /** 路由参数 */ + private String query; + + /** 是否为外链(0是 1否) */ + private String isFrame; + + /** 是否缓存(0缓存 1不缓存) */ + private String isCache; + + /** 类型(M目录 C菜单 F按钮) */ + private String menuType; + + /** 显示状态(0显示 1隐藏) */ + private String visible; + + /** 菜单状态(0正常 1停用) */ + private String status; + + /** 权限字符串 */ + private String perms; + + /** 菜单图标 */ + private String icon; + + /** 子菜单 */ + private List children = new ArrayList(); + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @NotBlank(message = "菜单名称不能为空") + @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符") + public String getMenuName() + { + return menuName; + } + + public void setMenuName(String menuName) + { + this.menuName = menuName; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + @Size(min = 0, max = 200, message = "路由地址不能超过200个字符") + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + @Size(min = 0, max = 200, message = "组件路径不能超过255个字符") + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public String getIsFrame() + { + return isFrame; + } + + public void setIsFrame(String isFrame) + { + this.isFrame = isFrame; + } + + public String getIsCache() + { + return isCache; + } + + public void setIsCache(String isCache) + { + this.isCache = isCache; + } + + @NotBlank(message = "菜单类型不能为空") + public String getMenuType() + { + return menuType; + } + + public void setMenuType(String menuType) + { + this.menuType = menuType; + } + + public String getVisible() + { + return visible; + } + + public void setVisible(String visible) + { + this.visible = visible; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符") + public String getPerms() + { + return perms; + } + + public void setPerms(String perms) + { + this.perms = perms; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("menuId", getMenuId()) + .append("menuName", getMenuName()) + .append("parentId", getParentId()) + .append("orderNum", getOrderNum()) + .append("path", getPath()) + .append("component", getComponent()) + .append("isFrame", getIsFrame()) + .append("IsCache", getIsCache()) + .append("menuType", getMenuType()) + .append("visible", getVisible()) + .append("status ", getStatus()) + .append("perms", getPerms()) + .append("icon", getIcon()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java new file mode 100644 index 0000000..2f8ed14 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java @@ -0,0 +1,241 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.Set; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 角色表 sys_role + * + * @author ruoyi + */ +public class SysRole extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 角色ID */ + @Excel(name = "角色序号", cellType = ColumnType.NUMERIC) + private Long roleId; + + /** 角色名称 */ + @Excel(name = "角色名称") + private String roleName; + + /** 角色权限 */ + @Excel(name = "角色权限") + private String roleKey; + + /** 角色排序 */ + @Excel(name = "角色排序") + private Integer roleSort; + + /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */ + @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限") + private String dataScope; + + /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */ + private boolean menuCheckStrictly; + + /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */ + private boolean deptCheckStrictly; + + /** 角色状态(0正常 1停用) */ + @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 用户是否存在此角色标识 默认不存在 */ + private boolean flag = false; + + /** 菜单组 */ + private Long[] menuIds; + + /** 部门组(数据权限) */ + private Long[] deptIds; + + /** 角色菜单权限 */ + private Set permissions; + + public SysRole() + { + + } + + public SysRole(Long roleId) + { + this.roleId = roleId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public boolean isAdmin() + { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Long roleId) + { + return roleId != null && 1L == roleId; + } + + @NotBlank(message = "角色名称不能为空") + @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符") + public String getRoleName() + { + return roleName; + } + + public void setRoleName(String roleName) + { + this.roleName = roleName; + } + + @NotBlank(message = "权限字符不能为空") + @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符") + public String getRoleKey() + { + return roleKey; + } + + public void setRoleKey(String roleKey) + { + this.roleKey = roleKey; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getRoleSort() + { + return roleSort; + } + + public void setRoleSort(Integer roleSort) + { + this.roleSort = roleSort; + } + + public String getDataScope() + { + return dataScope; + } + + public void setDataScope(String dataScope) + { + this.dataScope = dataScope; + } + + public boolean isMenuCheckStrictly() + { + return menuCheckStrictly; + } + + public void setMenuCheckStrictly(boolean menuCheckStrictly) + { + this.menuCheckStrictly = menuCheckStrictly; + } + + public boolean isDeptCheckStrictly() + { + return deptCheckStrictly; + } + + public void setDeptCheckStrictly(boolean deptCheckStrictly) + { + this.deptCheckStrictly = deptCheckStrictly; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + public Long[] getMenuIds() + { + return menuIds; + } + + public void setMenuIds(Long[] menuIds) + { + this.menuIds = menuIds; + } + + public Long[] getDeptIds() + { + return deptIds; + } + + public void setDeptIds(Long[] deptIds) + { + this.deptIds = deptIds; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("roleName", getRoleName()) + .append("roleKey", getRoleKey()) + .append("roleSort", getRoleSort()) + .append("dataScope", getDataScope()) + .append("menuCheckStrictly", isMenuCheckStrictly()) + .append("deptCheckStrictly", isDeptCheckStrictly()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java new file mode 100644 index 0000000..45d2d71 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java @@ -0,0 +1,324 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.Date; +import java.util.List; +import jakarta.validation.constraints.*; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.annotation.Excel.Type; +import com.ruoyi.common.annotation.Excels; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.xss.Xss; + +/** + * 用户对象 sys_user + * + * @author ruoyi + */ +public class SysUser extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 用户ID */ + @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号") + private Long userId; + + /** 部门ID */ + @Excel(name = "部门编号", type = Type.IMPORT) + private Long deptId; + + /** 用户账号 */ + @Excel(name = "登录名称") + private String userName; + + /** 用户昵称 */ + @Excel(name = "用户名称") + private String nickName; + + /** 用户邮箱 */ + @Excel(name = "用户邮箱") + private String email; + + /** 手机号码 */ + @Excel(name = "手机号码") + private String phonenumber; + + /** 用户性别 */ + @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知") + private String sex; + + /** 用户头像 */ + private String avatar; + + /** 密码 */ + private String password; + + /** 帐号状态(0正常 1停用) */ + @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 最后登录IP */ + @Excel(name = "最后登录IP", type = Type.EXPORT) + private String loginIp; + + /** 最后登录时间 */ + @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT) + private Date loginDate; + + /** 部门对象 */ + @Excels({ + @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT), + @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT) + }) + private SysDept dept; + + /** 角色对象 */ + private List roles; + + /** 角色组 */ + private Long[] roleIds; + + /** 岗位组 */ + private Long[] postIds; + + /** 角色ID */ + private Long roleId; + + public SysUser() + { + + } + + public SysUser(Long userId) + { + this.userId = userId; + } + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public boolean isAdmin() + { + return isAdmin(this.userId); + } + + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Xss(message = "用户昵称不能包含脚本字符") + @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") + public String getNickName() + { + return nickName; + } + + public void setNickName(String nickName) + { + this.nickName = nickName; + } + + @Xss(message = "用户账号不能包含脚本字符") + @NotBlank(message = "用户账号不能为空") + @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") + public String getPhonenumber() + { + return phonenumber; + } + + public void setPhonenumber(String phonenumber) + { + this.phonenumber = phonenumber; + } + + public String getSex() + { + return sex; + } + + public void setSex(String sex) + { + this.sex = sex; + } + + public String getAvatar() + { + return avatar; + } + + public void setAvatar(String avatar) + { + this.avatar = avatar; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getLoginIp() + { + return loginIp; + } + + public void setLoginIp(String loginIp) + { + this.loginIp = loginIp; + } + + public Date getLoginDate() + { + return loginDate; + } + + public void setLoginDate(Date loginDate) + { + this.loginDate = loginDate; + } + + public SysDept getDept() + { + return dept; + } + + public void setDept(SysDept dept) + { + this.dept = dept; + } + + public List getRoles() + { + return roles; + } + + public void setRoles(List roles) + { + this.roles = roles; + } + + public Long[] getRoleIds() + { + return roleIds; + } + + public void setRoleIds(Long[] roleIds) + { + this.roleIds = roleIds; + } + + public Long[] getPostIds() + { + return postIds; + } + + public void setPostIds(Long[] postIds) + { + this.postIds = postIds; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("deptId", getDeptId()) + .append("userName", getUserName()) + .append("nickName", getNickName()) + .append("email", getEmail()) + .append("phonenumber", getPhonenumber()) + .append("sex", getSex()) + .append("avatar", getAvatar()) + .append("password", getPassword()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("loginIp", getLoginIp()) + .append("loginDate", getLoginDate()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .append("dept", getDept()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/UserData.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/UserData.java new file mode 100644 index 0000000..064dfaa --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/UserData.java @@ -0,0 +1,37 @@ +package com.ruoyi.common.core.domain.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class UserData { + + private Integer userId; + private String userName; + private String nickName; + private String userType; + private Integer vipLevel; + private Integer promotionLevel; + private String password; + private String email; + private String avatar; + private BigDecimal accountAmount; + private BigDecimal accountCredits; + private String invitationCode; + private String parentInvitationCode; + private Long steamId; + private String transactionLink; + private String isRealCheck; + private String realName; + private Integer subordinateNumber; + private String deliveryAddress; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java new file mode 100644 index 0000000..b5bc8c8 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java @@ -0,0 +1,69 @@ +package com.ruoyi.common.core.domain.model; + +/** + * 用户登录对象 + * + * @author ruoyi + */ +public class LoginBody +{ + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; + + /** + * 验证码 + */ + private String code; + + /** + * 唯一标识 + */ + private String uuid; + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getCode() + { + return code; + } + + public void setCode(String code) + { + this.code = code; + } + + public String getUuid() + { + return uuid; + } + + public void setUuid(String uuid) + { + this.uuid = uuid; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java new file mode 100644 index 0000000..0350c9d --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java @@ -0,0 +1,181 @@ +package com.ruoyi.common.core.domain.model; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.entity.UserData; +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.ObjectUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +/** + * 登录用户身份权限 + * + * @author ruoyi + */ +@Data +public class LoginUser implements UserDetails +{ + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地点 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 用户信息 + */ + private SysUser user; + + private UserData userData; + + public LoginUser(Long userId, UserData userData) { + this.userId = userId; + this.userData = userData; + } + + public LoginUser() + { + } + + public LoginUser(SysUser user, Set permissions) + { + this.user = user; + this.permissions = permissions; + } + + public LoginUser(Long userId, Long deptId, SysUser user, Set permissions) + { + this.userId = userId; + this.deptId = deptId; + this.user = user; + this.permissions = permissions; + } + + @JSONField(serialize = false) + @Override + public String getPassword() + { + if (!ObjectUtils.isEmpty(user)) { + return user.getPassword(); + } + if (!ObjectUtils.isEmpty(userData)) { + return userData.getPassword(); + } + return null; + } + + @Override + public String getUsername() + { + if (!ObjectUtils.isEmpty(user)) { + return user.getUserName(); + } + if (!ObjectUtils.isEmpty(userData)) { + return userData.getUserName(); + } + return null; + } + + /** + * 账户是否未过期,过期无法验证 + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonExpired() + { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonLocked() + { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isCredentialsNonExpired() + { + return true; + } + + /** + * 是否可用 ,禁用的用户不能身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isEnabled() + { + return true; + } + + @Override + public Collection getAuthorities() + { + return Collections.emptyList(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java new file mode 100644 index 0000000..868a1fc --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java @@ -0,0 +1,11 @@ +package com.ruoyi.common.core.domain.model; + +/** + * 用户注册对象 + * + * @author ruoyi + */ +public class RegisterBody extends LoginBody +{ + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDataInfo.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDataInfo.java new file mode 100644 index 0000000..00918a3 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDataInfo.java @@ -0,0 +1,36 @@ +package com.ruoyi.common.core.page; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 表格分页数据对象 + * + * @author ruoyi + */ +@Data +public class PageDataInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 总记录数 + */ + private long total; + + /** + * 列表数据 + */ + private List rows; + + /** + * 消息状态码 + */ + private int code; + + /** + * 消息内容 + */ + private String msg; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java new file mode 100644 index 0000000..8966cb4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java @@ -0,0 +1,101 @@ +package com.ruoyi.common.core.page; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 分页数据 + * + * @author ruoyi + */ +public class PageDomain +{ + /** 当前记录起始索引 */ + private Integer pageNum; + + /** 每页显示记录数 */ + private Integer pageSize; + + /** 排序列 */ + private String orderByColumn; + + /** 排序的方向desc或者asc */ + private String isAsc = "asc"; + + /** 分页参数合理化 */ + private Boolean reasonable = true; + + public String getOrderBy() + { + if (StringUtils.isEmpty(orderByColumn)) + { + return ""; + } + return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc; + } + + public Integer getPageNum() + { + return pageNum; + } + + public void setPageNum(Integer pageNum) + { + this.pageNum = pageNum; + } + + public Integer getPageSize() + { + return pageSize; + } + + public void setPageSize(Integer pageSize) + { + this.pageSize = pageSize; + } + + public String getOrderByColumn() + { + return orderByColumn; + } + + public void setOrderByColumn(String orderByColumn) + { + this.orderByColumn = orderByColumn; + } + + public String getIsAsc() + { + return isAsc; + } + + public void setIsAsc(String isAsc) + { + if (StringUtils.isNotEmpty(isAsc)) + { + // 兼容前端排序类型 + if ("ascending".equals(isAsc)) + { + isAsc = "asc"; + } + else if ("descending".equals(isAsc)) + { + isAsc = "desc"; + } + this.isAsc = isAsc; + } + } + + public Boolean getReasonable() + { + if (StringUtils.isNull(reasonable)) + { + return Boolean.TRUE; + } + return reasonable; + } + + public void setReasonable(Boolean reasonable) + { + this.reasonable = reasonable; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java new file mode 100644 index 0000000..847685b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java @@ -0,0 +1,85 @@ +package com.ruoyi.common.core.page; + +import java.io.Serializable; +import java.util.List; + +/** + * 表格分页数据对象 + * + * @author ruoyi + */ +public class TableDataInfo implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 总记录数 */ + private long total; + + /** 列表数据 */ + private List rows; + + /** 消息状态码 */ + private int code; + + /** 消息内容 */ + private String msg; + + /** + * 表格数据对象 + */ + public TableDataInfo() + { + } + + /** + * 分页 + * + * @param list 列表数据 + * @param total 总记录数 + */ + public TableDataInfo(List list, int total) + { + this.rows = list; + this.total = total; + } + + public long getTotal() + { + return total; + } + + public void setTotal(long total) + { + this.total = total; + } + + public List getRows() + { + return rows; + } + + public void setRows(List rows) + { + this.rows = rows; + } + + public int getCode() + { + return code; + } + + public void setCode(int code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java new file mode 100644 index 0000000..a120c30 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java @@ -0,0 +1,56 @@ +package com.ruoyi.common.core.page; + +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 表格数据处理 + * + * @author ruoyi + */ +public class TableSupport +{ + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 分页参数合理化 + */ + public static final String REASONABLE = "reasonable"; + + /** + * 封装分页对象 + */ + public static PageDomain getPageDomain() + { + PageDomain pageDomain = new PageDomain(); + pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1)); + pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10)); + pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN)); + pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC)); + pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE)); + return pageDomain; + } + + public static PageDomain buildPageRequest() + { + return getPageDomain(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java new file mode 100644 index 0000000..c09df1c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java @@ -0,0 +1,284 @@ +package com.ruoyi.common.core.redis; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.BoundSetOperations; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +/** + * spring redis 工具类 + * + * @author ruoyi + **/ +@SuppressWarnings(value = { "unchecked", "rawtypes" }) +@Component +public class RedisCache +{ + @Autowired + public RedisTemplate redisTemplate; + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) + { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) + { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) + { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) + { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) + { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) + { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) + { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) + { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public boolean deleteObject(final Collection collection) + { + return redisTemplate.delete(collection) > 0; + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) + { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) + { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) + { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) + { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) + { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) + { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * + * @param key + * @param dataMap + * @param TTL + * @param unit + * @param + */ + public void setCacheMap(final String key, final Map dataMap,Integer TTL,TimeUnit unit) + { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + redisTemplate.opsForHash().getOperations().expire(key,TTL,unit); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) + { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) + { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) + { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) + { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) + { + return redisTemplate.opsForHash().delete(key, hKey) > 0; + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) + { + return redisTemplate.keys(pattern); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java new file mode 100644 index 0000000..84124aa --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java @@ -0,0 +1,86 @@ +package com.ruoyi.common.core.text; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import com.ruoyi.common.utils.StringUtils; + +/** + * 字符集工具类 + * + * @author ruoyi + */ +public class CharsetKit +{ + /** ISO-8859-1 */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** UTF-8 */ + public static final String UTF_8 = "UTF-8"; + /** GBK */ + public static final String GBK = "GBK"; + + /** ISO-8859-1 */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** UTF-8 */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** GBK */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) + { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) + { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) + { + if (null == srcCharset) + { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) + { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) + { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() + { + return Charset.defaultCharset().name(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java new file mode 100644 index 0000000..0066112 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java @@ -0,0 +1,1006 @@ +package com.ruoyi.common.core.text; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Set; +import com.ruoyi.common.utils.StringUtils; +import org.apache.commons.lang3.ArrayUtils; + +/** + * 类型转换器 + * + * @author ruoyi + */ +public class Convert +{ + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof String) + { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) + { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof Character) + { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) + { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Byte) + { + return (Byte) value; + } + if (value instanceof Number) + { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Byte.parseByte(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) + { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Short) + { + return (Short) value; + } + if (value instanceof Number) + { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Short.parseShort(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) + { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Number) + { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return NumberFormat.getInstance().parse(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) + { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Integer) + { + return (Integer) value; + } + if (value instanceof Number) + { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Integer.parseInt(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) + { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) + { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) + { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Integer[] {}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Long[] {}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) + { + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) + { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Long) + { + return (Long) value; + } + if (value instanceof Number) + { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) + { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Double) + { + return (Double) value; + } + if (value instanceof Number) + { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) + { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Float) + { + return (Float) value; + } + if (value instanceof Number) + { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Float.parseFloat(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) + { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Boolean) + { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) + { + case "true": + case "yes": + case "ok": + case "1": + return true; + case "false": + case "no": + case "0": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) + { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) + { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Enum.valueOf(clazz, valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) + { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigInteger) + { + return (BigInteger) value; + } + if (value instanceof Long) + { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigInteger(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) + { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigDecimal) + { + return (BigDecimal) value; + } + if (value instanceof Long) + { + return new BigDecimal((Long) value); + } + if (value instanceof Double) + { + return BigDecimal.valueOf((Double) value); + } + if (value instanceof Integer) + { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigDecimal(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) + { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) + { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) + { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) + { + if (null == obj) + { + return null; + } + + if (obj instanceof String) + { + return (String) obj; + } + else if (obj instanceof byte[]) + { + return str((byte[]) obj, charset); + } + else if (obj instanceof Byte[]) + { + byte[] bytes = ArrayUtils.toPrimitive((Byte[]) obj); + return str(bytes, charset); + } + else if (obj instanceof ByteBuffer) + { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) + { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) + { + if (data == null) + { + return null; + } + + if (null == charset) + { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) + { + if (data == null) + { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) + { + if (null == charset) + { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) + { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) + { + char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') + { + c[i] = '\u3000'; + } + else if (c[i] < '\177') + { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) + { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) + { + char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') + { + c[i] = ' '; + } + else if (c[i] > '\uFF00' && c[i] < '\uFF5F') + { + c[i] = (char) (c[i] - 65248); + } + } + String returnString = new String(c); + + return returnString; + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) + { + String[] fraction = { "角", "分" }; + String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } }; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) + { + // 优化double计算精度丢失问题 + BigDecimal nNum = new BigDecimal(n); + BigDecimal decimal = new BigDecimal(10); + BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN); + double d = scale.doubleValue(); + s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) + { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) + { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) + { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java new file mode 100644 index 0000000..c78ac77 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java @@ -0,0 +1,92 @@ +package com.ruoyi.common.core.text; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 字符串格式化 + * + * @author ruoyi + */ +public class StrFormatter +{ + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) + { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) + { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) + { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) + { + if (handledPosition == 0) + { + return strPattern; + } + else + { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } + else + { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) + { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) + { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + else + { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } + else + { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java new file mode 100644 index 0000000..10b7306 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java @@ -0,0 +1,20 @@ +package com.ruoyi.common.enums; + +/** + * 操作状态 + * + * @author ruoyi + * + */ +public enum BusinessStatus +{ + /** + * 成功 + */ + SUCCESS, + + /** + * 失败 + */ + FAIL, +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java new file mode 100644 index 0000000..2e17c4a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.enums; + +/** + * 业务操作类型 + * + * @author ruoyi + */ +public enum BusinessType +{ + /** + * 其它 + */ + OTHER, + + /** + * 新增 + */ + INSERT, + + /** + * 修改 + */ + UPDATE, + + /** + * 删除 + */ + DELETE, + + /** + * 授权 + */ + GRANT, + + /** + * 导出 + */ + EXPORT, + + /** + * 导入 + */ + IMPORT, + + /** + * 强退 + */ + FORCE, + + /** + * 生成代码 + */ + GENCODE, + + /** + * 清空数据 + */ + CLEAN, +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java new file mode 100644 index 0000000..0d945be --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.enums; + +/** + * 数据源 + * + * @author ruoyi + */ +public enum DataSourceType +{ + /** + * 主库 + */ + MASTER, + + /** + * 从库 + */ + SLAVE +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java new file mode 100644 index 0000000..be6f739 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java @@ -0,0 +1,36 @@ +package com.ruoyi.common.enums; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.lang.Nullable; + +/** + * 请求方式 + * + * @author ruoyi + */ +public enum HttpMethod +{ + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + private static final Map mappings = new HashMap<>(16); + + static + { + for (HttpMethod httpMethod : values()) + { + mappings.put(httpMethod.name(), httpMethod); + } + } + + @Nullable + public static HttpMethod resolve(@Nullable String method) + { + return (method != null ? mappings.get(method) : null); + } + + public boolean matches(String method) + { + return (this == resolve(method)); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java new file mode 100644 index 0000000..c609fd8 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java @@ -0,0 +1,20 @@ +package com.ruoyi.common.enums; + +/** + * 限流类型 + * + * @author ruoyi + */ + +public enum LimitType +{ + /** + * 默认策略全局限流 + */ + DEFAULT, + + /** + * 根据请求者IP进行限流 + */ + IP +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java new file mode 100644 index 0000000..bdd143c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.enums; + +/** + * 操作人类别 + * + * @author ruoyi + */ +public enum OperatorType +{ + /** + * 其它 + */ + OTHER, + + /** + * 后台用户 + */ + MANAGE, + + /** + * 手机端用户 + */ + MOBILE +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java new file mode 100644 index 0000000..d7ff44a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java @@ -0,0 +1,30 @@ +package com.ruoyi.common.enums; + +/** + * 用户状态 + * + * @author ruoyi + */ +public enum UserStatus +{ + OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除"); + + private final String code; + private final String info; + + UserStatus(String code, String info) + { + this.code = code; + this.info = info; + } + + public String getCode() + { + return code; + } + + public String getInfo() + { + return info; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java new file mode 100644 index 0000000..f6ad2ab --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java @@ -0,0 +1,15 @@ +package com.ruoyi.common.exception; + +/** + * 演示模式异常 + * + * @author ruoyi + */ +public class DemoModeException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public DemoModeException() + { + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java new file mode 100644 index 0000000..81a71b5 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java @@ -0,0 +1,58 @@ +package com.ruoyi.common.exception; + +/** + * 全局异常 + * + * @author ruoyi + */ +public class GlobalException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public GlobalException() + { + } + + public GlobalException(String message) + { + this.message = message; + } + + public String getDetailMessage() + { + return detailMessage; + } + + public GlobalException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } + + @Override + public String getMessage() + { + return message; + } + + public GlobalException setMessage(String message) + { + this.message = message; + return this; + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java new file mode 100644 index 0000000..fcc7ab6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java @@ -0,0 +1,74 @@ +package com.ruoyi.common.exception; + +/** + * 业务异常 + * + * @author ruoyi + */ +public final class ServiceException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() + { + } + + public ServiceException(String message) + { + this.message = message; + } + + public ServiceException(String message, Integer code) + { + this.message = message; + this.code = code; + } + + public String getDetailMessage() + { + return detailMessage; + } + + @Override + public String getMessage() + { + return message; + } + + public Integer getCode() + { + return code; + } + + public ServiceException setMessage(String message) + { + this.message = message; + return this; + } + + public ServiceException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java new file mode 100644 index 0000000..980fa46 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java @@ -0,0 +1,26 @@ +package com.ruoyi.common.exception; + +/** + * 工具类异常 + * + * @author ruoyi + */ +public class UtilException extends RuntimeException +{ + private static final long serialVersionUID = 8247610319171014183L; + + public UtilException(Throwable e) + { + super(e.getMessage(), e); + } + + public UtilException(String message) + { + super(message); + } + + public UtilException(String message, Throwable throwable) + { + super(message, throwable); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java new file mode 100644 index 0000000..b55d72e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java @@ -0,0 +1,97 @@ +package com.ruoyi.common.exception.base; + +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 基础异常 + * + * @author ruoyi + */ +public class BaseException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 所属模块 + */ + private String module; + + /** + * 错误码 + */ + private String code; + + /** + * 错误码对应的参数 + */ + private Object[] args; + + /** + * 错误消息 + */ + private String defaultMessage; + + public BaseException(String module, String code, Object[] args, String defaultMessage) + { + this.module = module; + this.code = code; + this.args = args; + this.defaultMessage = defaultMessage; + } + + public BaseException(String module, String code, Object[] args) + { + this(module, code, args, null); + } + + public BaseException(String module, String defaultMessage) + { + this(module, null, null, defaultMessage); + } + + public BaseException(String code, Object[] args) + { + this(null, code, args, null); + } + + public BaseException(String defaultMessage) + { + this(null, null, null, defaultMessage); + } + + @Override + public String getMessage() + { + String message = null; + if (!StringUtils.isEmpty(code)) + { + message = MessageUtils.message(code, args); + } + if (message == null) + { + message = defaultMessage; + } + return message; + } + + public String getModule() + { + return module; + } + + public String getCode() + { + return code; + } + + public Object[] getArgs() + { + return args; + } + + public String getDefaultMessage() + { + return defaultMessage; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java new file mode 100644 index 0000000..871f09b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.exception.file; + +import com.ruoyi.common.exception.base.BaseException; + +/** + * 文件信息异常类 + * + * @author ruoyi + */ +public class FileException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public FileException(String code, Object[] args) + { + super("file", code, args, null); + } + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java new file mode 100644 index 0000000..70e0ec9 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.file; + +/** + * 文件名称超长限制异常类 + * + * @author ruoyi + */ +public class FileNameLengthLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileNameLengthLimitExceededException(int defaultFileNameLength) + { + super("upload.filename.exceed.length", new Object[] { defaultFileNameLength }); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java new file mode 100644 index 0000000..ec6ab05 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.file; + +/** + * 文件名大小限制异常类 + * + * @author ruoyi + */ +public class FileSizeLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileSizeLimitExceededException(long defaultMaxSize) + { + super("upload.exceed.maxSize", new Object[] { defaultMaxSize }); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java new file mode 100644 index 0000000..f45e7ef --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java @@ -0,0 +1,61 @@ +package com.ruoyi.common.exception.file; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * 文件上传异常类 + * + * @author ruoyi + */ +public class FileUploadException extends Exception +{ + + private static final long serialVersionUID = 1L; + + private final Throwable cause; + + public FileUploadException() + { + this(null, null); + } + + public FileUploadException(final String msg) + { + this(msg, null); + } + + public FileUploadException(String msg, Throwable cause) + { + super(msg); + this.cause = cause; + } + + @Override + public void printStackTrace(PrintStream stream) + { + super.printStackTrace(stream); + if (cause != null) + { + stream.println("Caused by:"); + cause.printStackTrace(stream); + } + } + + @Override + public void printStackTrace(PrintWriter writer) + { + super.printStackTrace(writer); + if (cause != null) + { + writer.println("Caused by:"); + cause.printStackTrace(writer); + } + } + + @Override + public Throwable getCause() + { + return cause; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java new file mode 100644 index 0000000..011f308 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java @@ -0,0 +1,80 @@ +package com.ruoyi.common.exception.file; + +import java.util.Arrays; + +/** + * 文件上传 误异常类 + * + * @author ruoyi + */ +public class InvalidExtensionException extends FileUploadException +{ + private static final long serialVersionUID = 1L; + + private String[] allowedExtension; + private String extension; + private String filename; + + public InvalidExtensionException(String[] allowedExtension, String extension, String filename) + { + super("文件[" + filename + "]后缀[" + extension + "]不正确,请上传" + Arrays.toString(allowedExtension) + "格式"); + this.allowedExtension = allowedExtension; + this.extension = extension; + this.filename = filename; + } + + public String[] getAllowedExtension() + { + return allowedExtension; + } + + public String getExtension() + { + return extension; + } + + public String getFilename() + { + return filename; + } + + public static class InvalidImageExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidFlashExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidMediaExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidVideoExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java new file mode 100644 index 0000000..a567b40 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java @@ -0,0 +1,34 @@ +package com.ruoyi.common.exception.job; + +/** + * 计划策略异常 + * + * @author ruoyi + */ +public class TaskException extends Exception +{ + private static final long serialVersionUID = 1L; + + private Code code; + + public TaskException(String msg, Code code) + { + this(msg, code, null); + } + + public TaskException(String msg, Code code, Exception nestedEx) + { + super(msg, nestedEx); + this.code = code; + } + + public Code getCode() + { + return code; + } + + public enum Code + { + TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java new file mode 100644 index 0000000..2bf5038 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 黑名单IP异常类 + * + * @author ruoyi + */ +public class BlackListException extends UserException +{ + private static final long serialVersionUID = 1L; + + public BlackListException() + { + super("login.blocked", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java new file mode 100644 index 0000000..389dbc7 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 验证码错误异常类 + * + * @author ruoyi + */ +public class CaptchaException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaException() + { + super("user.jcaptcha.error", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java new file mode 100644 index 0000000..85f9486 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 验证码失效异常类 + * + * @author ruoyi + */ +public class CaptchaExpireException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaExpireException() + { + super("user.jcaptcha.expire", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java new file mode 100644 index 0000000..c292d70 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.exception.user; + +import com.ruoyi.common.exception.base.BaseException; + +/** + * 用户信息异常类 + * + * @author ruoyi + */ +public class UserException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public UserException(String code, Object[] args) + { + super("user", code, args, null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java new file mode 100644 index 0000000..eff8181 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户不存在异常类 + * + * @author ruoyi + */ +public class UserNotExistsException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserNotExistsException() + { + super("user.not.exists", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java new file mode 100644 index 0000000..a7f3e5f --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户密码不正确或不符合规范异常类 + * + * @author ruoyi + */ +public class UserPasswordNotMatchException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordNotMatchException() + { + super("user.password.not.match", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java new file mode 100644 index 0000000..c887cf1 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户错误最大次数异常类 + * + * @author ruoyi + */ +public class UserPasswordRetryLimitExceedException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime) + { + super("user.password.retry.limit.exceed", new Object[] { retryLimitCount, lockTime }); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/AesDecryptFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/AesDecryptFilter.java new file mode 100644 index 0000000..341626d --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/AesDecryptFilter.java @@ -0,0 +1,139 @@ +package com.ruoyi.common.filter; + +import com.ruoyi.common.utils.StringUtils; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; + +import java.io.IOException; +import java.util.regex.Pattern; + +/** + * AES 加解密过滤器 + *

+ * 拦截 POST / PUT 请求,对 application/json 类型的请求体进行 AES 解密; + * 对 /api/*、/promo/*、/admin/*下返回 application/json 类型的响应进行 AES 加密。 + * 支持全局开关(通过 {@code aes.enabled} 配置)与请求级临时关闭({@code X-Debug-Encrypt: false} Header)。 + *

+ * + * @author ruoyi + */ +public class AesDecryptFilter implements Filter { + + private static final Logger log = LoggerFactory.getLogger(AesDecryptFilter.class); + + /** + * 请求 Header 名称:将其值设为 "false" 可在单次请求中临时跳过解密(调试专用) + */ + public static final String HEADER_DEBUG_ENCRYPT = "X-Debug-Encrypt"; + + /** + * 第三方支付/发货回调路径正则:形如 {@code /api/{渠道}/notify}(单层渠道段), + * 同时兼容 callback/callBack 变体(如 jiujia、alipay)。这类接口由平台明文直连回调,须整体跳过 AES 加解密。 + */ + private static final Pattern PAYMENT_CALLBACK_PATTERN = + Pattern.compile("^/api/[^/]+/(notify|callback|callBack)$"); + + private final String aesKey; + private final String aesIv; + + public AesDecryptFilter(String aesKey, String aesIv) { + this.aesKey = aesKey; + this.aesIv = aesIv; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (request instanceof HttpServletRequest httpRequest) { + String method = httpRequest.getMethod(); + String contentType = httpRequest.getContentType(); + String requestUri = httpRequest.getRequestURI(); + + // 排除滑块验证码接口(第三方 TAC 库通过原生请求发送,不经过前端 axios,无法 AES 加密) + // Cocos 客户端用 /api/captcha/*,推广后台用 /promo/captcha/* + if (requestUri.startsWith("/api/captcha/") || requestUri.startsWith("/promo/captcha/")) { + log.debug("[AES] 跳过滑块验证码接口:{}", requestUri); + chain.doFilter(request, response); + return; + } + + // 排除第三方支付/发货回调接口(hipay、chengduhipay、richku、itpay、abkpay、snappay 等渠道的 notify/callback)。 + // 第三方平台直接以明文(JSON 或 form-urlencoded)回调,无法进行 AES 加密,且要求应答为裸文本 + // (success / OK / SUCCESS),若走 AES 会因解密失败返回 400、或应答被加密导致对账失败反复重推, + // 故请求与响应均直接放行、跳过 AES 加解密。 + if (isPaymentCallback(requestUri)) { + log.debug("[AES] 跳过第三方支付回调接口:{}", requestUri); + chain.doFilter(request, response); + return; + } + + // 管理后台 (/admin/*)、推广后台 (/promo/*)、Cocos 客户端 (/api/*) 都开启 AES + boolean isTargetPath = requestUri.startsWith("/api/") + || requestUri.startsWith("/promo/") + || requestUri.startsWith("/admin/"); + + if (isTargetPath) { + // 统一包装响应,用于后续 AES 加密 + AesEncryptResponseWrapper responseWrapper = new AesEncryptResponseWrapper((HttpServletResponse) response); + + // 仅对 POST/PUT 且 Content-Type 为 application/json/text/plain 的请求体解密 + boolean isPostOrPut = HttpMethod.POST.matches(method) || HttpMethod.PUT.matches(method); + boolean isJson = StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE) + || StringUtils.startsWithIgnoreCase(contentType, MediaType.TEXT_PLAIN_VALUE); + if (isPostOrPut && isJson) { + // 检查请求级临时开关: Header X-Debug-Encrypt: false 跳过解密 + String debugHeader = httpRequest.getHeader(HEADER_DEBUG_ENCRYPT); + if ("false".equalsIgnoreCase(debugHeader)) { + log.info("[AES] 请求级临时关闭解密 (X-Debug-Encrypt: false),请求:{}", requestUri); + chain.doFilter(request, responseWrapper); + responseWrapper.flushAndEncrypt(aesKey, aesIv); + return; + } + AesDecryptRequestWrapper wrapper = new AesDecryptRequestWrapper(httpRequest, responseWrapper, aesKey, aesIv); + if (wrapper.isDecryptFailed()) { + log.warn("[AES] 拒绝明文请求,返回 400,URI: {}", requestUri); + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + httpResponse.setContentType("application/json;charset=UTF-8"); + httpResponse.getWriter().write("{\"code\":400,\"msg\":\"请求参数错误\"}"); + return; + } + chain.doFilter(wrapper, responseWrapper); + responseWrapper.flushAndEncrypt(aesKey, aesIv); + return; + } + + // GET 等其他请求不解密请求体,但响应仍加密 + chain.doFilter(request, responseWrapper); + responseWrapper.flushAndEncrypt(aesKey, aesIv); + return; + } + } + chain.doFilter(request, response); + } + + /** + * 判断是否为第三方支付/发货回调接口。 + *

+ * 这类接口由第三方平台直接回调,请求体为明文(JSON 或 form-urlencoded),应答须为裸文本, + * 必须跳过 AES 加解密,否则会因解密失败返回 400,或应答被加密导致平台反复重推。 + * 约定:形如 {@code /api/{渠道}/notify}(及 callback/callBack 变体)的路径均视为第三方回调。 + *

+ */ + private boolean isPaymentCallback(String requestUri) { + return requestUri != null && PAYMENT_CALLBACK_PATTERN.matcher(requestUri).matches(); + } + + @Override + public void destroy() { + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/AesDecryptRequestWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/AesDecryptRequestWrapper.java new file mode 100644 index 0000000..fb08f65 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/AesDecryptRequestWrapper.java @@ -0,0 +1,99 @@ +package com.ruoyi.common.filter; + +import com.ruoyi.common.utils.AesUtils; +import com.ruoyi.common.constant.Constants; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * AES 解密请求包装器 + * 对 POST 请求体中的加密数据进行解密,使明文对后续过滤器和 Controller 透明 + * + * @author ruoyi + */ +public class AesDecryptRequestWrapper extends HttpServletRequestWrapper { + + private static final Logger log = LoggerFactory.getLogger(AesDecryptRequestWrapper.class); + + private final byte[] decryptedBody; + /** 解密是否失败(失败时 Filter 应返回 400) */ + private boolean decryptFailed = false; + + public AesDecryptRequestWrapper(HttpServletRequest request, ServletResponse response, + String aesKey, String aesIv) throws IOException { + super(request); + request.setCharacterEncoding(Constants.UTF8); + response.setCharacterEncoding(Constants.UTF8); + + byte[] rawBody = IOUtils.toByteArray(request.getInputStream()); + String rawBodyStr = new String(rawBody, StandardCharsets.UTF_8).trim(); + + byte[] body; + if (rawBodyStr.isEmpty()) { + body = rawBody; + } else { + try { + String decrypted = AesUtils.decrypt(rawBodyStr, aesKey, aesIv); + log.info("[AES] 解密成功,URI: {}", request.getRequestURI()); + body = decrypted.getBytes(StandardCharsets.UTF_8); + } catch (Exception e) { + log.warn("[AES] 请求体解密失败,URI: {},可能是明文请求,已拒绝: {}", request.getRequestURI(), e.getMessage()); + body = rawBody; + this.decryptFailed = true; + } + } + this.decryptedBody = body; + } + + public boolean isDecryptFailed() { + return decryptFailed; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream bais = new ByteArrayInputStream(decryptedBody); + return new ServletInputStream() { + @Override + public int read() throws IOException { + return bais.read(); + } + + @Override + public int available() throws IOException { + return decryptedBody.length; + } + + @Override + public boolean isFinished() { + return bais.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/AesEncryptResponseWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/AesEncryptResponseWrapper.java new file mode 100644 index 0000000..e7f3809 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/AesEncryptResponseWrapper.java @@ -0,0 +1,138 @@ +package com.ruoyi.common.filter; + +import com.ruoyi.common.utils.AesUtils; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * AES 加密响应包装器 + *

+ * 缓存后端写出的响应内容,在 {@link #flushAndEncrypt()} 中统一 AES 加密后再写回真实响应。 + * 仅对 Content-Type 包含 application/json 的响应进行加密。 + *

+ */ +public class AesEncryptResponseWrapper extends HttpServletResponseWrapper { + + private final ByteArrayOutputStream buffer; + private final ServletOutputStream outputStream; + private PrintWriter writer; + + private boolean writerUsed = false; + private boolean streamUsed = false; + + public AesEncryptResponseWrapper(HttpServletResponse response) { + super(response); + this.buffer = new ByteArrayOutputStream(); + this.outputStream = new ServletOutputStream() { + @Override + public void write(int b) throws IOException { + buffer.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + buffer.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + buffer.write(b, off, len); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + } + }; + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + if (writerUsed) { + throw new IllegalStateException("getWriter() 已被调用,不能再次调用 getOutputStream()"); + } + streamUsed = true; + return outputStream; + } + + @Override + public PrintWriter getWriter() throws IOException { + if (streamUsed) { + throw new IllegalStateException("getOutputStream() 已被调用,不能再次调用 getWriter()"); + } + if (writer == null) { + writer = new PrintWriter(new OutputStreamWriter(buffer, StandardCharsets.UTF_8)); + } + writerUsed = true; + return writer; + } + + /** + * 将缓存的响应内容加密后写回真实响应 + * + * @param aesKey AES 密钥 + * @param aesIv AES 偏移量 + */ + @Override + public void flushBuffer() throws IOException { + // 禁止在加密前把响应提交给客户端 + if (writer != null) { + writer.flush(); + } + } + + public void flushAndEncrypt(String aesKey, String aesIv) throws IOException { + if (writer != null) { + writer.flush(); + } + + byte[] rawBytes = buffer.toByteArray(); + HttpServletResponse response = (HttpServletResponse) getResponse(); + + if (rawBytes.length == 0) { + response.setContentLength(0); + response.getOutputStream().flush(); + return; + } + + String contentType = response.getContentType(); + + // 仅对 application/json 类型响应加密 + if (contentType != null && contentType.toLowerCase().contains("application/json")) { + String plainText = new String(rawBytes, StandardCharsets.UTF_8); + if (!plainText.isEmpty()) { + try { + String cipherText = AesUtils.encrypt(plainText, aesKey, aesIv); + byte[] encryptedBytes = cipherText.getBytes(StandardCharsets.UTF_8); + response.setContentLength(encryptedBytes.length); + response.getOutputStream().write(encryptedBytes); + response.getOutputStream().flush(); + return; + } catch (Exception e) { + // 加密失败时回退明文,避免接口完全不可用 + response.setContentLength(rawBytes.length); + response.getOutputStream().write(rawBytes); + response.getOutputStream().flush(); + return; + } + } + } + + // 非 JSON 响应直接写回 + response.setContentLength(rawBytes.length); + response.getOutputStream().write(rawBytes); + response.getOutputStream().flush(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java new file mode 100644 index 0000000..e1e431b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.filter; + +import com.alibaba.fastjson2.filter.SimplePropertyPreFilter; + +/** + * 排除JSON敏感属性 + * + * @author ruoyi + */ +public class PropertyPreExcludeFilter extends SimplePropertyPreFilter +{ + public PropertyPreExcludeFilter() + { + } + + public PropertyPreExcludeFilter addExcludes(String... filters) + { + for (int i = 0; i < filters.length; i++) + { + this.getExcludes().add(filters[i]); + } + return this; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java new file mode 100644 index 0000000..efeded9 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java @@ -0,0 +1,52 @@ +package com.ruoyi.common.filter; + +import java.io.IOException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.MediaType; +import com.ruoyi.common.utils.StringUtils; + +/** + * Repeatable 过滤器 + * + * @author ruoyi + */ +public class RepeatableFilter implements Filter +{ + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + ServletRequest requestWrapper = null; + if (request instanceof HttpServletRequest + && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) + { + requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response); + } + if (null == requestWrapper) + { + chain.doFilter(request, response); + } + else + { + chain.doFilter(requestWrapper, response); + } + } + + @Override + public void destroy() + { + + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java new file mode 100644 index 0000000..5c6ee08 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java @@ -0,0 +1,76 @@ +package com.ruoyi.common.filter; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import com.ruoyi.common.utils.http.HttpHelper; +import com.ruoyi.common.constant.Constants; + +/** + * 构建可重复读取inputStream的request + * + * @author ruoyi + */ +public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper +{ + private final byte[] body; + + public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException + { + super(request); + request.setCharacterEncoding(Constants.UTF8); + response.setCharacterEncoding(Constants.UTF8); + + body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8); + } + + @Override + public BufferedReader getReader() throws IOException + { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() + { + @Override + public int read() throws IOException + { + return bais.read(); + } + + @Override + public int available() throws IOException + { + return body.length; + } + + @Override + public boolean isFinished() + { + return false; + } + + @Override + public boolean isReady() + { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) + { + + } + }; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java new file mode 100644 index 0000000..da76913 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java @@ -0,0 +1,75 @@ +package com.ruoyi.common.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.enums.HttpMethod; + +/** + * 防止XSS攻击的过滤器 + * + * @author ruoyi + */ +public class XssFilter implements Filter +{ + /** + * 排除链接 + */ + public List excludes = new ArrayList<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + String tempExcludes = filterConfig.getInitParameter("excludes"); + if (StringUtils.isNotEmpty(tempExcludes)) + { + String[] url = tempExcludes.split(","); + for (int i = 0; url != null && i < url.length; i++) + { + excludes.add(url[i]); + } + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + if (handleExcludeURL(req, resp)) + { + chain.doFilter(request, response); + return; + } + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } + + private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) + { + String url = request.getServletPath(); + String method = request.getMethod(); + // GET DELETE 不过滤 + if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) + { + return true; + } + return StringUtils.matches(url, excludes); + } + + @Override + public void destroy() + { + + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..bf7837b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java @@ -0,0 +1,111 @@ +package com.ruoyi.common.filter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.html.EscapeUtil; + +/** + * XSS过滤处理 + * + * @author ruoyi + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper +{ + /** + * @param request + */ + public XssHttpServletRequestWrapper(HttpServletRequest request) + { + super(request); + } + + @Override + public String[] getParameterValues(String name) + { + String[] values = super.getParameterValues(name); + if (values != null) + { + int length = values.length; + String[] escapesValues = new String[length]; + for (int i = 0; i < length; i++) + { + // 防xss攻击和过滤前后空格 + escapesValues[i] = EscapeUtil.clean(values[i]).trim(); + } + return escapesValues; + } + return super.getParameterValues(name); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + // 非json类型,直接返回 + if (!isJsonRequest()) + { + return super.getInputStream(); + } + + // 为空,直接返回 + String json = IOUtils.toString(super.getInputStream(), "utf-8"); + if (StringUtils.isEmpty(json)) + { + return super.getInputStream(); + } + + // xss过滤 + json = EscapeUtil.clean(json).trim(); + byte[] jsonBytes = json.getBytes("utf-8"); + final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes); + return new ServletInputStream() + { + @Override + public boolean isFinished() + { + return true; + } + + @Override + public boolean isReady() + { + return true; + } + + @Override + public int available() throws IOException + { + return jsonBytes.length; + } + + @Override + public void setReadListener(ReadListener readListener) + { + } + + @Override + public int read() throws IOException + { + return bis.read(); + } + }; + } + + /** + * 是否是Json请求 + * + * @param request + */ + public boolean isJsonRequest() + { + String header = super.getHeader(HttpHeaders.CONTENT_TYPE); + return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/rabbitmq/config/DelayedQueueConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/rabbitmq/config/DelayedQueueConfig.java new file mode 100644 index 0000000..5e88630 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/rabbitmq/config/DelayedQueueConfig.java @@ -0,0 +1,85 @@ +package com.ruoyi.common.rabbitmq.config; + +import org.springframework.amqp.core.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class DelayedQueueConfig { + + //死信交换机,队列,路由相关配置 + public static final String DLK_EXCHANGE = "dlk_exchange"; + public static final String DLK_ROUTEKEY = "dlk_routeKey"; + public static final String DLK_QUEUE = "dlk_queue"; + + // 业务交换机... + public static final String OPEN_ROLL_EXCHANGE = "open_roll_exchange"; + public static final String OPEN_ROLL_QUEUE = "open_roll_queue"; + public static final String OPEN_ROLL_KEY = "open_roll_key"; + + // 业务交换机... + public static final String DELIVERY_QUEUE = "delivery_queue"; + + + // roll开奖倒计时 + @Bean + public CustomExchange openRollExchange() { + return new CustomExchange(OPEN_ROLL_EXCHANGE,"direct", true, false); + } + @Bean + public Queue openRollQueue() { + + //只需要在声明业务队列时添加x-dead-letter-exchange,值为死信交换机 + Map map = new HashMap<>(1); + map.put("x-dead-letter-exchange",DLK_EXCHANGE); + //该参数x-dead-letter-routing-key可以修改该死信的路由key,不设置则使用原消息的路由key + map.put("x-dead-letter-routing-key",DLK_ROUTEKEY); + + return new Queue(OPEN_ROLL_QUEUE,true,false,false,map); + } + @Bean + public Binding openRollBind(Queue openRollQueue, CustomExchange openRollExchange) { + return BindingBuilder + .bind(openRollQueue) + .to(openRollExchange) + .with(OPEN_ROLL_KEY) + .noargs(); + } + + // 死信交换机 + @Bean + public DirectExchange dlkExchange(){ + return new DirectExchange(DLK_EXCHANGE,true,false); + } + @Bean + public Queue dlkQueue(){ + return new Queue(DLK_QUEUE,true,false,false); + } + @Bean + public Binding dlkBind(){ + return BindingBuilder.bind(dlkQueue()).to(dlkExchange()).with(DLK_ROUTEKEY); + } + + + + // 自动提货业务消息队列 + //@Bean + public Queue deliveryQueue() { + return new Queue(DELIVERY_QUEUE); + } + + // 游戏消费佣金分发队列(疯狂转盘等游戏下注后,异步发佣金给上级) + public static final String WHEEL_COMMISSION_QUEUE = "wheel_commission_queue"; + + @Bean + public Queue wheelCommissionQueue() { + return new Queue(WHEEL_COMMISSION_QUEUE, true, false, false); + } + +} + diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/rabbitmq/config/FightQueueConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/rabbitmq/config/FightQueueConfig.java new file mode 100644 index 0000000..afa9068 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/rabbitmq/config/FightQueueConfig.java @@ -0,0 +1,41 @@ +package com.ruoyi.common.rabbitmq.config; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.CustomExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; + +// 弃用 +//@Configuration +public class FightQueueConfig { + + // public static final String FIGHT_EXCHANGE = "fight_exchange"; + // public static final String FIGHT_QUEUE = "fight_queue"; + // public static final String FIGHT_ROUTING_KEY = "fight_routingKey"; + // + // @Bean + // public Queue fightQueue() { + // return new Queue(FIGHT_QUEUE); + // } + // + // @Bean + // public CustomExchange fightExchange() { + // HashMap arguments = new HashMap<>(); + // arguments.put("x-fight-type", "direct"); + // // arguments.put("x-fight-type", "direct"); + // return new CustomExchange(FIGHT_EXCHANGE, "x-delayed-message", true, false, arguments); + // // return new CustomExchange(FIGHT_EXCHANGE, "x-fight-message", true, false, arguments); + // } + // + // + // + // @Bean + // public Binding fightQueueBindingFightExchange(Queue fightQueue, CustomExchange fightExchange) { + // return BindingBuilder.bind(fightQueue).to(fightExchange).with(FIGHT_ROUTING_KEY).noargs(); + // } +} + diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/rabbitmq/config/RabbitMQConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/rabbitmq/config/RabbitMQConfig.java new file mode 100644 index 0000000..bb91893 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/rabbitmq/config/RabbitMQConfig.java @@ -0,0 +1,15 @@ +package com.ruoyi.common.rabbitmq.config; + +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + + @Bean + public MessageConverter jsonMessageConverter() { + return new JacksonJsonMessageConverter(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/redis/config/RedisLock.java b/ruoyi-common/src/main/java/com/ruoyi/common/redis/config/RedisLock.java new file mode 100644 index 0000000..8a64b9d --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/redis/config/RedisLock.java @@ -0,0 +1,39 @@ +package com.ruoyi.common.redis.config; + +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class RedisLock { + @Autowired + private RedissonClient redissonClient; + public RLock getRLock(String lockKey) { + return redissonClient.getLock(lockKey); + } + + public Boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) { + RLock rLock = getRLock(lockKey); + boolean tryLock; + try { + tryLock = rLock.tryLock(waitTime, leaseTime, unit); + } catch (InterruptedException e) { + return false; + } + return tryLock; + } + + public void unlock(String lockKey) { + RLock lock = getRLock(lockKey); + + if(lock.isLocked()){ // 是否还是锁定状态 + if(lock.isHeldByCurrentThread()){ // 时候是当前执行线程的锁 + lock.unlock(); // 释放锁 + } + } + // lock.unlock(); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/redis/config/RedissonConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/redis/config/RedissonConfig.java new file mode 100644 index 0000000..d881e61 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/redis/config/RedissonConfig.java @@ -0,0 +1,35 @@ +package com.ruoyi.common.redis.config; + +import com.ruoyi.common.utils.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + @Value("${spring.redis.host}") + private String host; + + @Value("${spring.redis.port}") + private String port; + + @Value("${spring.redis.password}") + private String password; + + @Value("${spring.redis.database}") + private String database; + + @Bean(destroyMethod = "shutdown") + @ConditionalOnMissingBean(RedissonClient.class) + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password); + config.useSingleServer().setDatabase(Integer.parseInt(database)); // 设置Redis索引 + return Redisson.create(config); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/typehandler/JsonObjectHandler.java b/ruoyi-common/src/main/java/com/ruoyi/common/typehandler/JsonObjectHandler.java new file mode 100644 index 0000000..e580d50 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/typehandler/JsonObjectHandler.java @@ -0,0 +1,64 @@ +package com.ruoyi.common.typehandler; + +import com.ruoyi.common.utils.json.SnakeCaseJsonUtils; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + + +public class JsonObjectHandler extends BaseTypeHandler { + + private final Type actualType; + + public JsonObjectHandler() { + actualType = actualType(); + } + + protected Type actualType() { + Type t = getClass().getGenericSuperclass(); + // Cglib代理可能会继承子类,需要取父类名称 + if (!(t instanceof ParameterizedType)) { + t = getClass().getSuperclass().getGenericSuperclass(); + } + ParameterizedType p = (ParameterizedType) t; + return p.getActualTypeArguments()[0]; + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { + String json = SnakeCaseJsonUtils.toSnakeCaseJson(parameter); + if (jdbcType == null) { + ps.setObject(i, json); + } else { + ps.setObject(i, json, jdbcType.TYPE_CODE); + } + } + + @Override + public T getNullableResult(ResultSet rs, String columnName) throws SQLException { + return parseJson(rs.getString(columnName)); + } + + @Override + public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return parseJson(rs.getString(columnIndex)); + } + + @Override + public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return parseJson(cs.getString(columnIndex)); + } + + private T parseJson(String json) { + if (json == null || json.isEmpty()) { + return null; + } + return SnakeCaseJsonUtils.fromSnakeCaseJson(json, actualType); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/AesUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/AesUtils.java new file mode 100644 index 0000000..831e07a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/AesUtils.java @@ -0,0 +1,53 @@ +package com.ruoyi.common.utils; + +import cn.hutool.crypto.symmetric.AES; +import cn.hutool.crypto.Mode; +import cn.hutool.crypto.Padding; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * AES 加解密工具类(CBC模式,PKCS5Padding) + * 前端使用 crypto-js,后端使用 Hutool AES + * key 和 iv 均为 16 字节(128位) + * + * @author ruoyi + */ +public class AesUtils { + + /** + * AES 解密 + * + * @param cipherText Base64 编码的密文 + * @param key 16位密鑰字符串 + * @param iv 16位向量字符串 + * @return 解密后的明文 + */ + public static String decrypt(String cipherText, String key, String iv) { + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + byte[] ivBytes = iv.getBytes(StandardCharsets.UTF_8); + // 显式用 Java 标准库 Base64 解码,与 crypto-js 输出格式完全匹配 + byte[] cipherBytes = Base64.getDecoder().decode(cipherText.trim()); + // Hutool AES 构造器直接接受 byte[],不要传 SecretKeySpec / IvParameterSpec + AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, keyBytes, ivBytes); + byte[] decryptedBytes = aes.decrypt(cipherBytes); + return new String(decryptedBytes, StandardCharsets.UTF_8); + } + + /** + * AES 加密 + * + * @param plainText 明文 + * @param key 16位密鑰字符串 + * @param iv 16位向量字符串 + * @return Base64 编码的密文 + */ + public static String encrypt(String plainText, String key, String iv) { + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + byte[] ivBytes = iv.getBytes(StandardCharsets.UTF_8); + AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, keyBytes, ivBytes); + byte[] encryptedBytes = aes.encrypt(plainText.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encryptedBytes); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java new file mode 100644 index 0000000..b6326c2 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java @@ -0,0 +1,114 @@ +package com.ruoyi.common.utils; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 精确的浮点数运算 + * + * @author ruoyi + */ +public class Arith +{ + + /** 默认除法运算精度 */ + private static final int DEF_DIV_SCALE = 10; + + /** 这个类不能实例化 */ + private Arith() + { + } + + /** + * 提供精确的加法运算。 + * @param v1 被加数 + * @param v2 加数 + * @return 两个参数的和 + */ + public static double add(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.add(b2).doubleValue(); + } + + /** + * 提供精确的减法运算。 + * @param v1 被减数 + * @param v2 减数 + * @return 两个参数的差 + */ + public static double sub(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.subtract(b2).doubleValue(); + } + + /** + * 提供精确的乘法运算。 + * @param v1 被乘数 + * @param v2 乘数 + * @return 两个参数的积 + */ + public static double mul(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.multiply(b2).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 + * 小数点以后10位,以后的数字四舍五入。 + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(double v1, double v2) + { + return div(v1, v2, DEF_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 + * 定精度,以后的数字四舍五入。 + * @param v1 被除数 + * @param v2 除数 + * @param scale 表示表示需要精确到小数点以后几位。 + * @return 两个参数的商 + */ + public static double div(double v1, double v2, int scale) + { + if (scale < 0) + { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + if (b1.compareTo(BigDecimal.ZERO) == 0) + { + return BigDecimal.ZERO.doubleValue(); + } + return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue(); + } + + /** + * 提供精确的小数位四舍五入处理。 + * @param v 需要四舍五入的数字 + * @param scale 小数点后保留几位 + * @return 四舍五入后的结果 + */ + public static double round(double v, int scale) + { + if (scale < 0) + { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b = new BigDecimal(Double.toString(v)); + BigDecimal one = BigDecimal.ONE; + return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateTimeUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateTimeUtils.java new file mode 100644 index 0000000..6543bd8 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateTimeUtils.java @@ -0,0 +1,259 @@ +package com.ruoyi.common.utils; + +import org.apache.commons.lang3.time.DateFormatUtils; + +import java.lang.management.ManagementFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 时间工具类 + * + * @author ruoyi + */ +public class DateTimeUtils extends org.apache.commons.lang3.time.DateUtils { + // 定义系统默认时区常量,避免重复调用,保证线程安全 + public static final ZoneId ZONE_ID = ZoneId.of("Asia/Shanghai"); + public static final ZoneOffset ZONE_OFFSET = ZoneOffset.ofHours(8); // 东八区固定偏移 + + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static String YYYY_MM_DD = "yyyy-MM-dd"; + + public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; + + // 时间戳一套API + public static Long toSeconds(LocalDateTime localDateTime) { + return localDateTime + .atZone(ZONE_ID) // 假设是本地时区时间 + .withZoneSameInstant(ZoneOffset.UTC) // 转到 UTC + .toEpochSecond(); + } + + // LocalDateTime一套API + // 时区转换 + public static LocalDateTime utcToZone8(LocalDateTime utcDateTime) { + return utcDateTime.atZone(ZoneOffset.UTC).withZoneSameInstant(ZONE_ID).toLocalDateTime(); + } + + // 时间戳-LocalDateTime转换API + + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() { + return dateTimeNow(YYYY_MM_DD); + } + + public static final String getTime() { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static final String dateTimeNow() { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static final String dateTimeNow(final String format) { + return parseDateToStr(format, new Date()); + } + + public static final String dateTime(final Date date) { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static final String date(final LocalDateTime date) { + return date.format(DateTimeFormatter.ofPattern(YYYY_MM_DD)); + } + + public static final String dateTime(final LocalDateTime date) { + return date.format(DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS)); + } + public static final String dateTime(final LocalDateTime date, String format) { + return date.format(DateTimeFormatter.ofPattern(format)); + } + + public static final String parseDateToStr(final String format, final Date date) { + return new SimpleDateFormat(format).format(date); + } + + public static final LocalDateTime localDateTime(final String ts, String format) { + return LocalDateTime.parse(ts, DateTimeFormatter.ofPattern(format)); + } + + public static final Date dateTime(final String format, final String ts) { + try { + return new SimpleDateFormat(format).parse(ts); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + /** + * 日期路径 即年/月/日 如2018/08/08 + */ + public static final String datePath() { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static final String dateTime() { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) { + if (str == null) { + return null; + } + try { + return parseDate(str.toString(), parsePatterns); + } catch (ParseException e) { + return null; + } + } + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** + * 计算相差天数 + */ + public static int differentDaysByMillisecond(Date date1, Date date2) { + return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24))); + } + + /** + * 计算时间差 + * + * @param endDate 最后时间 + * @param startTime 开始时间 + * @return 时间差(天/小时/分钟) + */ + public static String timeDistance(Date endDate, Date startTime) { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + // long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - startTime.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + // long sec = diff % nd % nh % nm / ns; + return day + "天" + hour + "小时" + min + "分钟"; + } + + /** + * 增加 LocalDateTime ==> Date + */ + public static Date toDate(LocalDateTime temporalAccessor) { + ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + /** + * 增加 Date ==> LocalDateTime + */ + public static LocalDateTime toLocalDateTime(Date date) { + return date.toInstant() + .atZone(ZONE_ID) + .toLocalDateTime(); + } + + /** + * 增加 LocalDate ==> Date + */ + public static Date toDate(LocalDate temporalAccessor) { + LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0)); + ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + public static Long now() { + return System.currentTimeMillis() / 1000; + } + + public static Long getTodayAtZero() { + return LocalDate.now().atStartOfDay().atZone(ZONE_ID).toEpochSecond(); + } + + public static Long getTomorrowAtZero() { + return LocalDate.now().plusDays(1).atStartOfDay(ZONE_ID).toEpochSecond(); + } + + + /** + * Generate daily date strings between beginTime and endTime in yyyy-MM-dd format + * @param beginTime start date string in yyyy-MM-dd format + * @param endTime end date string in yyyy-MM-dd format + * @return list of date strings in yyyy-MM-dd format + */ + public static List generateDailyDates(String beginTime, String endTime) { + List dates = new ArrayList<>(); + try { + // Convert string dates to LocalDate + LocalDate startDate = LocalDate.parse(beginTime, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + LocalDate endDate = LocalDate.parse(endTime, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + + // Calculate days between start and end + long daysBetween = ChronoUnit.DAYS.between(startDate, endDate); + + // Generate each date in between + for (long i = 0; i <= daysBetween; i++) { + LocalDate currentDate = startDate.plusDays(i); + dates.add(currentDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + } + } catch (Exception e) { + // If parsing fails, return empty list + e.printStackTrace(); + } + return dates; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java new file mode 100644 index 0000000..dc7638c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java @@ -0,0 +1,199 @@ +package com.ruoyi.common.utils; + +import java.lang.management.ManagementFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; +import org.apache.commons.lang3.time.DateFormatUtils; + +/** + * 时间工具类 + * + * @author ruoyi + */ +public class DateUtils extends org.apache.commons.lang3.time.DateUtils +{ + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static String YYYY_MM_DD = "yyyy-MM-dd"; + + public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() + { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() + { + return dateTimeNow(YYYY_MM_DD); + } + + public static final String getTime() + { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static final String dateTimeNow() + { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static final String dateTimeNow(final String format) + { + return parseDateToStr(format, new Date()); + } + + public static final String dateTime(final Date date) + { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static final String parseDateToStr(final String format, final Date date) + { + return new SimpleDateFormat(format).format(date); + } + + public static final Date dateTime(final String format, final String ts) + { + try + { + return new SimpleDateFormat(format).parse(ts); + } + catch (ParseException e) + { + throw new RuntimeException(e); + } + } + + /** + * 日期路径 即年/月/日 如2018/08/08 + */ + public static final String datePath() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static final String dateTime() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) + { + if (str == null) + { + return null; + } + try + { + return parseDate(str.toString(), parsePatterns); + } + catch (ParseException e) + { + return null; + } + } + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() + { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** + * 计算相差天数 + */ + public static int differentDaysByMillisecond(Date date1, Date date2) + { + return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24))); + } + + /** + * 计算时间差 + * + * @param endDate 最后时间 + * @param startTime 开始时间 + * @return 时间差(天/小时/分钟) + */ + public static String timeDistance(Date endDate, Date startTime) + { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + // long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - startTime.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + // long sec = diff % nd % nh % nm / ns; + return day + "天" + hour + "小时" + min + "分钟"; + } + + /** + * 增加 LocalDateTime ==> Date + */ + public static Date toDate(LocalDateTime temporalAccessor) + { + ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + /** + * 增加 LocalDate ==> Date + */ + public static Date toDate(LocalDate temporalAccessor) + { + LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0)); + ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + public static LocalDateTime getNow() { + return LocalDateTime.now(); + } + + public static LocalDateTime getTodayAtZero() { + return LocalDateTime.of(LocalDate.now(), LocalTime.of(0, 0, 0)); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java new file mode 100644 index 0000000..cc5eab2 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java @@ -0,0 +1,186 @@ +package com.ruoyi.common.utils; + +import java.util.Collection; +import java.util.List; +import com.alibaba.fastjson2.JSONArray; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.spring.SpringUtils; + +/** + * 字典工具类 + * + * @author ruoyi + */ +public class DictUtils +{ + /** + * 分隔符 + */ + public static final String SEPARATOR = ","; + + /** + * 设置字典缓存 + * + * @param key 参数键 + * @param dictDatas 字典数据列表 + */ + public static void setDictCache(String key, List dictDatas) + { + SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas); + } + + /** + * 获取字典缓存 + * + * @param key 参数键 + * @return dictDatas 字典数据列表 + */ + public static List getDictCache(String key) + { + JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key)); + if (StringUtils.isNotNull(arrayCache)) + { + return arrayCache.toList(SysDictData.class); + } + return null; + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue) + { + return getDictLabel(dictType, dictValue, SEPARATOR); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel) + { + return getDictValue(dictType, dictLabel, SEPARATOR); + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue, String separator) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StringUtils.isNotNull(datas)) + { + if (StringUtils.containsAny(separator, dictValue)) + { + for (SysDictData dict : datas) + { + for (String value : dictValue.split(separator)) + { + if (value.equals(dict.getDictValue())) + { + propertyString.append(dict.getDictLabel()).append(separator); + break; + } + } + } + } + else + { + for (SysDictData dict : datas) + { + if (dictValue.equals(dict.getDictValue())) + { + return dict.getDictLabel(); + } + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @param separator 分隔符 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel, String separator) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StringUtils.containsAny(separator, dictLabel) && StringUtils.isNotEmpty(datas)) + { + for (SysDictData dict : datas) + { + for (String label : dictLabel.split(separator)) + { + if (label.equals(dict.getDictLabel())) + { + propertyString.append(dict.getDictValue()).append(separator); + break; + } + } + } + } + else + { + for (SysDictData dict : datas) + { + if (dictLabel.equals(dict.getDictLabel())) + { + return dict.getDictValue(); + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 删除指定字典缓存 + * + * @param key 字典键 + */ + public static void removeDictCache(String key) + { + SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key)); + } + + /** + * 清空字典缓存 + */ + public static void clearDictCache() + { + Collection keys = SpringUtils.getBean(RedisCache.class).keys(CacheConstants.SYS_DICT_KEY + "*"); + SpringUtils.getBean(RedisCache.class).deleteObject(keys); + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + public static String getCacheKey(String configKey) + { + return CacheConstants.SYS_DICT_KEY + configKey; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java new file mode 100644 index 0000000..214e4a0 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java @@ -0,0 +1,39 @@ +package com.ruoyi.common.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * 错误信息处理类。 + * + * @author ruoyi + */ +public class ExceptionUtil +{ + /** + * 获取exception的详细错误信息。 + */ + public static String getExceptionMessage(Throwable e) + { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw, true)); + return sw.toString(); + } + + public static String getRootErrorMessage(Exception e) + { + Throwable root = ExceptionUtils.getRootCause(e); + root = (root == null ? e : root); + if (root == null) + { + return ""; + } + String msg = root.getMessage(); + if (msg == null) + { + return "null"; + } + return StringUtils.defaultString(msg); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java new file mode 100644 index 0000000..0de30c6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.utils; + +/** + * 处理并记录日志文件 + * + * @author ruoyi + */ +public class LogUtils +{ + public static String getBlock(Object msg) + { + if (msg == null) + { + msg = ""; + } + return "[" + msg.toString() + "]"; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java new file mode 100644 index 0000000..7dac75a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java @@ -0,0 +1,26 @@ +package com.ruoyi.common.utils; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import com.ruoyi.common.utils.spring.SpringUtils; + +/** + * 获取i18n资源文件 + * + * @author ruoyi + */ +public class MessageUtils +{ + /** + * 根据消息键和参数 获取消息 委托给spring messageSource + * + * @param code 消息键 + * @param args 参数 + * @return 获取国际化翻译值 + */ + public static String message(String code, Object... args) + { + MessageSource messageSource = SpringUtils.getBean(MessageSource.class); + return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/MoneyUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/MoneyUtil.java new file mode 100644 index 0000000..5eb9769 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/MoneyUtil.java @@ -0,0 +1,11 @@ +package com.ruoyi.common.utils; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class MoneyUtil { + public static String toStr(BigDecimal money) { + if (money == null) return "0.00"; + return money.setScale(2, RoundingMode.HALF_UP).toPlainString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java new file mode 100644 index 0000000..70e9b08 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java @@ -0,0 +1,35 @@ +package com.ruoyi.common.utils; + +import com.github.pagehelper.PageHelper; +import com.ruoyi.common.core.page.PageDomain; +import com.ruoyi.common.core.page.TableSupport; +import com.ruoyi.common.utils.sql.SqlUtil; + +/** + * 分页工具类 + * + * @author ruoyi + */ +public class PageUtils extends PageHelper +{ + /** + * 设置请求分页数据 + */ + public static void startPage() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + Integer pageNum = pageDomain.getPageNum(); + Integer pageSize = pageDomain.getPageSize(); + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + Boolean reasonable = pageDomain.getReasonable(); + PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); + } + + /** + * 清理分页的线程变量 + */ + public static void clearPage() + { + PageHelper.clearPage(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/RandomUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/RandomUtil.java new file mode 100644 index 0000000..199e549 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/RandomUtil.java @@ -0,0 +1,138 @@ +package com.ruoyi.common.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public class RandomUtil { + // 返回[0,1)范围内的一个随机数 + public static double random() { + return ThreadLocalRandom.current().nextDouble(); + } + + // 返回[0,max)范围内的一个整数 + public static int rand(int max) { + int min = 0; + if (min > max) { + throw new IllegalArgumentException("min cannot be greater than max"); + } + if (min == max) { + return min; + } + return ThreadLocalRandom.current().nextInt(min, max); + } + + // 返回[min,max)范围内的一个整数 + public static int rand(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("min cannot be greater than max"); + } + if (min == max) { + return min; + } + return ThreadLocalRandom.current().nextInt(min, max); + } + + // 返回[min,max]范围内的一个整数 + public static int randInt(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("min cannot be greater than max"); + } + if (min == max) { + return min; + } + return ThreadLocalRandom.current().nextInt(min, max + 1); + } + + public static T choice(List collection) { + return choices(collection, 1).get(0); + } + + public static List choices(List list, int k) { + if (list == null || list.isEmpty() || k <= 0) { + return new ArrayList<>(); + } + double[] weights = new double[list.size()]; + for (int i = 0; i < list.size(); i++) { + weights[i] = 1; + } + return choices(list, weights, k); + } + + public static List choices(List list, List weights, int k) { + return choices(list, weights.stream().mapToDouble(Double::doubleValue).toArray(), k); + } + + public static List choices(T[] list, double[] weights, int k) { + return choices(Arrays.asList(list), weights, k); + } + + /** + * 随机获取集合中的元素,支持加权随机 + * + * @param list 候选集合 + * @param weights 对应集合中每个元素的权重数组 + * @param k 选取的元素个数 + * @param 泛型类型 + * @return 包含 k 个随机元素的列表 + */ + public static List choices(List list, double[] weights, int k) { + if (list == null || list.isEmpty()) { + throw new IllegalArgumentException("Collection cannot be null or empty"); + } + if (weights == null || weights.length != list.size()) { + throw new IllegalArgumentException("Weights array cannot be null and must match collection size"); + } + if (k <= 0) { + return new ArrayList<>(); + } + + + List result = new ArrayList<>(k); + + // 1. 构建前缀和数组(累积权重) + // 例如 weights: [1, 2, 3] -> prefixSum: [1, 3, 6] + double[] prefixSum = new double[weights.length]; + prefixSum[0] = weights[0]; + for (int i = 1; i < weights.length; i++) { + prefixSum[i] = prefixSum[i - 1] + weights[i]; + } + double totalWeight = prefixSum[weights.length - 1]; + + if (totalWeight <= 0) { + throw new IllegalArgumentException("Total weight must be positive"); + } + + // 2. 进行 k 次随机选择 + for (int i = 0; i < k; i++) { + // 生成 [0, totalWeight) 之间的随机数 + double r = ThreadLocalRandom.current().nextDouble() * totalWeight; + System.out.println(r); + // 3. 使用二分查找在累积权重数组中找到对应的索引 + int index = binarySearchPrefix(prefixSum, r); + + result.add(list.get(index)); + } + + return result; + } + + /** + * 在前缀和数组中二分查找第一个大于 target 的索引 + */ + private static int binarySearchPrefix(double[] prefixSum, double target) { + int low = 0; + int high = prefixSum.length - 1; + + while (low < high) { + int mid = (low + high) >>> 1; + if (prefixSum[mid] <= target) { + low = mid + 1; + } else { + high = mid; + } + } + return low; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java new file mode 100644 index 0000000..22119c7 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java @@ -0,0 +1,177 @@ +package com.ruoyi.common.utils; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.util.PatternMatchUtils; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.exception.ServiceException; + +/** + * 安全服务工具类 + * + * @author ruoyi + */ +public class SecurityUtils +{ + /** + * 用户ID + **/ + public static Long getUserId() + { + try + { + return getLoginUser().getUserId(); + } + catch (Exception e) + { + throw new ServiceException("获取用户ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取部门ID + **/ + public static Long getDeptId() + { + try + { + return getLoginUser().getDeptId(); + } + catch (Exception e) + { + throw new ServiceException("获取部门ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户账户 + **/ + public static String getUsername() + { + try + { + return getLoginUser().getUsername(); + } + catch (Exception e) + { + throw new ServiceException("获取用户账户异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户 + **/ + public static LoginUser getLoginUser() + { + try + { + return (LoginUser) getAuthentication().getPrincipal(); + } + catch (Exception e) + { + throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取Authentication + */ + public static Authentication getAuthentication() + { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } + + /** + * 判断密码是否相同 + * + * @param rawPassword 真实密码 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword, String encodedPassword) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 是否为管理员 + * + * @param userId 用户ID + * @return 结果 + */ + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } + + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public static boolean hasPermi(String permission) + { + return hasPermi(getLoginUser().getPermissions(), permission); + } + + /** + * 判断是否包含权限 + * + * @param authorities 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public static boolean hasPermi(Collection authorities, String permission) + { + return authorities.stream().filter(StringUtils::hasText) + .anyMatch(x -> Constants.ALL_PERMISSION.contains(x) || PatternMatchUtils.simpleMatch(x, permission)); + } + + /** + * 验证用户是否拥有某个角色 + * + * @param role 角色标识 + * @return 用户是否具备某角色 + */ + public static boolean hasRole(String role) + { + List roleList = getLoginUser().getUser().getRoles(); + Collection roles = roleList.stream().map(SysRole::getRoleKey).collect(Collectors.toSet()); + return hasRole(roles, role); + } + + /** + * 判断是否包含角色 + * + * @param roles 角色列表 + * @param role 角色 + * @return 用户是否具备某角色权限 + */ + public static boolean hasRole(Collection roles, String role) + { + return roles.stream().filter(StringUtils::hasText) + .anyMatch(x -> Constants.SUPER_ADMIN.contains(x) || PatternMatchUtils.simpleMatch(x, role)); + } + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java new file mode 100644 index 0000000..5635db7 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java @@ -0,0 +1,218 @@ +package com.ruoyi.common.utils; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.text.Convert; + +/** + * 客户端工具类 + * + * @author ruoyi + */ +public class ServletUtils +{ + /** + * 获取String参数 + */ + public static String getParameter(String name) + { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) + { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) + { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) + { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name) + { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) + { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParams(ServletRequest request) + { + final Map map = request.getParameterMap(); + return Collections.unmodifiableMap(map); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParamMap(ServletRequest request) + { + Map params = new HashMap<>(); + for (Map.Entry entry : getParams(request).entrySet()) + { + params.put(entry.getKey(), StringUtils.join(entry.getValue(), ",")); + } + return params; + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() + { + return getRequestAttributes().getRequest(); + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() + { + return getRequestAttributes().getResponse(); + } + + /** + * 获取session + */ + public static HttpSession getSession() + { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() + { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) + { + try + { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) + { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) + { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) + { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) + { + return true; + } + + String ajax = request.getParameter("__ajax"); + return StringUtils.inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) + { + try + { + return URLEncoder.encode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) + { + try + { + return URLDecoder.decode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java new file mode 100644 index 0000000..1e52ac2 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java @@ -0,0 +1,666 @@ +package com.ruoyi.common.utils; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.util.AntPathMatcher; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.text.StrFormatter; + +/** + * 字符串工具类 + * + * @author ruoyi + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils +{ + /** 空字符串 */ + private static final String NULLSTR = ""; + + /** 下划线 */ + private static final char SEPARATOR = '_'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) + { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) + { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) + { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + ** @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) + { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) + { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) + { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) + { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) + { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) + { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) + { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) + { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) + { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) + { + return (str == null ? "" : str.trim()); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) + { + if (str == null) + { + return NULLSTR; + } + + if (start < 0) + { + start = str.length() + start; + } + + if (start < 0) + { + start = 0; + } + if (start > str.length()) + { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) + { + if (str == null) + { + return NULLSTR; + } + + if (end < 0) + { + end = str.length() + end; + } + if (start < 0) + { + start = str.length() + start; + } + + if (end > str.length()) + { + end = str.length(); + } + + if (start > end) + { + return NULLSTR; + } + + if (start < 0) + { + start = 0; + } + if (end < 0) + { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 判断是否为空,并且不是空白字符 + * + * @param str 要判断的value + * @return 结果 + */ + public static boolean hasText(String str) + { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) + { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) + { + if (!Character.isWhitespace(str.charAt(i))) + { + return true; + } + } + return false; + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) + { + if (isEmpty(params) || isEmpty(template)) + { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean ishttp(String link) + { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static final Set str2Set(String str, String sep) + { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static final List str2List(String str, String sep, boolean filterBlank, boolean trim) + { + List list = new ArrayList(); + if (StringUtils.isEmpty(str)) + { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtils.isBlank(str)) + { + return list; + } + String[] split = str.split(sep); + for (String string : split) + { + if (filterBlank && StringUtils.isBlank(string)) + { + continue; + } + if (trim) + { + string = string.trim(); + } + list.add(string); + } + + return list; + } + + /** + * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value + * + * @param collection 给定的集合 + * @param array 给定的数组 + * @return boolean 结果 + */ + public static boolean containsAny(Collection collection, String... array) + { + if (isEmpty(collection) || isEmpty(array)) + { + return false; + } + else + { + for (String str : array) + { + if (collection.contains(str)) + { + return true; + } + } + return false; + } + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + * + * @param cs 指定字符串 + * @param searchCharSequences 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + */ + public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) + { + if (isEmpty(cs) || isEmpty(searchCharSequences)) + { + return false; + } + for (CharSequence testStr : searchCharSequences) + { + if (containsIgnoreCase(cs, testStr)) + { + return true; + } + } + return false; + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) + { + if (str == null) + { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) + { + char c = str.charAt(i); + if (i > 0) + { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } + else + { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) + { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) + { + sb.append(SEPARATOR); + } + else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) + { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) + { + if (str != null && strs != null) + { + for (String s : strs) + { + if (str.equalsIgnoreCase(trim(s))) + { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) + { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) + { + // 没必要转换 + return ""; + } + else if (!name.contains("_")) + { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) + { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) + { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 + * 例如:user_name->userName + */ + public static String toCamelCase(String s) + { + if (s == null) + { + return null; + } + if (s.indexOf(SEPARATOR) == -1) + { + return s; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + + if (c == SEPARATOR) + { + upperCase = true; + } + else if (upperCase) + { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } + else + { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) + { + if (isEmpty(str) || isEmpty(strs)) + { + return false; + } + for (String pattern : strs) + { + if (isMatch(pattern, str)) + { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) + { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) + { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static final String padl(final Number num, final int size) + { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static final String padl(final String s, final int size, final char c) + { + final StringBuilder sb = new StringBuilder(size); + if (s != null) + { + final int len = s.length(); + if (s.length() <= size) + { + for (int i = size - len; i > 0; i--) + { + sb.append(c); + } + sb.append(s); + } + else + { + return s.substring(len - size, len); + } + } + else + { + for (int i = size; i > 0; i--) + { + sb.append(c); + } + } + return sb.toString(); + } + + public static Long convertToLong(String str) { + try { + // 获取字符串的字节数组 + byte[] bytes = str.getBytes(); + + // 使用 MD5 获取摘要 + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] hash = md.digest(bytes); + + // 将字节数组转换为 BigInteger + BigInteger bigInteger = new BigInteger(1, hash); + + // 获取 BigInteger 的绝对值并转换为 Long + Long result = bigInteger.abs().longValue(); + + // 确保返回值是非负数 + return result >= 0 ? result : -result; + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return -1L; + } + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java new file mode 100644 index 0000000..71fe6d5 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java @@ -0,0 +1,99 @@ +package com.ruoyi.common.utils; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 线程相关工具类. + * + * @author ruoyi + */ +public class Threads +{ + private static final Logger logger = LoggerFactory.getLogger(Threads.class); + + /** + * sleep等待,单位为毫秒 + */ + public static void sleep(long milliseconds) + { + try + { + Thread.sleep(milliseconds); + } + catch (InterruptedException e) + { + return; + } + } + + /** + * 停止线程池 + * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. + * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. + * 如果仍然超時,則強制退出. + * 另对在shutdown时线程本身被调用中断做了处理. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool) + { + if (pool != null && !pool.isShutdown()) + { + pool.shutdown(); + try + { + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + pool.shutdownNow(); + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + logger.info("Pool did not terminate"); + } + } + } + catch (InterruptedException ie) + { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** + * 打印线程异常信息 + */ + public static void printException(Runnable r, Throwable t) + { + if (t == null && r instanceof Future) + { + try + { + Future future = (Future) r; + if (future.isDone()) + { + future.get(); + } + } + catch (CancellationException ce) + { + t = ce; + } + catch (ExecutionException ee) + { + t = ee.getCause(); + } + catch (InterruptedException ie) + { + Thread.currentThread().interrupt(); + } + } + if (t != null) + { + logger.error(t.getMessage(), t); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java new file mode 100644 index 0000000..4463662 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java @@ -0,0 +1,110 @@ +package com.ruoyi.common.utils.bean; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Bean 工具类 + * + * @author ruoyi + */ +public class BeanUtils extends org.springframework.beans.BeanUtils +{ + /** Bean方法名中属性名开始的下标 */ + private static final int BEAN_METHOD_PROP_INDEX = 3; + + /** * 匹配getter方法的正则表达式 */ + private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)"); + + /** * 匹配setter方法的正则表达式 */ + private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)"); + + /** + * Bean属性复制工具方法。 + * + * @param dest 目标对象 + * @param src 源对象 + */ + public static void copyBeanProp(Object dest, Object src) + { + try + { + copyProperties(src, dest); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * 获取对象的setter方法。 + * + * @param obj 对象 + * @return 对象的setter方法列表 + */ + public static List getSetterMethods(Object obj) + { + // setter方法列表 + List setterMethods = new ArrayList(); + + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + + // 查找setter方法 + + for (Method method : methods) + { + Matcher m = SET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 1)) + { + setterMethods.add(method); + } + } + // 返回setter方法列表 + return setterMethods; + } + + /** + * 获取对象的getter方法。 + * + * @param obj 对象 + * @return 对象的getter方法列表 + */ + + public static List getGetterMethods(Object obj) + { + // getter方法列表 + List getterMethods = new ArrayList(); + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + // 查找getter方法 + for (Method method : methods) + { + Matcher m = GET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 0)) + { + getterMethods.add(method); + } + } + // 返回getter方法列表 + return getterMethods; + } + + /** + * 检查Bean方法名中的属性名是否相等。
+ * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。 + * + * @param m1 方法名1 + * @param m2 方法名2 + * @return 属性名一样返回true,否则返回false + */ + + public static boolean isMethodPropEquals(String m1, String m2) + { + return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX)); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java new file mode 100644 index 0000000..898aca5 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.utils.bean; + +import java.util.Set; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + +/** + * bean对象属性验证 + * + * @author ruoyi + */ +public class BeanValidators +{ + public static void validateWithException(Validator validator, Object object, Class... groups) + throws ConstraintViolationException + { + Set> constraintViolations = validator.validate(object, groups); + if (!constraintViolations.isEmpty()) + { + throw new ConstraintViolationException(constraintViolations); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/EntityFieldMapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/EntityFieldMapper.java new file mode 100644 index 0000000..954e310 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/EntityFieldMapper.java @@ -0,0 +1,108 @@ +package com.ruoyi.common.utils.bean; + +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.ruoyi.common.core.domain.EntityChange; + +/** Explicit user audit allowlist: secrets and technical metadata never enter the snapshot. */ +public class EntityFieldMapper +{ + private static final Map LABELS = new LinkedHashMap<>(); + static + { + LABELS.put("accountAmount", "账户余额"); + LABELS.put("accountCredits", "账户积分"); + LABELS.put("merchantBalance", "卡商余额"); + LABELS.put("nickName", "用户昵称"); + LABELS.put("userName", "用户账号"); + LABELS.put("userType", "用户类型"); + LABELS.put("email", "邮箱"); + LABELS.put("phoneNumber", "手机号码"); + LABELS.put("invitationCode", "邀请码"); + LABELS.put("parentId", "上级用户ID"); + LABELS.put("vipLevel", "VIP等级"); + LABELS.put("promotionLevel", "推广等级"); + LABELS.put("totalRechargeAmount", "累计充值金额"); + LABELS.put("status", "账号状态"); + LABELS.put("deliveryStatus", "发货状态"); + LABELS.put("giftStatus", "礼包状态"); + LABELS.put("isRealCheck", "实名认证状态"); + LABELS.put("isBlankAccount", "是否白号"); + LABELS.put("commissionRate", "佣金比例"); + LABELS.put("newGameCommissionRate", "新游戏佣金比例"); + LABELS.put("classicGameCommissionRate", "经典游戏佣金比例"); + } + + public static Map snapshot(Object entity) + { + Map values = new LinkedHashMap<>(); + for (String name : LABELS.keySet()) + { + Field field = field(entity.getClass(), name); + if (field == null) continue; + try + { + field.setAccessible(true); + Object value = field.get(entity); + values.put(name, value == null ? null : value instanceof BigDecimal + ? ((BigDecimal) value).stripTrailingZeros().toPlainString() : String.valueOf(value)); + } + catch (IllegalAccessException ex) { throw new IllegalStateException("Audit field inaccessible", ex); } + } + return values; + } + + private static Field field(Class type, String name) + { + for (Class c = type; c != null; c = c.getSuperclass()) + { + try { return c.getDeclaredField(name); } + catch (NoSuchFieldException ignored) { } + } + return null; + } + + public static List changes(Map before, Map after, Class type) + { + List changes = new ArrayList<>(); + for (String name : before.keySet()) + { + if (!after.containsKey(name) || java.util.Objects.equals(before.get(name), after.get(name))) continue; + changes.add(new EntityChange(name, LABELS.get(name), display(name, before.get(name)), display(name, after.get(name)))); + } + return changes; + } + + private static String display(String field, String value) + { + if (value == null || value.isEmpty()) return "空"; + if ("status".equals(field)) + { + if ("0".equals(value)) return "正常"; + if ("1".equals(value)) return "停用"; + } + if ("isBlankAccount".equals(field)) + { + if ("0".equals(value)) return "否"; + if ("1".equals(value)) return "是"; + } + return value; + } + + public static List map(Object before, Object after) + { + if (before == null || after == null || before.getClass() != after.getClass()) return new ArrayList<>(); + return changes(snapshot(before), snapshot(after), before.getClass()); + } + + public static String toText(List changes) + { + List lines = new ArrayList<>(); + if (changes != null) for (EntityChange change : changes) lines.add(change.toLine()); + return String.join(";", lines); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ApiStringUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ApiStringUtils.java new file mode 100644 index 0000000..4fefad0 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ApiStringUtils.java @@ -0,0 +1,32 @@ +package com.ruoyi.common.utils.file; + +import com.ruoyi.common.config.RuoYiConfig; + +import java.io.File; + +public class ApiStringUtils { + + public static int findNthMatchIndex(String str, String matchStr, int n) { + return findNthMatchIndexHelper(str, matchStr, n, 0); + } + + private static int findNthMatchIndexHelper(String str, String matchStr, int n, int startIndex) { + int index = str.indexOf(matchStr, startIndex); + if (index == -1) { + return -1; + } + if (n == 1) { + return index; + } + return findNthMatchIndexHelper(str, matchStr, n - 1, index + matchStr.length()); + } + + public static String delAvatar(String filePath) { + String domainName = RuoYiConfig.getDomainName(); + int numberOfSlashes = domainName.length() - domainName.replaceAll("/", "").length(); + int nthMatchIndex = ApiStringUtils.findNthMatchIndex(filePath, "/", numberOfSlashes + 2); + File delFile = new File(RuoYiConfig.getProfile() + filePath.substring(nthMatchIndex)); + if (delFile.delete()) return "删除成功!"; + return ""; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java new file mode 100644 index 0000000..68130b9 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java @@ -0,0 +1,76 @@ +package com.ruoyi.common.utils.file; + +import java.io.File; +import org.apache.commons.lang3.StringUtils; + +/** + * 文件类型工具类 + * + * @author ruoyi + */ +public class FileTypeUtils +{ + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param file 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(File file) + { + if (null == file) + { + return StringUtils.EMPTY; + } + return getFileType(file.getName()); + } + + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) + { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) + { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "GIF"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "JPG"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "BMP"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java new file mode 100644 index 0000000..d9f2b13 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java @@ -0,0 +1,232 @@ +package com.ruoyi.common.utils.file; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Objects; +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.exception.file.FileNameLengthLimitExceededException; +import com.ruoyi.common.exception.file.FileSizeLimitExceededException; +import com.ruoyi.common.exception.file.InvalidExtensionException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.Seq; + +/** + * 文件上传工具类 + * + * @author ruoyi + */ +public class FileUploadUtils +{ + /** + * 默认大小 50M + */ + public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024; + + /** + * 默认的文件名最大长度 100 + */ + public static final int DEFAULT_FILE_NAME_LENGTH = 100; + + /** + * 默认上传的地址 + */ + private static String defaultBaseDir = RuoYiConfig.getProfile(); + + public static void setDefaultBaseDir(String defaultBaseDir) + { + FileUploadUtils.defaultBaseDir = defaultBaseDir; + } + + public static String getDefaultBaseDir() + { + return defaultBaseDir; + } + + /** + * 以默认配置进行文件上传 + * + * @param file 上传的文件 + * @return 文件名称 + * @throws Exception + */ + public static final String upload(MultipartFile file) throws IOException + { + try + { + return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 根据文件路径上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @return 文件名称 + * @throws IOException + */ + public static final String upload(String baseDir, MultipartFile file) throws IOException + { + try + { + return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException + { + int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length(); + if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) + { + throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); + } + + assertAllowed(file, allowedExtension); + + String fileName = extractFilename(file); + + String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath(); + file.transferTo(Paths.get(absPath)); + return getPathFileName(baseDir, fileName); + } + + /** + * 编码文件名 + */ + public static final String extractFilename(MultipartFile file) + { + return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(), + FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file)); + } + + public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException + { + File desc = new File(uploadDir + File.separator + fileName); + + if (!desc.exists()) + { + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + } + return desc; + } + + public static final String getPathFileName(String uploadDir, String fileName) throws IOException + { + int dirLastIndex = RuoYiConfig.getProfile().length() + 1; + String currentDir = StringUtils.substring(uploadDir, dirLastIndex); + return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; + } + + /** + * 文件大小校验 + * + * @param file 上传的文件 + * @return + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws InvalidExtensionException + */ + public static final void assertAllowed(MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, InvalidExtensionException + { + long size = file.getSize(); + if (size > DEFAULT_MAX_SIZE) + { + throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); + } + + String fileName = file.getOriginalFilename(); + String extension = getExtension(file); + if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) + { + if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) + { + throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) + { + throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) + { + throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) + { + throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, + fileName); + } + else + { + throw new InvalidExtensionException(allowedExtension, extension, fileName); + } + } + } + + /** + * 判断MIME类型是否是允许的MIME类型 + * + * @param extension + * @param allowedExtension + * @return + */ + public static final boolean isAllowedExtension(String extension, String[] allowedExtension) + { + for (String str : allowedExtension) + { + if (str.equalsIgnoreCase(extension)) + { + return true; + } + } + return false; + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) + { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (StringUtils.isEmpty(extension)) + { + extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType())); + } + return extension; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java new file mode 100644 index 0000000..2062b75 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java @@ -0,0 +1,291 @@ +package com.ruoyi.common.utils.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import org.apache.commons.io.FilenameUtils; + +/** + * 文件处理工具类 + * + * @author ruoyi + */ +public class FileUtils +{ + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException + { + FileInputStream fis = null; + try + { + File file = new File(filePath); + if (!file.exists()) + { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) + { + os.write(b, 0, length); + } + } + catch (IOException e) + { + throw e; + } + finally + { + IOUtils.close(os); + IOUtils.close(fis); + } + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeImportBytes(byte[] data) throws IOException + { + return writeBytes(data, RuoYiConfig.getImportPath()); + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @param uploadDir 目标文件 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeBytes(byte[] data, String uploadDir) throws IOException + { + FileOutputStream fos = null; + String pathName = ""; + try + { + String extension = getFileExtendName(data); + pathName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension; + File file = FileUploadUtils.getAbsoluteFile(uploadDir, pathName); + fos = new FileOutputStream(file); + fos.write(data); + } + finally + { + IOUtils.close(fos); + } + return FileUploadUtils.getPathFileName(uploadDir, pathName); + } + + /** + * 删除文件 + * + * @param filePath 文件 + * @return + */ + public static boolean deleteFile(String filePath) + { + boolean flag = false; + File file = new File(filePath); + // 路径为文件且不为空则进行删除 + if (file.isFile() && file.exists()) + { + flag = file.delete(); + } + return flag; + } + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) + { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 检查文件是否可下载 + * + * @param resource 需要下载的文件 + * @return true 正常 false 非法 + */ + public static boolean checkAllowDownload(String resource) + { + // 禁止目录上跳级别 + if (StringUtils.contains(resource, "..")) + { + return false; + } + + // 检查允许下载的文件规则 + if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) + { + return true; + } + + // 不在允许下载的文件规则 + return false; + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException + { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) + { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } + else if (agent.contains("Firefox")) + { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } + else if (agent.contains("Chrome")) + { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + else + { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException + { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename"); + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException + { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } + + /** + * 获取图像后缀 + * + * @param photoByte 图像数据 + * @return 后缀名 + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "jpg"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "gif"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "jpg"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "bmp"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "png"; + } + return strFileExtendName; + } + + /** + * 获取文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi.png + * + * @param fileName 路径名称 + * @return 没有文件路径的名称 + */ + public static String getName(String fileName) + { + if (fileName == null) + { + return null; + } + int lastUnixPos = fileName.lastIndexOf('/'); + int lastWindowsPos = fileName.lastIndexOf('\\'); + int index = Math.max(lastUnixPos, lastWindowsPos); + return fileName.substring(index + 1); + } + + /** + * 获取不带后缀文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi + * + * @param fileName 路径名称 + * @return 没有文件路径和后缀的名称 + */ + public static String getNameNotSuffix(String fileName) + { + if (fileName == null) + { + return null; + } + String baseName = FilenameUtils.getBaseName(fileName); + return baseName; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java new file mode 100644 index 0000000..432dfda --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java @@ -0,0 +1,98 @@ +package com.ruoyi.common.utils.file; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import org.apache.poi.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; + +/** + * 图片处理工具类 + * + * @author ruoyi + */ +public class ImageUtils +{ + private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); + + public static byte[] getImage(String imagePath) + { + InputStream is = getFile(imagePath); + try + { + return IOUtils.toByteArray(is); + } + catch (Exception e) + { + log.error("图片加载异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(is); + } + } + + public static InputStream getFile(String imagePath) + { + try + { + byte[] result = readFile(imagePath); + result = Arrays.copyOf(result, result.length); + return new ByteArrayInputStream(result); + } + catch (Exception e) + { + log.error("获取图片异常 {}", e); + } + return null; + } + + /** + * 读取文件为字节数据 + * + * @param url 地址 + * @return 字节数据 + */ + public static byte[] readFile(String url) + { + InputStream in = null; + try + { + if (url.startsWith("http")) + { + // 网络地址 + URL urlObj = new URL(url); + URLConnection urlConnection = urlObj.openConnection(); + urlConnection.setConnectTimeout(30 * 1000); + urlConnection.setReadTimeout(60 * 1000); + urlConnection.setDoInput(true); + in = urlConnection.getInputStream(); + } + else + { + // 本机地址 + String localPath = RuoYiConfig.getProfile(); + String downloadPath = localPath + StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX); + in = new FileInputStream(downloadPath); + } + return IOUtils.toByteArray(in); + } + catch (Exception e) + { + log.error("获取文件路径异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(in); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java new file mode 100644 index 0000000..f968f1a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.utils.file; + +/** + * 媒体类型工具类 + * + * @author ruoyi + */ +public class MimeTypeUtils +{ + public static final String IMAGE_PNG = "image/png"; + + public static final String IMAGE_JPG = "image/jpg"; + + public static final String IMAGE_JPEG = "image/jpeg"; + + public static final String IMAGE_BMP = "image/bmp"; + + public static final String IMAGE_GIF = "image/gif"; + + public static final String[] IMAGE_EXTENSION = { "bmp", "gif", "jpg", "jpeg", "png" }; + + public static final String[] FLASH_EXTENSION = { "swf", "flv" }; + + public static final String[] MEDIA_EXTENSION = { "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", + "asf", "rm", "rmvb" }; + + public static final String[] VIDEO_EXTENSION = { "mp4", "avi", "rmvb" }; + + public static final String[] DEFAULT_ALLOWED_EXTENSION = { + // 图片 + "bmp", "gif", "jpg", "jpeg", "png", + // word excel powerpoint + "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", + // 压缩文件 + "rar", "zip", "gz", "bz2", + // 视频格式 + "mp4", "avi", "rmvb", + // pdf + "pdf" }; + + public static String getExtension(String prefix) + { + switch (prefix) + { + case IMAGE_PNG: + return "png"; + case IMAGE_JPG: + return "jpg"; + case IMAGE_JPEG: + return "jpeg"; + case IMAGE_BMP: + return "bmp"; + case IMAGE_GIF: + return "gif"; + default: + return ""; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java new file mode 100644 index 0000000..f52e83e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java @@ -0,0 +1,167 @@ +package com.ruoyi.common.utils.html; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 转义和反转义工具类 + * + * @author ruoyi + */ +public class EscapeUtil +{ + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + + private static final char[][] TEXT = new char[64][]; + + static + { + for (int i = 0; i < 64; i++) + { + TEXT[i] = new char[] { (char) i }; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 + TEXT['"'] = """.toCharArray(); // 双引号 + TEXT['&'] = "&".toCharArray(); // &符 + TEXT['<'] = "<".toCharArray(); // 小于号 + TEXT['>'] = ">".toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符 + * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) + { + return encode(text); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param content 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String content) + { + return decode(content); + } + + /** + * 清除所有HTML标签,但是不删除标签内的内容 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String clean(String content) + { + return new HTMLFilter().filter(content); + } + + /** + * Escape编码 + * + * @param text 被编码的文本 + * @return 编码后的字符 + */ + private static String encode(String text) + { + if (StringUtils.isEmpty(text)) + { + return StringUtils.EMPTY; + } + + final StringBuilder tmp = new StringBuilder(text.length() * 6); + char c; + for (int i = 0; i < text.length(); i++) + { + c = text.charAt(i); + if (c < 256) + { + tmp.append("%"); + if (c < 16) + { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + else + { + tmp.append("%u"); + if (c <= 0xfff) + { + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String decode(String content) + { + if (StringUtils.isEmpty(content)) + { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0, pos = 0; + char ch; + while (lastPos < content.length()) + { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) + { + if (content.charAt(pos + 1) == 'u') + { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } + else + { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } + else + { + if (pos == -1) + { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } + else + { + tmp.append(content.substring(lastPos, pos)); + lastPos = pos; + } + } + } + return tmp.toString(); + } + + public static void main(String[] args) + { + String html = ""; + String escape = EscapeUtil.escape(html); + // String html = "ipt>alert(\"XSS\")ipt>"; + // String html = "<123"; + // String html = "123>"; + System.out.println("clean: " + EscapeUtil.clean(html)); + System.out.println("escape: " + escape); + System.out.println("unescape: " + EscapeUtil.unescape(escape)); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java new file mode 100644 index 0000000..ebff3fd --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java @@ -0,0 +1,570 @@ +package com.ruoyi.common.utils.html; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML过滤器,用于去除XSS漏洞隐患。 + * + * @author ruoyi + */ +public final class HTMLFilter +{ + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() + { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[] { "img" }; + vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" }; + vDisallowed = new String[] {}; + vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp. + vProtocolAtts = new String[] { "src", "href" }; + vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" }; + vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" }; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) + { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() + { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) + { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) + { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) + { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + // s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() + { + return alwaysMakeTags; + } + + public boolean isStripComments() + { + return stripComment; + } + + private String escapeComments(final String s) + { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) + { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) + { + if (alwaysMakeTags) + { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + // 不追加结束标签 + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } + else + { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) + { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) + { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) + { + for (int ii = 0; ii < vTagCounts.get(key); ii++) + { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) + { + String result = s; + for (String tag : vRemoveBlanks) + { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) + { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) + { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) + { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) + { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) + { + if (!inArray(name, vSelfClosingTags)) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) + { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) + { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) + { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) + { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) + { + if (inArray(paramName, vProtocolAtts)) + { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\""); + } + } + + if (inArray(name, vSelfClosingTags)) + { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) + { + ending = ""; + } + + if (ending == null || ending.length() < 1) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } + else + { + vTagCounts.put(name, 1); + } + } + else + { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } + else + { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) + { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) + { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) + { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) + { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) + { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) + { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) + { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) + { + if (encodeQuotes) + { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } + else + { + return s; + } + } + + private String checkEntity(final String preamble, final String term) + { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) + { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) + { + for (String item : array) + { + if (item != null && item.equals(s)) + { + return true; + } + } + return false; + } + + private boolean allowed(final String name) + { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) + { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java new file mode 100644 index 0000000..401f25a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java @@ -0,0 +1,55 @@ +package com.ruoyi.common.utils.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import jakarta.servlet.ServletRequest; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 通用http工具封装 + * + * @author ruoyi + */ +public class HttpHelper +{ + private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class); + + public static String getBodyString(ServletRequest request) + { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + try (InputStream inputStream = request.getInputStream()) + { + reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = ""; + while ((line = reader.readLine()) != null) + { + sb.append(line); + } + } + catch (IOException e) + { + LOGGER.warn("getBodyString出现问题!"); + } + finally + { + if (reader != null) + { + try + { + reader.close(); + } + catch (IOException e) + { + LOGGER.error(ExceptionUtils.getMessage(e)); + } + } + } + return sb.toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java new file mode 100644 index 0000000..02bd060 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java @@ -0,0 +1,272 @@ +package com.ruoyi.common.utils.http; + +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.*; +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.Map; + +public class HttpUtils { + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + public static String sendGet(String url) { + return sendGet(url, StringUtils.EMPTY); + } + + public static String sendGet(String url, String param) { + return sendGet(url, param, Constants.UTF8); + } + + public static String sendGet(String url, String param, String contentType) { + StringBuilder result = new StringBuilder(); + BufferedReader in = null; + try { + String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url; + //log.info("sendGet - {}", urlNameString); + URL realUrl = new URL(urlNameString); + URLConnection connection = realUrl.openConnection(); + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + connection.connect(); + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType)); + String line; + while ((line = in.readLine()) != null) { + result.append(line); + } + //log.info("recv - {}", result); + } catch (ConnectException e) { + log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e); + } finally { + try { + if (in != null) { + in.close(); + } + } catch (Exception ex) { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + public static String sendPost(String url, String param) { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try { + // log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("contentType", "utf-8"); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) { + result.append(line); + } + } catch (ConnectException e) { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (IOException ex) { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + public static String sendPostJSONString(String url, String JSONString) { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try { + // log.info("sendPostJSONString - {}", url); + URL realUrl = new URL(url); + HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(JSONString); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) { + result.append(line); + } + } catch (ConnectException e) { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",JSONString=" + JSONString, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",JSONString=" + JSONString, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",JSONString=" + JSONString, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",JSONString=" + JSONString, e); + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (IOException ex) { + log.error("调用in.close Exception, url=" + url + ",JSONString=" + JSONString, ex); + } + } + return result.toString(); + } + + public static String sendPostJSONString(String url, String jsonString, Map headers) { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try { + URL realUrl = new URL(url); + HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); + + // 设置请求方法 + conn.setRequestMethod("POST"); + + // 设置默认请求头 + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Content-Type", "application/json"); + + // 添加自定义请求头 + for (Map.Entry entry : headers.entrySet()) { + conn.setRequestProperty(entry.getKey(), entry.getValue()); + } + + // 设置连接属性 + conn.setDoOutput(true); + conn.setDoInput(true); + + out = new PrintWriter(new OutputStreamWriter(conn.getOutputStream())); + out.print(jsonString); + out.flush(); + + in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String line; + while ((line = in.readLine()) != null) { + result.append(line); + } + } catch (IOException e) { + log.error("调用HttpUtils.sendPostJSONString IOException, url=" + url + ",jsonString=" + jsonString, e); + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (IOException ex) { + log.error("调用in.close Exception, url=" + url + ",jsonString=" + jsonString, ex); + } + } + return result.toString(); + } + + public static String sendSSLPost(String url, String param) { + StringBuilder result = new StringBuilder(); + String urlNameString = url + "?" + param; + try { + log.info("sendSSLPost - {}", urlNameString); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom()); + URL console = new URL(urlNameString); + HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("contentType", "utf-8"); + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setSSLSocketFactory(sc.getSocketFactory()); + conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); + conn.connect(); + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String ret = ""; + while ((ret = br.readLine()) != null) { + if (!ret.trim().isEmpty()) { + result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } + } + conn.disconnect(); + br.close(); + } catch (ConnectException e) { + log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e); + } + return result.toString(); + } + + private static class TrustAnyTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[]{}; + } + } + + private static class TrustAnyHostnameVerifier implements HostnameVerifier { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java new file mode 100644 index 0000000..edfe419 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java @@ -0,0 +1,56 @@ +package com.ruoyi.common.utils.ip; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpUtils; + +/** + * 获取地址类 + * + * @author ruoyi + */ +public class AddressUtils +{ + private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); + + // IP地址查询 + public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp"; + + // 未知地址 + public static final String UNKNOWN = "XX XX"; + + public static String getRealAddressByIP(String ip) + { + // 内网不查询 + if (IpUtils.internalIp(ip)) + { + return "内网IP"; + } + if (RuoYiConfig.isAddressEnabled()) + { + try + { + String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", Constants.GBK); + if (StringUtils.isEmpty(rspStr)) + { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + JSONObject obj = JSON.parseObject(rspStr); + String region = obj.getString("pro"); + String city = obj.getString("city"); + return String.format("%s %s", region, city); + } + catch (Exception e) + { + log.error("获取地理位置异常 {}", ip); + } + } + return UNKNOWN; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java new file mode 100644 index 0000000..a376416 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java @@ -0,0 +1,382 @@ +package com.ruoyi.common.utils.ip; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import jakarta.servlet.http.HttpServletRequest; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 获取IP方法 + * + * @author ruoyi + */ +public class IpUtils +{ + public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)"; + // 匹配 ip + public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")"; + public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))"; + // 匹配网段 + public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")"; + + /** + * 获取客户端IP + * + * @return IP地址 + */ + public static String getIpAddr() + { + return getIpAddr(ServletUtils.getRequest()); + } + + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) + { + if (request == null) + { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) + { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) + { + if (StringUtils.isNull(addr) || addr.length < 2) + { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) + { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) + { + return true; + } + case SECTION_5: + switch (b1) + { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) + { + if (text.length() == 0) + { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try + { + long l; + int i; + switch (elements.length) + { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) + { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) + { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) + { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } + catch (NumberFormatException e) + { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() + { + try + { + return InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) + { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException e) + { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) + { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) + { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) + { + if (false == isUnknown(subIp)) + { + ip = subIp; + break; + } + } + } + return StringUtils.substring(ip, 0, 255); + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) + { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + /** + * 是否为IP + */ + public static boolean isIP(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP); + } + + /** + * 是否为IP,或 *为间隔的通配符地址 + */ + public static boolean isIpWildCard(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD); + } + + /** + * 检测参数是否在ip通配符里 + */ + public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip) + { + String[] s1 = ipWildCard.split("\\."); + String[] s2 = ip.split("\\."); + boolean isMatchedSeg = true; + for (int i = 0; i < s1.length && !s1[i].equals("*"); i++) + { + if (!s1[i].equals(s2[i])) + { + isMatchedSeg = false; + break; + } + } + return isMatchedSeg; + } + + /** + * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串 + */ + public static boolean isIPSegment(String ipSeg) + { + return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG); + } + + /** + * 判断ip是否在指定网段中 + */ + public static boolean ipIsInNetNoCheck(String iparea, String ip) + { + int idx = iparea.indexOf('-'); + String[] sips = iparea.substring(0, idx).split("\\."); + String[] sipe = iparea.substring(idx + 1).split("\\."); + String[] sipt = ip.split("\\."); + long ips = 0L, ipe = 0L, ipt = 0L; + for (int i = 0; i < 4; ++i) + { + ips = ips << 8 | Integer.parseInt(sips[i]); + ipe = ipe << 8 | Integer.parseInt(sipe[i]); + ipt = ipt << 8 | Integer.parseInt(sipt[i]); + } + if (ips > ipe) + { + long t = ips; + ips = ipe; + ipe = t; + } + return ips <= ipt && ipt <= ipe; + } + + /** + * 校验ip是否符合过滤串规则 + * + * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99` + * @param ip 校验IP地址 + * @return boolean 结果 + */ + public static boolean isMatchedIp(String filter, String ip) + { + if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip)) + { + return false; + } + String[] ips = filter.split(";"); + for (String iStr : ips) + { + if (isIP(iStr) && iStr.equals(ip)) + { + return true; + } + else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip)) + { + return true; + } + else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip)) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/json/JsonUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/json/JsonUtils.java new file mode 100644 index 0000000..479e5ab --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/json/JsonUtils.java @@ -0,0 +1,11 @@ +package com.ruoyi.common.utils.json; + +public class JsonUtils { + public static T fromSnakeCaseJson(String json, Class clazz) { + return SnakeCaseJsonUtils.fromSnakeCaseJson(json, clazz); + } + + public static String toSnakeCaseJson(Object obj) { + return SnakeCaseJsonUtils.toSnakeCaseJson(obj); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/json/SnakeCaseJsonUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/json/SnakeCaseJsonUtils.java new file mode 100644 index 0000000..edad410 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/json/SnakeCaseJsonUtils.java @@ -0,0 +1,247 @@ +package com.ruoyi.common.utils.json; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.NameFilter; + +import java.lang.reflect.Type; +import java.util.*; + +/** + * 基于 FastJSON2 的通用蛇形命名 JSON 工具类 + * 特性:性能更高、安全性更好、API 更现代 + * + * 使用方式: + * // 标准蛇形 JSON + * String snakeJson = SnakeCaseJsonUtils.toSnakeCaseJson(user); + * // 格式化输出 + * String pretty = SnakeCaseJsonUtils2.toSnakeCaseJsonPretty(user); + * + * // 自动映射蛇形到驼峰 + * UserProfile parsed = SnakeCaseJsonUtils.fromSnakeCaseJson(inputJson, UserProfile.class); + * // 泛型集合 + * List userList = SnakeCaseJsonUtils2.parseSnakeCaseList(jsonArrayString, UserProfile.class); + * + * // ============ Map 转换示例 ============ + * Map snakeMap = SnakeCaseJsonUtils2.toSnakeCaseMap(user); + * + * UserProfile fromMap = SnakeCaseJsonUtils2.fromSnakeCaseMap(snakeMap, UserProfile.class); + */ +public class SnakeCaseJsonUtils { + + // FastJSON2 内置的命名策略过滤器(性能最优) + private static final NameFilter SNAKE_CASE_FILTER = (object, name, value) -> + camelToSnake(name); + + // 序列化特性:使用蛇形命名 + 标准特性 + private static final JSONWriter.Feature[] SERIALIZE_FEATURES = { + JSONWriter.Feature.WriteMapNullValue, // 输出 null 值 + JSONWriter.Feature.WriteLongAsString, // Long 转为 String(避免前端精度丢失) + JSONWriter.Feature.WriteEnumsUsingName // 枚举使用 name + }; + + // 反序列化特性:支持蛇形命名自动映射 + private static final JSONReader.Feature[] DESERIALIZE_FEATURES = { + JSONReader.Feature.SupportSmartMatch // 智能匹配(自动处理命名差异) + }; + + // ==================== 核心序列化方法 ==================== + + /** + * 将对象序列化为蛇形命名的 JSON 字符串 + * 使用 FastJSON2 的 NameFilter 实现高性能转换 + */ + public static String toSnakeCaseJson(Object obj) { + if (obj == null) { + return "{}"; + } + return JSON.toJSONString(obj, SNAKE_CASE_FILTER, SERIALIZE_FEATURES); + } + + /** + * 格式化输出(Pretty Format) + */ + public static String toSnakeCaseJsonPretty(Object obj) { + if (obj == null) { + return "{}"; + } + return JSON.toJSONString(obj, SNAKE_CASE_FILTER, + JSONWriter.Feature.PrettyFormat, + JSONWriter.Feature.WriteMapNullValue + ); + } + + /** + * 序列化为字节数组(使用 JSONB 格式,性能更高) + */ + public static byte[] toSnakeCaseJsonb(Object obj) { + if (obj == null) { + return new byte[0]; + } + // JSONB 也支持 NameFilter + return JSON.toJSONBytes(obj, SNAKE_CASE_FILTER); + } + + // ==================== 核心反序列化方法 ==================== + + /** + * 将蛇形命名的 JSON 字符串反序列化为指定类型的对象 + * FastJSON2 的 SupportSmartMatch 自动处理命名映射 + */ + public static T fromSnakeCaseJson(String json, Class clazz) { + if (json == null || json.trim().isEmpty()) { + return null; + } + // SupportSmartMatch 特性会自动将 snake_case 映射到 camelCase + return JSON.parseObject(json, clazz, DESERIALIZE_FEATURES); + } + + /** + * 支持泛型的反序列化(如 List、Map 等) + */ + public static T fromSnakeCaseJson(String json, Type type) { + if (json == null || json.trim().isEmpty()) { + return null; + } + return JSON.parseObject(json, type, DESERIALIZE_FEATURES); + } + + /** + * 从 JSONB 字节数组反序列化 + */ + public static T fromSnakeCaseJsonb(byte[] jsonbBytes, Class clazz) { + if (jsonbBytes == null || jsonbBytes.length == 0) { + return null; + } + return JSON.parseObject(jsonbBytes, clazz, DESERIALIZE_FEATURES); + } + + // ==================== Map/JSONObject 转换 ==================== + + /** + * 将对象转换为蛇形命名的 JSONObject + */ + public static JSONObject toSnakeCaseJSONObject(Object obj) { + if (obj == null) { + return new JSONObject(); + } + String json = toSnakeCaseJson(obj); + return JSON.parseObject(json); + } + + /** + * 将对象转换为蛇形命名的 Map + */ + public static Map toSnakeCaseMap(Object obj) { + if (obj == null) { + return new HashMap<>(); + } + JSONObject jsonObject = toSnakeCaseJSONObject(obj); + return jsonObject.toJavaObject(Map.class); + } + + /** + * 将蛇形命名的 Map 转换为指定类型的对象 + */ + public static T fromSnakeCaseMap(Map map, Class clazz) { + if (map == null) { + return null; + } + // FastJSON2 的 toJSONString 直接支持 Map + String json = JSON.toJSONString(map); + return fromSnakeCaseJson(json, clazz); + } + + // ==================== 数组/集合处理 ==================== + + /** + * 将 List 序列化为蛇形命名的 JSON 数组字符串 + */ + public static String toSnakeCaseJsonList(List list) { + if (list == null) { + return "[]"; + } + return JSON.toJSONString(list, SNAKE_CASE_FILTER, SERIALIZE_FEATURES); + } + + /** + * 将蛇形命名的 JSON 数组反序列化为 List + */ + public static List parseSnakeCaseList(String json, Class clazz) { + if (json == null || json.trim().isEmpty()) { + return new ArrayList<>(); + } + return JSON.parseArray(json, clazz, DESERIALIZE_FEATURES); + } + + /** + * 将蛇形命名的 JSON 数组反序列化为 JSONArray + */ + public static JSONArray parseSnakeCaseArray(String json) { + if (json == null || json.trim().isEmpty()) { + return new JSONArray(); + } + return JSON.parseArray(json); + } + + // ==================== 命名转换工具方法 ==================== + + /** + * 将 camelCase 转换为 snake_case + * 优化版:处理连续大写字母(如 HTTPRequest -> http_request) + */ + public static String camelToSnake(String camel) { + if (camel == null || camel.isEmpty()) { + return camel; + } + + StringBuilder snake = new StringBuilder(camel.length() + 10); + char[] chars = camel.toCharArray(); + + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + + if (Character.isUpperCase(c)) { + // 处理首字母大写或连续大写的情况 + if (i > 0 && + (Character.isLowerCase(chars[i - 1]) || + (i + 1 < chars.length && Character.isLowerCase(chars[i + 1])))) { + snake.append('_'); + } + snake.append(Character.toLowerCase(c)); + } else { + snake.append(c); + } + } + + return snake.toString(); + } + + /** + * 将 snake_case 转换为 camelCase + */ + public static String snakeToCamel(String snake) { + if (snake == null || !snake.contains("_")) { + return snake; + } + + StringBuilder camel = new StringBuilder(snake.length()); + boolean nextUpper = false; + + for (char c : snake.toCharArray()) { + if (c == '_') { + nextUpper = true; + } else if (nextUpper) { + camel.append(Character.toUpperCase(c)); + nextUpper = false; + } else { + camel.append(c); + } + } + + return camel.toString(); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java new file mode 100644 index 0000000..ccab288 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.utils.poi; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Workbook; + +/** + * Excel数据格式处理适配器 + * + * @author ruoyi + */ +public interface ExcelHandlerAdapter +{ + /** + * 格式化 + * + * @param value 单元格数据值 + * @param args excel注解args参数组 + * @param cell 单元格对象 + * @param wb 工作簿对象 + * + * @return 处理后的值 + */ + Object format(Object value, String[] args, Cell cell, Workbook wb); +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java new file mode 100644 index 0000000..2cdca67 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java @@ -0,0 +1,1715 @@ +package com.ruoyi.common.utils.poi; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.annotation.Excel.Type; +import com.ruoyi.common.annotation.Excels; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.UtilException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileTypeUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.ImageUtils; +import com.ruoyi.common.utils.reflect.ReflectUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.poi.hssf.usermodel.*; +import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.*; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Excel相关处理 + * + * @author ruoyi + */ +public class ExcelUtil +{ + private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); + + public static final String FORMULA_REGEX_STR = "=|-|\\+|@"; + + public static final String[] FORMULA_STR = { "=", "-", "+", "@" }; + + /** + * 用于dictType属性数据存储,避免重复查缓存 + */ + public Map sysDictMap = new HashMap(); + + /** + * Excel sheet最大行数,默认65536 + */ + public static final int sheetSize = 65536; + + /** + * 工作表名称 + */ + private String sheetName; + + /** + * 导出类型(EXPORT:导出数据;IMPORT:导入模板) + */ + private Type type; + + /** + * 工作薄对象 + */ + private Workbook wb; + + /** + * 工作表对象 + */ + private Sheet sheet; + + /** + * 样式列表 + */ + private Map styles; + + /** + * 导入导出数据列表 + */ + private List list; + + /** + * 注解列表 + */ + private List fields; + + /** + * 当前行号 + */ + private int rownum; + + /** + * 标题 + */ + private String title; + + /** + * 最大高度 + */ + private short maxHeight; + + /** + * 合并后最后行数 + */ + private int subMergedLastRowNum = 0; + + /** + * 合并后开始行数 + */ + private int subMergedFirstRowNum = 1; + + /** + * 对象的子列表方法 + */ + private Method subMethod; + + /** + * 对象的子列表属性 + */ + private List subFields; + + /** + * 统计列表 + */ + private Map statistics = new HashMap(); + + /** + * 数字格式 + */ + private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00"); + + /** + * 实体对象 + */ + public Class clazz; + + /** + * 需要排除列属性 + */ + public String[] excludeFields; + + public ExcelUtil(Class clazz) + { + this.clazz = clazz; + } + + /** + * 隐藏Excel中列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + * @throws Exception + */ + public void hideColumn(String... fields) + { + this.excludeFields = fields; + } + + public void init(List list, String sheetName, String title, Type type) + { + if (list == null) + { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.type = type; + this.title = title; + createExcelField(); + createWorkbook(); + createTitle(); + createSubHead(); + } + + /** + * 创建excel第一行标题 + */ + public void createTitle() + { + if (StringUtils.isNotEmpty(title)) + { + subMergedFirstRowNum++; + subMergedLastRowNum++; + int titleLastCol = this.fields.size() - 1; + if (isSubList()) + { + titleLastCol = titleLastCol + subFields.size() - 1; + } + Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0); + titleRow.setHeightInPoints(30); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellStyle(styles.get("title")); + titleCell.setCellValue(title); + sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(), titleLastCol)); + } + } + + /** + * 创建对象的子列表名称 + */ + public void createSubHead() + { + if (isSubList()) + { + subMergedFirstRowNum++; + subMergedLastRowNum++; + Row subRow = sheet.createRow(rownum); + int excelNum = 0; + for (Object[] objects : fields) + { + Excel attr = (Excel) objects[1]; + Cell headCell1 = subRow.createCell(excelNum); + headCell1.setCellValue(attr.name()); + headCell1.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + excelNum++; + } + int headFirstRow = excelNum - 1; + int headLastRow = headFirstRow + subFields.size() - 1; + if (headLastRow > headFirstRow) + { + sheet.addMergedRegion(new CellRangeAddress(rownum, rownum, headFirstRow, headLastRow)); + } + rownum++; + } + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(InputStream is) + { + List list = null; + try + { + list = importExcel(is, 0); + } + catch (Exception e) + { + log.error("导入Excel异常{}", e.getMessage()); + throw new UtilException(e.getMessage()); + } + finally + { + IOUtils.closeQuietly(is); + } + return list; + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @param titleNum 标题占用行数 + * @return 转换后集合 + */ + public List importExcel(InputStream is, int titleNum) throws Exception + { + return importExcel(StringUtils.EMPTY, is, titleNum); + } + + /** + * 对excel表单指定表格索引名转换成list + * + * @param sheetName 表格索引名 + * @param titleNum 标题占用行数 + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(String sheetName, InputStream is, int titleNum) throws Exception + { + this.type = Type.IMPORT; + this.wb = WorkbookFactory.create(is); + List list = new ArrayList(); + // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet + Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0); + if (sheet == null) + { + throw new IOException("文件sheet不存在"); + } + boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook); + Map pictures; + if (isXSSFWorkbook) + { + pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb); + } + else + { + pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb); + } + // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1 + int rows = sheet.getLastRowNum(); + if (rows > 0) + { + // 定义一个map用于存放excel列的序号和field. + Map cellMap = new HashMap(); + // 获取表头 + Row heard = sheet.getRow(titleNum); + for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) + { + Cell cell = heard.getCell(i); + if (StringUtils.isNotNull(cell)) + { + String value = this.getCellValue(heard, i).toString(); + cellMap.put(value, i); + } + else + { + cellMap.put(null, i); + } + } + // 有数据时才处理 得到类的所有field. + List fields = this.getFields(); + Map fieldsMap = new HashMap(); + for (Object[] objects : fields) + { + Excel attr = (Excel) objects[1]; + Integer column = cellMap.get(attr.name()); + if (column != null) + { + fieldsMap.put(column, objects); + } + } + for (int i = titleNum + 1; i <= rows; i++) + { + // 从第2行开始取数据,默认第一行是表头. + Row row = sheet.getRow(i); + // 判断当前行是否是空行 + if (isRowEmpty(row)) + { + continue; + } + T entity = null; + for (Map.Entry entry : fieldsMap.entrySet()) + { + Object val = this.getCellValue(row, entry.getKey()); + + // 如果不存在实例则新建. + entity = (entity == null ? clazz.newInstance() : entity); + // 从map中得到对应列的field. + Field field = (Field) entry.getValue()[0]; + Excel attr = (Excel) entry.getValue()[1]; + // 取得类型,并根据对象类型设置值. + Class fieldType = field.getType(); + if (String.class == fieldType) + { + String s = Convert.toStr(val); + if (StringUtils.endsWith(s, ".0")) + { + val = StringUtils.substringBefore(s, ".0"); + } + else + { + String dateFormat = field.getAnnotation(Excel.class).dateFormat(); + if (StringUtils.isNotEmpty(dateFormat)) + { + val = parseDateToStr(dateFormat, val); + } + else + { + val = Convert.toStr(val); + } + } + } + else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toInt(val); + } + else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toLong(val); + } + else if (Double.TYPE == fieldType || Double.class == fieldType) + { + val = Convert.toDouble(val); + } + else if (Float.TYPE == fieldType || Float.class == fieldType) + { + val = Convert.toFloat(val); + } + else if (BigDecimal.class == fieldType) + { + val = Convert.toBigDecimal(val); + } + else if (Date.class == fieldType) + { + if (val instanceof String) + { + val = DateUtils.parseDate(val); + } + else if (val instanceof Double) + { + val = DateUtil.getJavaDate((Double) val); + } + } + else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) + { + val = Convert.toBool(val, false); + } + if (StringUtils.isNotNull(fieldType)) + { + String propertyName = field.getName(); + if (StringUtils.isNotEmpty(attr.targetAttr())) + { + propertyName = field.getName() + "." + attr.targetAttr(); + } + if (StringUtils.isNotEmpty(attr.readConverterExp())) + { + val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); + } + else if (StringUtils.isNotEmpty(attr.dictType())) + { + val = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator()); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + val = dataFormatHandlerAdapter(val, attr, null); + } + else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures)) + { + PictureData image = pictures.get(row.getRowNum() + "_" + entry.getKey()); + if (image == null) + { + val = ""; + } + else + { + byte[] data = image.getData(); + val = FileUtils.writeImportBytes(data); + } + } + ReflectUtils.invokeSetter(entity, propertyName, val); + } + } + list.add(entity); + } + } + return list; + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName) + { + return exportExcel(list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName, String title) + { + this.init(list, sheetName, title, Type.EXPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName) + { + exportExcel(response, list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(list, sheetName, title, Type.EXPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName) + { + return importTemplateExcel(sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName, String title) + { + this.init(null, sheetName, title, Type.IMPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName) + { + importTemplateExcel(response, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(null, sheetName, title, Type.IMPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public void exportExcel(HttpServletResponse response) + { + try + { + writeSheet(); + wb.write(response.getOutputStream()); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + } + finally + { + IOUtils.closeQuietly(wb); + } + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public AjaxResult exportExcel() + { + OutputStream out = null; + try + { + writeSheet(); + String filename = encodingFilename(sheetName); + out = new FileOutputStream(getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + throw new UtilException("导出Excel失败,请联系网站管理员!"); + } + finally + { + IOUtils.closeQuietly(wb); + IOUtils.closeQuietly(out); + } + } + + /** + * 创建写入数据到Sheet + */ + public void writeSheet() + { + // 取出一共有多少个sheet. + int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize)); + for (int index = 0; index < sheetNo; index++) + { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(rownum); + int column = 0; + // 写入各个字段的列头名称 + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + this.createHeadCell(subExcel, row, column++); + } + } + else + { + this.createHeadCell(excel, row, column++); + } + } + if (Type.EXPORT.equals(type)) + { + fillExcelData(index, row); + addStatisticsRow(); + } + } + } + + /** + * 填充excel数据 + * + * @param index 序号 + * @param row 单元格行 + */ + @SuppressWarnings("unchecked") + public void fillExcelData(int index, Row row) + { + int startNo = index * sheetSize; + int endNo = Math.min(startNo + sheetSize, list.size()); + int rowNo = (1 + rownum) - startNo; + for (int i = startNo; i < endNo; i++) + { + rowNo = isSubList() ? (i > 1 ? rowNo + 1 : rowNo + i) : i + 1 + rownum - startNo; + row = sheet.createRow(rowNo); + // 得到导出对象. + T vo = (T) list.get(i); + Collection subList = null; + if (isSubList()) + { + if (isSubListValue(vo)) + { + subList = getListCellValue(vo); + subMergedLastRowNum = subMergedLastRowNum + subList.size(); + } + else + { + subMergedFirstRowNum++; + subMergedLastRowNum++; + } + } + int column = 0; + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType()) && StringUtils.isNotNull(subList)) + { + boolean subFirst = false; + for (Object obj : subList) + { + if (subFirst) + { + rowNo++; + row = sheet.createRow(rowNo); + } + List subFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), Excel.class); + int subIndex = 0; + for (Field subField : subFields) + { + if (subField.isAnnotationPresent(Excel.class)) + { + subField.setAccessible(true); + Excel attr = subField.getAnnotation(Excel.class); + this.addCell(attr, row, (T) obj, subField, column + subIndex); + } + subIndex++; + } + subFirst = true; + } + this.subMergedFirstRowNum = this.subMergedFirstRowNum + subList.size(); + } + else + { + this.addCell(excel, row, vo, field, column++); + } + } + } + } + + /** + * 创建表格样式 + * + * @param wb 工作薄对象 + * @return 样式列表 + */ + private Map createStyles(Workbook wb) + { + // 写入各条记录,每条记录对应excel表中的一行 + Map styles = new HashMap(); + CellStyle style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font titleFont = wb.createFont(); + titleFont.setFontName("Arial"); + titleFont.setFontHeightInPoints((short) 16); + titleFont.setBold(true); + style.setFont(titleFont); + styles.put("title", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + style.setFont(dataFont); + styles.put("data", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font totalFont = wb.createFont(); + totalFont.setFontName("Arial"); + totalFont.setFontHeightInPoints((short) 10); + style.setFont(totalFont); + styles.put("total", style); + + styles.putAll(annotationHeaderStyles(wb, styles)); + + styles.putAll(annotationDataStyles(wb)); + + return styles; + } + + /** + * 根据Excel注解创建表格头样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationHeaderStyles(Workbook wb, Map styles) + { + Map headerStyles = new HashMap(); + for (Object[] os : fields) + { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor()); + if (!headerStyles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setFillForegroundColor(excel.headerBackgroundColor().index); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(excel.headerColor().index); + style.setFont(headerFont); + headerStyles.put(key, style); + } + } + return headerStyles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationDataStyles(Workbook wb) + { + Map styles = new HashMap(); + for (Object[] os : fields) + { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("data_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor()); + if (!styles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.setAlignment(excel.align()); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setFillForegroundColor(excel.backgroundColor().getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + dataFont.setColor(excel.color().index); + style.setFont(dataFont); + styles.put(key, style); + } + } + return styles; + } + + /** + * 创建单元格 + */ + public Cell createHeadCell(Excel attr, Row row, int column) + { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(attr.name()); + setDataValidation(attr, row, column); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (isSubList()) + { + // 填充默认样式,防止合并单元格样式失效 + sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor()))); + if (attr.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column)); + } + } + return cell; + } + + /** + * 设置单元格信息 + * + * @param value 单元格值 + * @param attr 注解相关 + * @param cell 单元格信息 + */ + public void setCellVo(Object value, Excel attr, Cell cell) + { + if (ColumnType.STRING == attr.cellType()) + { + String cellValue = Convert.toStr(value); + // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。 + if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) + { + cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0"); + } + if (value instanceof Collection && StringUtils.equals("[]", cellValue)) + { + cellValue = StringUtils.EMPTY; + } + cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix()); + } + else if (ColumnType.NUMERIC == attr.cellType()) + { + if (StringUtils.isNotNull(value)) + { + cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value)); + } + } + else if (ColumnType.IMAGE == attr.cellType()) + { + ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1); + String imagePath = Convert.toStr(value); + if (StringUtils.isNotEmpty(imagePath)) + { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()).createPicture(anchor, + cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } + } + } + + /** + * 获取画布 + */ + public static Drawing getDrawingPatriarch(Sheet sheet) + { + if (sheet.getDrawingPatriarch() == null) + { + sheet.createDrawingPatriarch(); + } + return sheet.getDrawingPatriarch(); + } + + /** + * 获取图片类型,设置图片插入类型 + */ + public int getImageType(byte[] value) + { + String type = FileTypeUtils.getFileExtendName(value); + if ("JPG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_JPEG; + } + else if ("PNG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_PNG; + } + return Workbook.PICTURE_TYPE_JPEG; + } + + /** + * 创建表格样式 + */ + public void setDataValidation(Excel attr, Row row, int column) + { + if (attr.name().indexOf("注:") >= 0) + { + sheet.setColumnWidth(column, 6000); + } + else + { + // 设置列宽 + sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); + } + if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0) + { + if (attr.combo().length > 15 || StringUtils.join(attr.combo()).length() > 255) + { + // 如果下拉数大于15或字符串长度大于255,则使用一个新sheet存储,避免生成的模板下拉值获取不到 + setXSSFValidationWithHidden(sheet, attr.combo(), attr.prompt(), 1, 100, column, column); + } + else + { + // 提示信息或只能选择不能输入的列内容. + setPromptOrValidation(sheet, attr.combo(), attr.prompt(), 1, 100, column, column); + } + } + } + + /** + * 添加单元格 + */ + public Cell addCell(Excel attr, Row row, T vo, Field field, int column) + { + Cell cell = null; + try + { + // 设置行高 + row.setHeight(maxHeight); + // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. + if (attr.isExport()) + { + // 创建cell + cell = row.createCell(column); + if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge()) + { + CellRangeAddress cellAddress = new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column); + sheet.addMergedRegion(cellAddress); + } + cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor()))); + + // 用于读取对象中的属性 + Object value = getTargetValue(vo, field, attr); + String dateFormat = attr.dateFormat(); + String readConverterExp = attr.readConverterExp(); + String separator = attr.separator(); + String dictType = attr.dictType(); + if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) + { + cell.setCellValue(parseDateToStr(dateFormat, value)); + } + else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) + { + cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); + } + else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value)) + { + if (!sysDictMap.containsKey(dictType + value)) + { + String lable = convertDictByExp(Convert.toStr(value), dictType, separator); + sysDictMap.put(dictType + value, lable); + } + cell.setCellValue(sysDictMap.get(dictType + value)); + } + else if (value instanceof BigDecimal && -1 != attr.scale()) + { + cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue()); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + cell.setCellValue(dataFormatHandlerAdapter(value, attr, cell)); + } + else + { + // 设置列类型 + setCellVo(value, attr, cell); + } + addStatisticsData(column, Convert.toStr(value), attr); + } + } + catch (Exception e) + { + log.error("导出Excel失败{}", e); + } + return cell; + } + + /** + * 设置 POI XSSFSheet 单元格提示或选择框 + * + * @param sheet 表单 + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, + int firstCol, int endCol) + { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + sheet.addValidationData(dataValidation); + } + + /** + * 设置某些列的值只能输入预制的数据,显示下拉框(兼容超出一定数量的下拉框). + * + * @param sheet 要设置的sheet. + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setXSSFValidationWithHidden(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, int firstCol, int endCol) + { + String hideSheetName = "combo_" + firstCol + "_" + endCol; + Sheet hideSheet = wb.createSheet(hideSheetName); // 用于存储 下拉菜单数据 + for (int i = 0; i < textlist.length; i++) + { + hideSheet.createRow(i).createCell(0).setCellValue(textlist[i]); + } + // 创建名称,可被其他单元格引用 + Name name = wb.createName(); + name.setNameName(hideSheetName + "_data"); + name.setRefersToFormula(hideSheetName + "!$A$1:$A$" + textlist.length); + DataValidationHelper helper = sheet.getDataValidationHelper(); + // 加载下拉列表内容 + DataValidationConstraint constraint = helper.createFormulaListConstraint(hideSheetName + "_data"); + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + // 数据有效性对象 + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + + sheet.addValidationData(dataValidation); + // 设置hiddenSheet隐藏 + wb.setSheetHidden(wb.getSheetIndex(hideSheet), true); + } + + /** + * 解析导出值 0=男,1=女,2=未知 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String convertByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[0].equals(value)) + { + propertyString.append(itemArray[1] + separator); + break; + } + } + } + else + { + if (itemArray[0].equals(propertyValue)) + { + return itemArray[1]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 反向解析值 男=0,女=1,未知=2 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String reverseByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[1].equals(value)) + { + propertyString.append(itemArray[0] + separator); + break; + } + } + } + else + { + if (itemArray[1].equals(propertyValue)) + { + return itemArray[0]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 解析字典值 + * + * @param dictValue 字典值 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String convertDictByExp(String dictValue, String dictType, String separator) + { + return DictUtils.getDictLabel(dictType, dictValue, separator); + } + + /** + * 反向解析值字典值 + * + * @param dictLabel 字典标签 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典值 + */ + public static String reverseDictByExp(String dictLabel, String dictType, String separator) + { + return DictUtils.getDictValue(dictType, dictLabel, separator); + } + + /** + * 数据处理器 + * + * @param value 数据值 + * @param excel 数据注解 + * @return + */ + public String dataFormatHandlerAdapter(Object value, Excel excel, Cell cell) + { + try + { + Object instance = excel.handler().newInstance(); + Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class, Cell.class, Workbook.class }); + value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb); + } + catch (Exception e) + { + log.error("不能格式化数据 " + excel.handler(), e.getMessage()); + } + return Convert.toStr(value); + } + + /** + * 合计统计信息 + */ + private void addStatisticsData(Integer index, String text, Excel entity) + { + if (entity != null && entity.isStatistics()) + { + Double temp = 0D; + if (!statistics.containsKey(index)) + { + statistics.put(index, temp); + } + try + { + temp = Double.valueOf(text); + } + catch (NumberFormatException e) + { + } + statistics.put(index, statistics.get(index) + temp); + } + } + + /** + * 创建统计行 + */ + public void addStatisticsRow() + { + if (statistics.size() > 0) + { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + Set keys = statistics.keySet(); + Cell cell = row.createCell(0); + cell.setCellStyle(styles.get("total")); + cell.setCellValue("合计"); + + for (Integer key : keys) + { + cell = row.createCell(key); + cell.setCellStyle(styles.get("total")); + cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key))); + } + statistics.clear(); + } + } + + /** + * 编码文件名 + */ + public String encodingFilename(String filename) + { + filename = UUID.randomUUID() + "_" + filename + ".xlsx"; + return filename; + } + + /** + * 获取下载路径 + * + * @param filename 文件名称 + */ + public String getAbsoluteFile(String filename) + { + String downloadPath = RuoYiConfig.getDownloadPath() + "/" + filename; + File desc = new File(downloadPath); + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + return downloadPath; + } + + /** + * 获取bean中的属性值 + * + * @param vo 实体对象 + * @param field 字段 + * @param excel 注解 + * @return 最终的属性值 + * @throws Exception + */ + private Object getTargetValue(T vo, Field field, Excel excel) throws Exception + { + Object o = field.get(vo); + if (StringUtils.isNotEmpty(excel.targetAttr())) + { + String target = excel.targetAttr(); + if (target.contains(".")) + { + String[] targets = target.split("[.]"); + for (String name : targets) + { + o = getValue(o, name); + } + } + else + { + o = getValue(o, target); + } + } + return o; + } + + /** + * 以类的属性的get方法方法形式获取值 + * + * @param o + * @param name + * @return value + * @throws Exception + */ + private Object getValue(Object o, String name) throws Exception + { + if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) + { + Class clazz = o.getClass(); + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + o = field.get(o); + } + return o; + } + + /** + * 得到所有定义字段 + */ + private void createExcelField() + { + this.fields = getFields(); + this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); + this.maxHeight = getRowHeight(); + } + + /** + * 获取字段注解信息 + */ + public List getFields() + { + List fields = new ArrayList(); + List tempFields = new ArrayList<>(); + tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); + tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + for (Field field : tempFields) + { + if (!ArrayUtils.contains(this.excludeFields, field.getName())) + { + // 单注解 + if (field.isAnnotationPresent(Excel.class)) + { + Excel attr = field.getAnnotation(Excel.class); + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) + { + field.setAccessible(true); + fields.add(new Object[] { field, attr }); + } + if (Collection.class.isAssignableFrom(field.getType())) + { + subMethod = getSubMethod(field.getName(), clazz); + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + } + } + + // 多注解 + if (field.isAnnotationPresent(Excels.class)) + { + Excels attrs = field.getAnnotation(Excels.class); + Excel[] excels = attrs.value(); + for (Excel attr : excels) + { + if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr()) + && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) + { + field.setAccessible(true); + fields.add(new Object[] { field, attr }); + } + } + } + } + } + return fields; + } + + /** + * 根据注解获取最大行高 + */ + public short getRowHeight() + { + double maxHeight = 0; + for (Object[] os : this.fields) + { + Excel excel = (Excel) os[1]; + maxHeight = Math.max(maxHeight, excel.height()); + } + return (short) (maxHeight * 20); + } + + /** + * 创建一个工作簿 + */ + public void createWorkbook() + { + this.wb = new SXSSFWorkbook(500); + this.sheet = wb.createSheet(); + wb.setSheetName(0, sheetName); + this.styles = createStyles(wb); + } + + /** + * 创建工作表 + * + * @param sheetNo sheet数量 + * @param index 序号 + */ + public void createSheet(int sheetNo, int index) + { + // 设置工作表的名称. + if (sheetNo > 1 && index > 0) + { + this.sheet = wb.createSheet(); + this.createTitle(); + wb.setSheetName(index, sheetName + index); + } + } + + /** + * 获取单元格值 + * + * @param row 获取的行 + * @param column 获取单元格列号 + * @return 单元格值 + */ + public Object getCellValue(Row row, int column) + { + if (row == null) + { + return row; + } + Object val = ""; + try + { + Cell cell = row.getCell(column); + if (StringUtils.isNotNull(cell)) + { + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) + { + val = cell.getNumericCellValue(); + if (DateUtil.isCellDateFormatted(cell)) + { + val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 + } + else + { + if ((Double) val % 1 != 0) + { + val = new BigDecimal(val.toString()); + } + else + { + val = new DecimalFormat("0").format(val); + } + } + } + else if (cell.getCellType() == CellType.STRING) + { + val = cell.getStringCellValue(); + } + else if (cell.getCellType() == CellType.BOOLEAN) + { + val = cell.getBooleanCellValue(); + } + else if (cell.getCellType() == CellType.ERROR) + { + val = cell.getErrorCellValue(); + } + + } + } + catch (Exception e) + { + return val; + } + return val; + } + + /** + * 判断是否是空行 + * + * @param row 判断的行 + * @return + */ + private boolean isRowEmpty(Row row) + { + if (row == null) + { + return true; + } + for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) + { + Cell cell = row.getCell(i); + if (cell != null && cell.getCellType() != CellType.BLANK) + { + return false; + } + } + return true; + } + + /** + * 获取Excel2003图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map getSheetPictures03(HSSFSheet sheet, HSSFWorkbook workbook) + { + Map sheetIndexPicMap = new HashMap(); + List pictures = workbook.getAllPictures(); + if (!pictures.isEmpty()) + { + for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) + { + HSSFClientAnchor anchor = (HSSFClientAnchor) shape.getAnchor(); + if (shape instanceof HSSFPicture) + { + HSSFPicture pic = (HSSFPicture) shape; + int pictureIndex = pic.getPictureIndex() - 1; + HSSFPictureData picData = pictures.get(pictureIndex); + String picIndex = anchor.getRow1() + "_" + anchor.getCol1(); + sheetIndexPicMap.put(picIndex, picData); + } + } + return sheetIndexPicMap; + } + else + { + return sheetIndexPicMap; + } + } + + /** + * 获取Excel2007图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map getSheetPictures07(XSSFSheet sheet, XSSFWorkbook workbook) + { + Map sheetIndexPicMap = new HashMap(); + for (POIXMLDocumentPart dr : sheet.getRelations()) + { + if (dr instanceof XSSFDrawing) + { + XSSFDrawing drawing = (XSSFDrawing) dr; + List shapes = drawing.getShapes(); + for (XSSFShape shape : shapes) + { + if (shape instanceof XSSFPicture) + { + XSSFPicture pic = (XSSFPicture) shape; + XSSFClientAnchor anchor = pic.getPreferredSize(); + CTMarker ctMarker = anchor.getFrom(); + String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol(); + sheetIndexPicMap.put(picIndex, pic.getPictureData()); + } + } + } + } + return sheetIndexPicMap; + } + + /** + * 格式化不同类型的日期对象 + * + * @param dateFormat 日期格式 + * @param val 被格式化的日期对象 + * @return 格式化后的日期字符 + */ + public String parseDateToStr(String dateFormat, Object val) + { + if (val == null) + { + return ""; + } + String str; + if (val instanceof Date) + { + str = DateUtils.parseDateToStr(dateFormat, (Date) val); + } + else if (val instanceof LocalDateTime) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val)); + } + else if (val instanceof LocalDate) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val)); + } + else + { + str = val.toString(); + } + return str; + } + + /** + * 是否有对象的子列表 + */ + public boolean isSubList() + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0; + } + + /** + * 是否有对象的子列表,集合不为空 + */ + public boolean isSubListValue(T vo) + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0 && StringUtils.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0; + } + + /** + * 获取集合的值 + */ + public Collection getListCellValue(Object obj) + { + Object value; + try + { + value = subMethod.invoke(obj, new Object[] {}); + } + catch (Exception e) + { + return new ArrayList(); + } + return (Collection) value; + } + + /** + * 获取对象的子列表方法 + * + * @param name 名称 + * @param pojoClass 类对象 + * @return 子列表方法 + */ + public Method getSubMethod(String name, Class pojoClass) + { + StringBuffer getMethodName = new StringBuffer("get"); + getMethodName.append(name.substring(0, 1).toUpperCase()); + getMethodName.append(name.substring(1)); + Method method = null; + try + { + method = pojoClass.getMethod(getMethodName.toString(), new Class[] {}); + } + catch (Exception e) + { + log.error("获取对象异常{}", e.getMessage()); + } + return method; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java new file mode 100644 index 0000000..b19953e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java @@ -0,0 +1,410 @@ +package com.ruoyi.common.utils.reflect; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Date; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.poi.ss.usermodel.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.DateUtils; + +/** + * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. + * + * @author ruoyi + */ +@SuppressWarnings("rawtypes") +public class ReflectUtils +{ + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + private static final String CGLIB_CLASS_SEPARATOR = "$$"; + + private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class); + + /** + * 调用Getter方法. + * 支持多级,如:对象名.对象名.方法 + */ + @SuppressWarnings("unchecked") + public static E invokeGetter(Object obj, String propertyName) + { + Object object = obj; + for (String name : StringUtils.split(propertyName, ".")) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + return (E) object; + } + + /** + * 调用Setter方法, 仅匹配方法名。 + * 支持多级,如:对象名.对象名.方法 + */ + public static void invokeSetter(Object obj, String propertyName, E value) + { + Object object = obj; + String[] names = StringUtils.split(propertyName, "."); + for (int i = 0; i < names.length; i++) + { + if (i < names.length - 1) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + else + { + String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]); + invokeMethodByName(object, setterMethodName, new Object[] { value }); + } + } + } + + /** + * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数. + */ + @SuppressWarnings("unchecked") + public static E getFieldValue(final Object obj, final String fieldName) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return null; + } + E result = null; + try + { + result = (E) field.get(obj); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常{}", e.getMessage()); + } + return result; + } + + /** + * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数. + */ + public static void setFieldValue(final Object obj, final String fieldName, final E value) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return; + } + try + { + field.set(obj, value); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常: {}", e.getMessage()); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符. + * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用. + * 同时匹配方法名+参数类型, + */ + @SuppressWarnings("unchecked") + public static E invokeMethod(final Object obj, final String methodName, final Class[] parameterTypes, + final Object[] args) + { + if (obj == null || methodName == null) + { + return null; + } + Method method = getAccessibleMethod(obj, methodName, parameterTypes); + if (method == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符, + * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用. + * 只匹配函数名,如果有多个同名函数调用第一个。 + */ + @SuppressWarnings("unchecked") + public static E invokeMethodByName(final Object obj, final String methodName, final Object[] args) + { + Method method = getAccessibleMethodByName(obj, methodName, args.length); + if (method == null) + { + // 如果为空不报错,直接返回空。 + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + // 类型转换(将参数数据类型转换为目标方法参数类型) + Class[] cs = method.getParameterTypes(); + for (int i = 0; i < cs.length; i++) + { + if (args[i] != null && !args[i].getClass().equals(cs[i])) + { + if (cs[i] == String.class) + { + args[i] = Convert.toStr(args[i]); + if (StringUtils.endsWith((String) args[i], ".0")) + { + args[i] = StringUtils.substringBefore((String) args[i], ".0"); + } + } + else if (cs[i] == Integer.class) + { + args[i] = Convert.toInt(args[i]); + } + else if (cs[i] == Long.class) + { + args[i] = Convert.toLong(args[i]); + } + else if (cs[i] == Double.class) + { + args[i] = Convert.toDouble(args[i]); + } + else if (cs[i] == Float.class) + { + args[i] = Convert.toFloat(args[i]); + } + else if (cs[i] == Date.class) + { + if (args[i] instanceof String) + { + args[i] = DateUtils.parseDate(args[i]); + } + else + { + args[i] = DateUtil.getJavaDate((Double) args[i]); + } + } + else if (cs[i] == boolean.class || cs[i] == Boolean.class) + { + args[i] = Convert.toBool(args[i]); + } + } + } + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + */ + public static Field getAccessibleField(final Object obj, final String fieldName) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(fieldName, "fieldName can't be blank"); + for (Class superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) + { + try + { + Field field = superClass.getDeclaredField(fieldName); + makeAccessible(field); + return field; + } + catch (NoSuchFieldException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 匹配函数名+参数类型。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethod(final Object obj, final String methodName, + final Class... parameterTypes) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + try + { + Method method = searchType.getDeclaredMethod(methodName, parameterTypes); + makeAccessible(method); + return method; + } + catch (NoSuchMethodException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 只匹配函数名。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethodByName(final Object obj, final String methodName, int argsNum) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + Method[] methods = searchType.getDeclaredMethods(); + for (Method method : methods) + { + if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum) + { + makeAccessible(method); + return method; + } + } + } + return null; + } + + /** + * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Method method) + { + if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) + && !method.isAccessible()) + { + method.setAccessible(true); + } + } + + /** + * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Field field) + { + if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) + || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) + { + field.setAccessible(true); + } + } + + /** + * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处 + * 如无法找到, 返回Object.class. + */ + @SuppressWarnings("unchecked") + public static Class getClassGenricType(final Class clazz) + { + return getClassGenricType(clazz, 0); + } + + /** + * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. + * 如无法找到, 返回Object.class. + */ + public static Class getClassGenricType(final Class clazz, final int index) + { + Type genType = clazz.getGenericSuperclass(); + + if (!(genType instanceof ParameterizedType)) + { + logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType"); + return Object.class; + } + + Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); + + if (index >= params.length || index < 0) + { + logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + + params.length); + return Object.class; + } + if (!(params[index] instanceof Class)) + { + logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); + return Object.class; + } + + return (Class) params[index]; + } + + public static Class getUserClass(Object instance) + { + if (instance == null) + { + throw new RuntimeException("Instance must not be null"); + } + Class clazz = instance.getClass(); + if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) + { + Class superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) + { + return superClass; + } + } + return clazz; + + } + + /** + * 将反射时的checked exception转换为unchecked exception. + */ + public static RuntimeException convertReflectionExceptionToUnchecked(String msg, Exception e) + { + if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException + || e instanceof NoSuchMethodException) + { + return new IllegalArgumentException(msg, e); + } + else if (e instanceof InvocationTargetException) + { + return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException()); + } + return new RuntimeException(msg, e); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java new file mode 100644 index 0000000..ca1cd92 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java @@ -0,0 +1,291 @@ +package com.ruoyi.common.utils.sign; + +/** + * Base64工具类 + * + * @author ruoyi + */ +public final class Base64 +{ + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static + { + for (int i = 0; i < BASELENGTH; ++i) + { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) + { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) + { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) + { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) + { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + } + + private static boolean isWhiteSpace(char octect) + { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) + { + return (octect == PAD); + } + + private static boolean isData(char octect) + { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) + { + if (binaryData == null) + { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) + { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) + { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) + { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } + else if (fewerThan24bits == SIXTEENBIT) + { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) + { + if (encoded == null) + { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) + { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) + { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) + { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) + { + return null; + } // if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) + { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) + {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) + { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } + else if (!isPad(d3) && isPad(d4)) + { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } + else + { + return null; + } + } + else + { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) + { + if (data == null) + { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) + { + if (!isWhiteSpace(data[i])) + { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java new file mode 100644 index 0000000..c1c58db --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java @@ -0,0 +1,67 @@ +package com.ruoyi.common.utils.sign; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Md5加密方法 + * + * @author ruoyi + */ +public class Md5Utils +{ + private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); + + private static byte[] md5(String s) + { + MessageDigest algorithm; + try + { + algorithm = MessageDigest.getInstance("MD5"); + algorithm.reset(); + algorithm.update(s.getBytes("UTF-8")); + byte[] messageDigest = algorithm.digest(); + return messageDigest; + } + catch (Exception e) + { + log.error("MD5 Error...", e); + } + return null; + } + + private static final String toHex(byte hash[]) + { + if (hash == null) + { + return null; + } + StringBuffer buf = new StringBuffer(hash.length * 2); + int i; + + for (i = 0; i < hash.length; i++) + { + if ((hash[i] & 0xff) < 0x10) + { + buf.append("0"); + } + buf.append(Long.toString(hash[i] & 0xff, 16)); + } + return buf.toString(); + } + + public static String hash(String s) + { + try + { + return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + } + catch (Exception e) + { + log.error("not supported charset...{}", e); + return s; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java new file mode 100644 index 0000000..f290ec3 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java @@ -0,0 +1,158 @@ +package com.ruoyi.common.utils.spring; + +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.StringUtils; + +/** + * spring工具类 方便在非spring管理环境中获取bean + * + * @author ruoyi + */ +@Component +public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware +{ + /** Spring应用上下文环境 */ + private static ConfigurableListableBeanFactory beanFactory; + + private static ApplicationContext applicationContext; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException + { + SpringUtils.beanFactory = beanFactory; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + SpringUtils.applicationContext = applicationContext; + } + + /** + * 获取对象 + * + * @param name + * @return Object 一个以所给名字注册的bean的实例 + * @throws org.springframework.beans.BeansException + * + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException + { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + * + * @param clz + * @return + * @throws org.springframework.beans.BeansException + * + */ + public static T getBean(Class clz) throws BeansException + { + T result = (T) beanFactory.getBean(clz); + return result; + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) + { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + * @return + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getAliases(name); + } + + /** + * 获取aop代理对象 + * + * @param invoker + * @return + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) + { + return (T) AopContext.currentProxy(); + } + + /** + * 获取当前的环境配置,无配置返回null + * + * @return 当前的环境配置 + */ + public static String[] getActiveProfiles() + { + return applicationContext.getEnvironment().getActiveProfiles(); + } + + /** + * 获取当前的环境配置,当有多个环境配置时,只获取第一个 + * + * @return 当前的环境配置 + */ + public static String getActiveProfile() + { + final String[] activeProfiles = getActiveProfiles(); + return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null; + } + + /** + * 获取配置文件中的值 + * + * @param key 配置文件的key + * @return 当前的配置文件的值 + * + */ + public static String getRequiredProperty(String key) + { + return applicationContext.getEnvironment().getRequiredProperty(key); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java new file mode 100644 index 0000000..93b0347 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java @@ -0,0 +1,70 @@ +package com.ruoyi.common.utils.sql; + +import com.ruoyi.common.exception.UtilException; +import com.ruoyi.common.utils.StringUtils; + +/** + * sql操作工具类 + * + * @author ruoyi + */ +public class SqlUtil +{ + /** + * 定义常用的 sql关键字 + */ + public static String SQL_REGEX = "and |extractvalue|updatexml|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |+|user()"; + + /** + * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) + */ + public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; + + /** + * 限制orderBy最大长度 + */ + private static final int ORDER_BY_MAX_LENGTH = 500; + + /** + * 检查字符,防止注入绕过 + */ + public static String escapeOrderBySql(String value) + { + if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) + { + throw new UtilException("参数不符合规范,不能进行查询"); + } + if (StringUtils.length(value) > ORDER_BY_MAX_LENGTH) + { + throw new UtilException("参数已超过最大限制,不能进行查询"); + } + return value; + } + + /** + * 验证 order by 语法是否符合规范 + */ + public static boolean isValidOrderBySql(String value) + { + return value.matches(SQL_PATTERN); + } + + /** + * SQL关键字检查 + */ + public static void filterKeyword(String value) + { + if (StringUtils.isEmpty(value)) + { + return; + } + String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|"); + for (String sqlKeyword : sqlKeywords) + { + if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) + { + throw new UtilException("参数存在SQL注入风险"); + } + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java new file mode 100644 index 0000000..2c84427 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java @@ -0,0 +1,49 @@ +package com.ruoyi.common.utils.uuid; + +/** + * ID生成器工具类 + * + * @author ruoyi + */ +public class IdUtils +{ + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() + { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() + { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + */ + public static String fastUUID() + { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + */ + public static String fastSimpleUUID() + { + return UUID.fastUUID().toString(true); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java new file mode 100644 index 0000000..bf99611 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java @@ -0,0 +1,86 @@ +package com.ruoyi.common.utils.uuid; + +import java.util.concurrent.atomic.AtomicInteger; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * @author ruoyi 序列生成类 + */ +public class Seq +{ + // 通用序列类型 + public static final String commSeqType = "COMMON"; + + // 上传序列类型 + public static final String uploadSeqType = "UPLOAD"; + + // 通用接口序列数 + private static AtomicInteger commSeq = new AtomicInteger(1); + + // 上传接口序列数 + private static AtomicInteger uploadSeq = new AtomicInteger(1); + + // 机器标识 + private static final String machineCode = "A"; + + /** + * 获取通用序列号 + * + * @return 序列值 + */ + public static String getId() + { + return getId(commSeqType); + } + + /** + * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串 + * + * @return 序列值 + */ + public static String getId(String type) + { + AtomicInteger atomicInt = commSeq; + if (uploadSeqType.equals(type)) + { + atomicInt = uploadSeq; + } + return getId(atomicInt, 3); + } + + /** + * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串 + * + * @param atomicInt 序列数 + * @param length 数值长度 + * @return 序列值 + */ + public static String getId(AtomicInteger atomicInt, int length) + { + String result = DateUtils.dateTimeNow(); + result += machineCode; + result += getSeq(atomicInt, length); + return result; + } + + /** + * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数 + * + * @return 序列值 + */ + private synchronized static String getSeq(AtomicInteger atomicInt, int length) + { + // 先取值再+1 + int value = atomicInt.getAndIncrement(); + + // 如果更新后值>=10 的 (length)幂次方则重置为1 + int maxSeq = (int) Math.pow(10, length); + if (atomicInt.get() >= maxSeq) + { + atomicInt.set(1); + } + // 转字符串,用0左补齐 + return StringUtils.padl(value, length); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java new file mode 100644 index 0000000..a5585d6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java @@ -0,0 +1,484 @@ +package com.ruoyi.common.utils.uuid; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import com.ruoyi.common.exception.UtilException; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现 + * + * @author ruoyi + */ +public final class UUID implements java.io.Serializable, Comparable +{ + private static final long serialVersionUID = -1185015143654744140L; + + /** + * SecureRandom 的单例 + * + */ + private static class Holder + { + static final SecureRandom numberGenerator = getSecureRandom(); + } + + /** 此UUID的最高64有效位 */ + private final long mostSigBits; + + /** 此UUID的最低64有效位 */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) + { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) + { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) + { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) + { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() + { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() + { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) + { + final Random ng = isSecure ? Holder.numberGenerator : getRandom(); + + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) + { + MessageDigest md; + try + { + md = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException nsae) + { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + * + */ + public static UUID fromString(String name) + { + String[] components = name.split("-"); + if (components.length != 5) + { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) + { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() + { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() + { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() + { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() + { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException + { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 + * UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException + { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException + { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() + { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) + { + final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (!isSimple) + { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (!isSimple) + { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (!isSimple) + { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (!isSimple) + { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() + { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) + { + if ((null == obj) || (obj.getClass() != UUID.class)) + { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + * + */ + @Override + public int compareTo(UUID val) + { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Private method start + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) + { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() + { + if (version() != 1) + { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + */ + public static SecureRandom getSecureRandom() + { + try + { + return SecureRandom.getInstance("SHA1PRNG"); + } + catch (NoSuchAlgorithmException e) + { + throw new UtilException(e); + } + } + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + */ + public static ThreadLocalRandom getRandom() + { + return ThreadLocalRandom.current(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java b/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java new file mode 100644 index 0000000..cb4b408 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java @@ -0,0 +1,27 @@ +package com.ruoyi.common.xss; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义xss校验注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) +@Constraint(validatedBy = { XssValidator.class }) +public @interface Xss +{ + String message() + + default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java b/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java new file mode 100644 index 0000000..cc282f3 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java @@ -0,0 +1,34 @@ +package com.ruoyi.common.xss; + +import com.ruoyi.common.utils.StringUtils; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 自定义xss校验注解实现 + * + * @author ruoyi + */ +public class XssValidator implements ConstraintValidator +{ + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) + { + if (StringUtils.isBlank(value)) + { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) + { + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + return matcher.matches(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml new file mode 100644 index 0000000..08c3a40 --- /dev/null +++ b/ruoyi-framework/pom.xml @@ -0,0 +1,89 @@ + + + + ruoyi + com.ruoyi + 4.8.2 + + 4.0.0 + + ruoyi-framework + + + framework框架核心 + + + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + + + org.springframework.boot + spring-boot-starter-aspectj + + + + + com.alibaba + druid-spring-boot-4-starter + + + + + pro.fessional + kaptcha + + + servlet-api + javax.servlet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.github.oshi + oshi-core + + + + + com.ruoyi + ruoyi-system + + + + \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java new file mode 100644 index 0000000..1bc2f69 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java @@ -0,0 +1,174 @@ +package com.ruoyi.framework.aspectj; + +import java.util.ArrayList; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.PermissionContextHolder; + +/** + * 数据过滤处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class DataScopeAspect +{ + /** + * 全部数据权限 + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定数据权限 + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限 + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限 + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限 + */ + public static final String DATA_SCOPE_SELF = "5"; + + /** + * 数据权限过滤关键字 + */ + public static final String DATA_SCOPE = "dataScope"; + + @Before("@annotation(controllerDataScope)") + public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable + { + clearDataScope(point); + handleDataScope(point, controllerDataScope); + } + + protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNotNull(loginUser)) + { + SysUser currentUser = loginUser.getUser(); + // 如果是超级管理员,则不过滤数据 + if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) + { + String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext()); + dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), + controllerDataScope.userAlias(), permission); + } + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param user 用户 + * @param deptAlias 部门别名 + * @param userAlias 用户别名 + * @param permission 权限字符 + */ + public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission) + { + StringBuilder sqlString = new StringBuilder(); + List conditions = new ArrayList(); + + for (SysRole role : user.getRoles()) + { + String dataScope = role.getDataScope(); + if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)) + { + continue; + } + if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions()) + && !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + continue; + } + if (DATA_SCOPE_ALL.equals(dataScope)) + { + sqlString = new StringBuilder(); + conditions.add(dataScope); + break; + } + else if (DATA_SCOPE_CUSTOM.equals(dataScope)) + { + sqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, + role.getRoleId())); + } + else if (DATA_SCOPE_DEPT.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); + } + else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) + { + sqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", + deptAlias, user.getDeptId(), user.getDeptId())); + } + else if (DATA_SCOPE_SELF.equals(dataScope)) + { + if (StringUtils.isNotBlank(userAlias)) + { + sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId())); + } + else + { + // 数据权限为仅本人且没有userAlias别名不查询任何数据 + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + } + conditions.add(dataScope); + } + + // 多角色情况下,所有角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据 + if (StringUtils.isEmpty(conditions)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + + if (StringUtils.isNotBlank(sqlString.toString())) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")"); + } + } + } + + /** + * 拼接权限sql前先清空params.dataScope参数防止注入 + */ + private void clearDataScope(final JoinPoint joinPoint) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, ""); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java new file mode 100644 index 0000000..8c2c9f4 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java @@ -0,0 +1,72 @@ +package com.ruoyi.framework.aspectj; + +import java.util.Objects; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.DataSource; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder; + +/** + * 多数据源处理 + * + * @author ruoyi + */ +@Aspect +@Order(1) +@Component +public class DataSourceAspect +{ + protected Logger logger = LoggerFactory.getLogger(getClass()); + + @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)" + + "|| @within(com.ruoyi.common.annotation.DataSource)") + public void dsPointCut() + { + + } + + @Around("dsPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable + { + DataSource dataSource = getDataSource(point); + + if (StringUtils.isNotNull(dataSource)) + { + DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); + } + + try + { + return point.proceed(); + } + finally + { + // 销毁数据源 在执行方法之后 + DynamicDataSourceContextHolder.clearDataSourceType(); + } + } + + /** + * 获取需要切换的数据源 + */ + public DataSource getDataSource(ProceedingJoinPoint point) + { + MethodSignature signature = (MethodSignature) point.getSignature(); + DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); + if (Objects.nonNull(dataSource)) + { + return dataSource; + } + + return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java new file mode 100644 index 0000000..6735673 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java @@ -0,0 +1,442 @@ +package com.ruoyi.framework.aspectj; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.NamedThreadLocal; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.EntityChange; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.HttpMethod; +import com.ruoyi.common.filter.PropertyPreExcludeFilter; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.EntityFieldMapper; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.system.domain.SysOperLog; + +/** Operation metadata plus opt-in, persisted before/after audit snapshots. */ +@Aspect +@Component +public class LogAspect +{ + private static final Logger log = LoggerFactory.getLogger(LogAspect.class); + public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword", + "safePassword", "safePasswordPlain", "remark", "token", "cookie", "secret", "transactionLink" }; + private static final ThreadLocal TIME_THREADLOCAL = new NamedThreadLocal<>("Cost Time"); + private static final ThreadLocal SNAPSHOT = new NamedThreadLocal<>("Operation snapshot"); + + private static class Snapshot + { + Object mapper; + Method selectMethod; + Serializable id; + Map before; + String nickname; + } + + @Before(value = "@annotation(controllerLog)") + public void boBefore(JoinPoint joinPoint, Log controllerLog) + { + TIME_THREADLOCAL.set(System.currentTimeMillis()); + SNAPSHOT.remove(); + if (controllerLog.entityClass() == Void.class) return; + Snapshot snapshot = new Snapshot(); + SNAPSHOT.set(snapshot); + try + { + for (Object arg : joinPoint.getArgs()) + { + if (!controllerLog.entityClass().isInstance(arg)) continue; + for (Field field : controllerLog.entityClass().getDeclaredFields()) + { + if (field.getAnnotation(TableId.class) == null) continue; + field.setAccessible(true); + snapshot.id = (Serializable) field.get(arg); + } + break; + } + if (snapshot.id == null && StringUtils.isNotEmpty(controllerLog.idField())) + { + snapshot.id = findIdByField(joinPoint.getArgs(), controllerLog.entityClass(), controllerLog.idField()); + } + if (snapshot.id == null || controllerLog.mapperClass() == Void.class) return; + // 按 mapperClass 拿具体 bean;不假定它是 BaseMapper(仓库里大量 RuoYi 生成的普通 MyBatis 接口) + snapshot.mapper = SpringUtils.getBean(controllerLog.mapperClass()); + snapshot.selectMethod = findSelectMethod(controllerLog.mapperClass(), controllerLog.entityClass(), snapshot.id); + if (snapshot.selectMethod == null) return; + Object persisted = snapshot.selectMethod.invoke(snapshot.mapper, snapshot.id); + if (persisted != null) + { + snapshot.before = EntityFieldMapper.snapshot(persisted); + snapshot.nickname = snapshot.before.get("nickName"); + } + } + catch (Exception ex) + { + log.warn("Operation before snapshot unavailable: {}", ex.getClass().getSimpleName()); + } + } + + /** + * 找这个 Mapper 上"按主键查一行"的方法:优先 selectById,其次名字以 select 开头、 + * 单参数可接收主键、返回实体类型的任意方法(RuoYi 生成的 selectTtWelfareByWelfareId 这类)。 + */ + private Method findSelectMethod(Class mapperClass, Class entityClass, Serializable id) + { + Method fallback = null; + for (Method m : mapperClass.getMethods()) + { + if (m.getParameterCount() != 1 || !m.getName().startsWith("select")) continue; + if (!m.getReturnType().isAssignableFrom(entityClass) && !entityClass.isAssignableFrom(m.getReturnType())) continue; + Class param = m.getParameterTypes()[0]; + if (param.isPrimitive() || !Serializable.class.isAssignableFrom(boxed(param))) continue; + if ("selectById".equals(m.getName()) && param.isInstance(id)) return m; + if (fallback == null && param.isInstance(id)) fallback = m; + } + return fallback; + } + + private Class boxed(Class c) + { + if (c == int.class) return Integer.class; + if (c == long.class) return Long.class; + if (c == boolean.class) return Boolean.class; + if (c == double.class) return Double.class; + if (c == float.class) return Float.class; + if (c == short.class) return Short.class; + if (c == byte.class) return Byte.class; + return c; + } + + @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) + { + try { handleLog(joinPoint, controllerLog, null, jsonResult); } + finally { clearContext(); } + } + + @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) + { + try { handleLog(joinPoint, controllerLog, e, null); } + finally { clearContext(); } + } + + private void clearContext() + { + TIME_THREADLOCAL.remove(); + SNAPSHOT.remove(); + } + + /** + * 主键不在实体参数里时的兜底:按 @Log(idField=...) 从入参里找。 + * 支持请求体 Map(params.get("userId"))、DTO 的 getXxx()、以及入参本身就是数字/字符串。 + */ + private Serializable findIdByField(Object[] args, Class entityClass, String idField) + { + if (args == null || StringUtils.isEmpty(idField)) return null; + String getter = "get" + Character.toUpperCase(idField.charAt(0)) + idField.substring(1); + for (Object arg : args) + { + if (arg == null || entityClass.isInstance(arg)) continue; + if (arg instanceof Number || arg instanceof String) return (Serializable) arg; + if (arg instanceof Map) + { + Object value = ((Map) arg).get(idField); + if (value instanceof Serializable) return (Serializable) value; + continue; + } + try + { + Object value = arg.getClass().getMethod(getter).invoke(arg); + if (value instanceof Serializable) return (Serializable) value; + } + catch (Exception ignored) + { + // 这个入参没有该属性,继续找下一个 + } + } + return null; + } + + protected void handleLog(JoinPoint joinPoint, Log controllerLog, Exception e, Object jsonResult) + { + try + { + SysOperLog operLog = new SysOperLog(); + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser != null) operLog.setOperName(loginUser.getUsername()); + operLog.setOperIp(IpUtils.getIpAddr()); + operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); + operLog.setMethod(joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"); + operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); + boolean success = e == null && responseSucceeded(jsonResult); + operLog.setStatus(success ? 0 : 1); + // Never put exception messages/response data containing user secrets into configured audit logs. + if (!success) operLog.setErrorMsg(isConfigured(controllerLog) ? "操作未成功,请检查业务返回结果" : + StringUtils.substring(e == null ? "业务操作失败" : e.getMessage(), 0, 2000)); + getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); + Long started = TIME_THREADLOCAL.get(); + operLog.setCostTime(started == null ? 0L : System.currentTimeMillis() - started); + saveLog(operLog); + } + catch (Exception ex) + { + log.warn("Operation log could not be saved: {}", ex.getClass().getSimpleName()); + } + } + + /** Separate persistence boundary so tests exercise the actual advice without writing live audit rows. */ + protected void saveLog(SysOperLog operLog) + { + AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); + } + + private boolean responseSucceeded(Object response) + { + Object code = null; + if (response instanceof AjaxResult) code = ((AjaxResult) response).get("code"); + else if (response instanceof R) code = ((R) response).getCode(); + else return true; + return code != null && "200".equals(String.valueOf(code)); + } + + private boolean isConfigured(Log annotation) + { + return annotation.entityClass() != Void.class || StringUtils.isNotEmpty(annotation.action()) + || StringUtils.isNotEmpty(annotation.menu()); + } + + public void getControllerMethodDescription(JoinPoint point, Log annotation, SysOperLog row, Object result) throws Exception + { + row.setBusinessType(annotation.businessType().ordinal()); + row.setTitle(StringUtils.isNotEmpty(annotation.menu()) ? annotation.menu() : annotation.title()); + row.setOperatorType(annotation.operatorType().ordinal()); + if (isConfigured(annotation)) + { + row.setOperParam(auditPayload(point, annotation, Integer.valueOf(0).equals(row.getStatus()))); + return; + } + if (annotation.isSaveRequestData()) setRequestValue(point, row, annotation.excludeParamNames()); + if (annotation.isSaveResponseData() && result != null) + row.setJsonResult(StringUtils.substring(JSON.toJSONString(result), 0, 2000)); + } + + private String auditPayload(JoinPoint point, Log annotation, boolean success) + { + Map payload = new LinkedHashMap<>(); + payload.put("auditVersion", 1); + payload.put("action", annotation.action()); + Snapshot snapshot = SNAPSHOT.get(); + Map target = new LinkedHashMap<>(); + if (snapshot != null) + { + target.put("id", snapshot.id); + target.put("name", snapshot.nickname); + } + payload.put("target", target); + String state = success ? "notCaptured" : "failed"; + List changes = new ArrayList<>(); + if (success && annotation.entityClass() != Void.class) + { + state = "unavailable"; + if (snapshot != null && snapshot.before != null && snapshot.mapper != null) + { + try + { + // Compare DB snapshots, never a partial request DTO or a controller-mutated argument. + Object after = snapshot.selectMethod.invoke(snapshot.mapper, snapshot.id); + if (after != null) + { + changes = EntityFieldMapper.changes(snapshot.before, EntityFieldMapper.snapshot(after), annotation.entityClass()); + state = changes.isEmpty() ? "unchanged" : "changed"; + } + } + catch (Exception ex) + { + log.warn("Operation after snapshot unavailable: {}", ex.getClass().getSimpleName()); + } + } + } + payload.put("state", state); + payload.put("changes", changes); + // 创建/批量类操作没有"改前改后"可比,但要知道"生成了几张、哪个产品"——带上标量入参 + if (changes.isEmpty()) + { + Map params = scalarParams(point); + if (!params.isEmpty()) + { + payload.put("params", params); + } + } + payload.put("truncated", false); + return boundedJson(payload, changes); + } + + /** 单次记录最多带几个入参、单个值多长 */ + private static final int MAX_PARAMS = 8; + private static final int MAX_PARAM_LEN = 60; + + /** + * 从入参里挑出"能看懂的标量":数字/字符串/布尔、请求体 Map 的标量项、DTO 的标量 getter。 + * 只用于创建类操作补充信息,带不出来的就不带,绝不因此丢掉整条日志。 + */ + private Map scalarParams(JoinPoint point) + { + Map out = new LinkedHashMap<>(); + Object[] args = point.getArgs(); + if (args == null) return out; + String[] names = null; + try + { + names = ((MethodSignature) point.getSignature()).getParameterNames(); + } + catch (Exception ignored) + { + } + for (int i = 0; i < args.length && out.size() < MAX_PARAMS; i++) + { + Object arg = args[i]; + if (arg == null || isFilterObject(arg)) continue; + String argName = names != null && i < names.length ? names[i] : "参数" + (i + 1); + if (arg instanceof Number || arg instanceof Boolean || arg instanceof String) + { + putScalar(out, argName, arg); + } + else if (arg instanceof Map) + { + for (Object key : ((Map) arg).keySet()) + { + if (out.size() >= MAX_PARAMS) break; + putScalar(out, String.valueOf(key), ((Map) arg).get(key)); + } + } + else + { + for (Method getter : arg.getClass().getMethods()) + { + if (out.size() >= MAX_PARAMS) break; + if (getter.getParameterCount() != 0 || !getter.getName().startsWith("get") + || "getClass".equals(getter.getName())) continue; + try + { + putScalar(out, decapitalize(getter.getName().substring(3)), getter.invoke(arg)); + } + catch (Exception ignored) + { + } + } + } + } + return out; + } + + private void putScalar(Map out, String name, Object value) + { + if (value == null || out.containsKey(name) || out.size() >= MAX_PARAMS) return; + if (!(value instanceof Number || value instanceof Boolean || value instanceof String)) return; + String lower = name.toLowerCase(); + for (String mask : new String[] { "password", "cookie", "token", "secret" }) + { + if (lower.contains(mask)) return; + } + String text = String.valueOf(value); + if (text.length() == 0) return; + out.put(name, text.length() > MAX_PARAM_LEN ? text.substring(0, MAX_PARAM_LEN) + "…" : text); + } + + private String decapitalize(String s) + { + return s == null || s.isEmpty() ? s : Character.toLowerCase(s.charAt(0)) + s.substring(1); + } + + /** oper_param is varchar(2000). Drop whole rows instead of slicing serialized JSON. */ + private String boundedJson(Map payload, List changes) + { + int total = changes.size(); + String json = JSON.toJSONString(payload); + while (json.length() > 2000 && !changes.isEmpty()) + { + changes.remove(changes.size() - 1); + payload.put("truncated", true); + payload.put("omittedChanges", total - changes.size()); + json = JSON.toJSONString(payload); + } + if (json.length() > 2000) + { + payload.put("action", "操作"); + payload.put("target", new LinkedHashMap<>()); + payload.put("truncated", true); + json = JSON.toJSONString(payload); + } + return json; + } + + private void setRequestValue(JoinPoint point, SysOperLog row, String[] exclusions) throws Exception + { + Map params = ServletUtils.getParamMap(ServletUtils.getRequest()); + String method = row.getRequestMethod(); + if (StringUtils.isEmpty(params) && (HttpMethod.PUT.name().equals(method) || HttpMethod.POST.name().equals(method))) + row.setOperParam(StringUtils.substring(argsArrayToString(point.getArgs(), exclusions), 0, 2000)); + else row.setOperParam(StringUtils.substring(JSON.toJSONString(params, excludePropertyPreFilter(exclusions)), 0, 2000)); + } + + private String argsArrayToString(Object[] args, String[] exclusions) + { + StringBuilder value = new StringBuilder(); + if (args != null) for (Object arg : args) + { + if (arg != null && !isFilterObject(arg)) + { + try { value.append(JSON.toJSONString(arg, excludePropertyPreFilter(exclusions))).append(' '); } + catch (Exception ignored) { } + } + } + return value.toString().trim(); + } + + public PropertyPreExcludeFilter excludePropertyPreFilter(String[] exclusions) + { + return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, exclusions)); + } + + @SuppressWarnings("rawtypes") + public boolean isFilterObject(Object value) + { + Class type = value.getClass(); + if (type.isArray()) return MultipartFile.class.isAssignableFrom(type.getComponentType()); + if (value instanceof Collection) for (Object item : (Collection) value) if (item instanceof MultipartFile) return true; + if (value instanceof Map) for (Object item : ((Map) value).values()) if (item instanceof MultipartFile) return true; + return value instanceof MultipartFile || value instanceof HttpServletRequest || value instanceof HttpServletResponse + || value instanceof BindingResult; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java new file mode 100644 index 0000000..b720bc1 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java @@ -0,0 +1,89 @@ +package com.ruoyi.framework.aspectj; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.RateLimiter; +import com.ruoyi.common.enums.LimitType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; + +/** + * 限流处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class RateLimiterAspect +{ + private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); + + private RedisTemplate redisTemplate; + + private RedisScript limitScript; + + @Autowired + public void setRedisTemplate1(RedisTemplate redisTemplate) + { + this.redisTemplate = redisTemplate; + } + + @Autowired + public void setLimitScript(RedisScript limitScript) + { + this.limitScript = limitScript; + } + + @Before("@annotation(rateLimiter)") + public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable + { + int time = rateLimiter.time(); + int count = rateLimiter.count(); + + String combineKey = getCombineKey(rateLimiter, point); + List keys = Collections.singletonList(combineKey); + try + { + Long number = redisTemplate.execute(limitScript, keys, count, time); + if (StringUtils.isNull(number) || number.intValue() > count) + { + throw new ServiceException("访问过于频繁,请稍候再试"); + } + log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey); + } + catch (ServiceException e) + { + throw e; + } + catch (Exception e) + { + throw new RuntimeException("服务器限流异常,请稍候再试"); + } + } + + public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) + { + StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); + if (rateLimiter.limitType() == LimitType.IP) + { + stringBuffer.append(IpUtils.getIpAddr()).append("-"); + } + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Class targetClass = method.getDeclaringClass(); + stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); + return stringBuffer.toString(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RequestLogAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RequestLogAspect.java new file mode 100644 index 0000000..3d624c3 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RequestLogAspect.java @@ -0,0 +1,169 @@ +package com.ruoyi.framework.aspectj; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.enums.HttpMethod; +import com.ruoyi.common.filter.PropertyPreExcludeFilter; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.NamedThreadLocal; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Collection; +import java.util.Map; + +/** + * 操作日志记录处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class RequestLogAspect { + private static final boolean close = false; + private static final Logger log = LoggerFactory.getLogger(RequestLogAspect.class); + + /** + * 排除敏感属性字段 + */ + public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"}; + + /** + * 计算操作消耗时间 + */ + private static final ThreadLocal TIME_THREADLOCAL = new NamedThreadLocal("Cost Time"); + + /** + * 处理请求前执行 + */ + @Before(value = "execution(public * com.ruoyi..*.controller..*.*(..))") + public void boBefore(JoinPoint joinPoint) { + TIME_THREADLOCAL.set(System.currentTimeMillis()); + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "execution(public * com.ruoyi.*.controller..*.*(..))", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) { + handleLog(joinPoint, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "execution(public * com.ruoyi.*.controller..*.*(..))", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + handleLog(joinPoint, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) { + if (close) { + return; + } + try { + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + String call = className + "." + methodName; + var speed = System.currentTimeMillis() - TIME_THREADLOCAL.get(); + var request = getRequestValue(joinPoint, ServletUtils.getRequest().getMethod()); + if (e != null) { + var err = StringUtils.substring(e.getMessage(), 0, 2000); + log.info("Call={} Speed={}ms request={}\n\nerror={}\n", call, speed, request, err); + } else { + var result = JSON.toJSONString(jsonResult); + log.info("Call={} Speed={}ms request={}\n\nresponse={}\n", call, speed, request, result); + } + } catch (Exception exp) { + // 记录本地异常日志 + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } finally { + TIME_THREADLOCAL.remove(); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @throws Exception 异常 + */ + private String getRequestValue(JoinPoint joinPoint, String requestMethod) throws Exception { + Map paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest()); + if (StringUtils.isEmpty(paramsMap) + && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))) { + return argsArrayToString(joinPoint.getArgs()); + } else { + return StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter()), 0, 2000); + } + } + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray) { + String params = ""; + if (paramsArray != null && paramsArray.length > 0) { + for (Object o : paramsArray) { + if (StringUtils.isNotNull(o) && !isFilterObject(o)) { + try { + String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter()); + params += jsonObj.toString() + " "; + } catch (Exception e) { + } + } + } + } + return params.trim(); + } + + /** + * 忽略敏感属性 + */ + public PropertyPreExcludeFilter excludePropertyPreFilter() { + return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES)); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) { + Class clazz = o.getClass(); + if (clazz.isArray()) { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection collection = (Collection) o; + for (Object value : collection) { + return value instanceof MultipartFile; + } + } else if (Map.class.isAssignableFrom(clazz)) { + Map map = (Map) o; + for (Object value : map.entrySet()) { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java new file mode 100644 index 0000000..26451c6 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java @@ -0,0 +1,19 @@ +package com.ruoyi.framework.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * 程序注解配置 + * + * @author ruoyi + */ +@Configuration +// 表示通过aop框架暴露该代理对象,AopContext能够访问 +@EnableAspectJAutoProxy(exposeProxy = true) +// 指定要扫描的Mapper类的包的路径 +@MapperScan("com.ruoyi.**.mapper") +public class ApplicationConfig +{ +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java new file mode 100644 index 0000000..43e78ae --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java @@ -0,0 +1,83 @@ +package com.ruoyi.framework.config; + +import java.util.Properties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author ruoyi + */ +@Configuration +public class CaptchaConfig +{ + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java new file mode 100644 index 0000000..53f1491 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java @@ -0,0 +1,126 @@ +package com.ruoyi.framework.config; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import javax.sql.DataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot4.autoconfigure.DruidDataSourceBuilder; +import com.alibaba.druid.spring.boot4.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.util.Utils; +import com.ruoyi.common.enums.DataSourceType; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.framework.config.properties.DruidProperties; +import com.ruoyi.framework.datasource.DynamicDataSource; + +/** + * druid 配置多数据源 + * + * @author ruoyi + */ +@Configuration +public class DruidConfig +{ + @Bean + @ConfigurationProperties("spring.datasource.druid.master") + public DataSource masterDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean + @ConfigurationProperties("spring.datasource.druid.slave") + @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") + public DataSource slaveDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean(name = "dynamicDataSource") + @Primary + public DynamicDataSource dataSource(DataSource masterDataSource) + { + Map targetDataSources = new HashMap<>(); + targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); + setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); + return new DynamicDataSource(masterDataSource, targetDataSources); + } + + /** + * 设置数据源 + * + * @param targetDataSources 备选数据源集合 + * @param sourceName 数据源名称 + * @param beanName bean名称 + */ + public void setDataSource(Map targetDataSources, String sourceName, String beanName) + { + try + { + DataSource dataSource = SpringUtils.getBean(beanName); + targetDataSources.put(sourceName, dataSource); + } + catch (Exception e) + { + } + } + + /** + * 去除监控页面底部的广告 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") + public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) + { + // 获取web监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取common.js的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + final String filePath = "support/http/resources/js/common.js"; + // 创建filter进行过滤 + Filter filter = new Filter() + { + @Override + public void init(jakarta.servlet.FilterConfig filterConfig) throws ServletException + { + } + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取common.js + String text = Utils.readFromResource(filePath); + // 正则替换banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + @Override + public void destroy() + { + } + }; + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(filter); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..4adbb7f --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java @@ -0,0 +1,52 @@ +package com.ruoyi.framework.config; + +import java.nio.charset.Charset; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import com.ruoyi.common.constant.Constants; + +/** + * Redis使用FastJson序列化 + * + * @author ruoyi + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR); + + private Class clazz; + + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java new file mode 100644 index 0000000..a241903 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java @@ -0,0 +1,90 @@ +package com.ruoyi.framework.config; + +import java.util.HashMap; +import java.util.Map; +import jakarta.servlet.DispatcherType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.ruoyi.common.filter.AesDecryptFilter; +import com.ruoyi.common.filter.RepeatableFilter; +import com.ruoyi.common.filter.XssFilter; +import com.ruoyi.common.utils.StringUtils; + +/** + * Filter配置 + * + * @author ruoyi + */ +@Configuration +public class FilterConfig +{ + @Value("${xss.excludes}") + private String excludes; + + @Value("${xss.urlPatterns}") + private String urlPatterns; + + // ----------------------------- AES 配置 ----------------------------- + @Value("${aes.key}") + private String aesKey; + + @Value("${aes.iv}") + private String aesIv; + // ----------------------------------------------------------------------- + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") + public FilterRegistrationBean xssFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns(StringUtils.split(urlPatterns, ",")); + registration.setName("xssFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + Map initParameters = new HashMap(); + initParameters.put("excludes", excludes); + registration.setInitParameters(initParameters); + return registration; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + public FilterRegistrationBean someFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new RepeatableFilter()); + registration.addUrlPatterns("/*"); + registration.setName("repeatableFilter"); + registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); + return registration; + } + + /** + * AES 解密过滤器 + *

+ * 全局开关:配置文件中设置 {@code aes.enabled=true} 才注册此过滤器。
+ * 调试临时关闭:单次请求却 Header {@code X-Debug-Encrypt: false} 可跳过解密。
+ * 生效范围:/api/*、/promo/*、/admin/*下所有 POST/PUT + application/json 请求全量解密。 + *

+ */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(value = "aes.enabled", havingValue = "true") + public FilterRegistrationBean aesDecryptFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new AesDecryptFilter(aesKey, aesIv)); + // 管理后台(/admin/*)、推广后台(/promo/*)、Cocos 客户端(/api/*)都开启 AES 加密 + registration.addUrlPatterns("/api/*", "/promo/*", "/admin/*"); + registration.setName("aesDecryptFilter"); + // 优先级低于 XSS,高于 RepeatableFilter,确保先解密再过 XSS 过滤 + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 10); + return registration; + } + +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java new file mode 100644 index 0000000..7f8e1d5 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java @@ -0,0 +1,68 @@ +package com.ruoyi.framework.config; + +import java.util.Random; +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +/** + * 验证码文本生成器 + * + * @author ruoyi + */ +public class KaptchaTextCreator extends DefaultTextCreator +{ + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + @Override + public String getText() + { + Integer result = 0; + Random random = new Random(); + int x = random.nextInt(10); + int y = random.nextInt(10); + StringBuilder suChinese = new StringBuilder(); + int randomoperands = random.nextInt(3); + if (randomoperands == 0) + { + result = x * y; + suChinese.append(CNUMBERS[x]); + suChinese.append("*"); + suChinese.append(CNUMBERS[y]); + } + else if (randomoperands == 1) + { + if ((x != 0) && y % x == 0) + { + result = y / x; + suChinese.append(CNUMBERS[y]); + suChinese.append("/"); + suChinese.append(CNUMBERS[x]); + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + } + else + { + if (x >= y) + { + result = x - y; + suChinese.append(CNUMBERS[x]); + suChinese.append("-"); + suChinese.append(CNUMBERS[y]); + } + else + { + result = y - x; + suChinese.append(CNUMBERS[y]); + suChinese.append("-"); + suChinese.append(CNUMBERS[x]); + } + } + suChinese.append("=?@" + result); + return suChinese.toString(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusConfig.java new file mode 100644 index 0000000..7eff4bb --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusConfig.java @@ -0,0 +1,62 @@ +package com.ruoyi.framework.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * Mybatis Plus 配置 + * + * @author ruoyi + */ +@EnableTransactionManagement(proxyTargetClass = true) +@Configuration +public class MybatisPlusConfig +{ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() + { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 分页插件 + interceptor.addInnerInterceptor(paginationInnerInterceptor()); + // 乐观锁插件 + interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor()); + // 阻断插件 + interceptor.addInnerInterceptor(blockAttackInnerInterceptor()); + return interceptor; + } + + /** + * 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html + */ + public PaginationInnerInterceptor paginationInnerInterceptor() + { + PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); + // 设置数据库类型为mysql + paginationInnerInterceptor.setDbType(DbType.MYSQL); + // 设置最大单页限制数量,默认 500 条,-1 不受限制 + paginationInnerInterceptor.setMaxLimit(-1L); + return paginationInnerInterceptor; + } + + /** + * 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html + */ + public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() + { + return new OptimisticLockerInnerInterceptor(); + } + + /** + * 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html + */ + public BlockAttackInnerInterceptor blockAttackInnerInterceptor() + { + return new BlockAttackInnerInterceptor(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java new file mode 100644 index 0000000..ce4df41 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java @@ -0,0 +1,110 @@ +package com.ruoyi.framework.config; + +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +/** + * redis配置 + * + * @author ruoyi + */ +@Configuration +@EnableCaching +public class RedisConfig { + + @Bean + public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { + // 初始化 RedisCacheWriter + RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); + + // 获取默认的缓存配置(可选:这里可以自定义序列化方式) + RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() + // 设置 key 的序列化方式(使用 String 序列化) + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + // 设置 value 的序列化方式(这里使用 JSON 序列化,需确保 FastJson2JsonRedisSerializer 存在) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new FastJson2JsonRedisSerializer<>(Object.class))) + // 默认过期时间(可选,例如 30 分钟) + .entryTtl(Duration.ofMinutes(30)); + + // 返回 RedisCacheManager 实例 + return new RedisCacheManager(redisCacheWriter, defaultCacheConfig); + } + + @Bean + @SuppressWarnings(value = {"unchecked", "rawtypes"}) + public RedisTemplate longRedisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + template.afterPropertiesSet(); + return template; + } + + @Bean + @SuppressWarnings(value = {"unchecked", "rawtypes"}) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + template.afterPropertiesSet(); + return template; + } + + @Bean + public DefaultRedisScript limitScript() + { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(limitScriptText()); + redisScript.setResultType(Long.class); + return redisScript; + } + + /** + * 限流脚本 + */ + private String limitScriptText() + { + return "local key = KEYS[1]\n" + + "local count = tonumber(ARGV[1])\n" + + "local time = tonumber(ARGV[2])\n" + + "local current = redis.call('get', key);\n" + + "if current and tonumber(current) > count then\n" + + " return tonumber(current);\n" + + "end\n" + + "current = redis.call('incr', key)\n" + + "if tonumber(current) == 1 then\n" + + " redis.call('expire', key, time)\n" + + "end\n" + + "return tonumber(current);"; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java new file mode 100644 index 0000000..258bc5c --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java @@ -0,0 +1,120 @@ +package com.ruoyi.framework.config; + +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; +import com.ruoyi.framework.interceptor.UserPermissionInterceptor; + +/** + * 通用配置 + * + * @author ruoyi + */ +@Configuration +public class ResourcesConfig implements WebMvcConfigurer +{ + @Autowired + private RepeatSubmitInterceptor repeatSubmitInterceptor; + + @Autowired + private UserPermissionInterceptor userPermissionInterceptor; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + /** 本地文件上传路径 */ + registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") + .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/"); + + registry.addResourceHandler("/uploads/**") + .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/uploads/"); + + registry.addResourceHandler("/upload/**") + .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/upload/"); + + /** swagger配置 */ + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") + .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic()); + } + + /** + * 自定义拦截规则 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); + registry.addInterceptor(userPermissionInterceptor).addPathPatterns("/**"); + } + + /** + * 配置 Jackson 支持 text/plain 类型,使 @RequestBody 能解析 text/plain 请求体。 + * 注意:StringHttpMessageConverter 必须排在 Jackson 之前,保证 Controller 返回 String 时 + * 输出裸文本(如支付回调应答 SUCCESS),而不是被 Jackson 序列化成带引号的 "SUCCESS", + * 否则第三方支付平台严格比对应答失败会反复重推回调,切勿删除! + */ + @Override + public void configureMessageConverters(List> converters) + { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper); + List supported = new ArrayList<>(converter.getSupportedMediaTypes()); + supported.add(MediaType.TEXT_PLAIN); + converter.setSupportedMediaTypes(supported); + // String 返回值走裸文本输出(不带引号),必须排在 Jackson 之前 + converters.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8)); + converters.add(1, converter); + } + + /** + * 跨域配置 + */ + @Bean + public CorsFilter corsFilter() + { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + // 设置访问源地址 + config.addAllowedOriginPattern("*"); + // 设置访问源请求头 + config.addAllowedHeader("*"); + // 设置访问源请求方法 + config.addAllowedMethod("*"); + // 有效期 1800秒 + config.setMaxAge(1800L); + // 添加映射路径,拦截一切请求 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + // 返回新的CorsFilter + return new CorsFilter(source); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java new file mode 100644 index 0000000..249eacd --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -0,0 +1,157 @@ +package com.ruoyi.framework.config; + +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.framework.config.properties.PermitAllUrlProperties; +import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; +import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; +import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.web.filter.CorsFilter; + +import java.util.function.Supplier; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class SecurityConfig { + @Qualifier("apiUserDetailsServiceImpl") + @Autowired + private UserDetailsService apiUserDetailsService; + + @Autowired + private AuthenticationEntryPointImpl unauthorizedHandler; + + @Autowired + private LogoutSuccessHandlerImpl logoutSuccessHandler; + + @Autowired + private JwtAuthenticationTokenFilter authenticationTokenFilter; + + @Autowired + private CorsFilter corsFilter; + + @Autowired + private PermitAllUrlProperties permitAllUrl; + + /** + * 配置 AuthenticationManager + * 替代原 configure(AuthenticationManagerBuilder auth) 方法 + */ + @Bean + public AuthenticationManager authenticationManager() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(apiUserDetailsService); + provider.setPasswordEncoder(bCryptPasswordEncoder()); + return new ProviderManager(provider); + } + + /** + * 配置 SecurityFilterChain + * 替代原 configure(HttpSecurity http) 方法 + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // CSRF 禁用 + .csrf(AbstractHttpConfigurer::disable) +// 禁用缓存 + .headers(headers -> headers.cacheControl(HeadersConfigurer.CacheControlConfig::disable).frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) + // 异常处理 + .exceptionHandling(exception -> exception + .authenticationEntryPoint(unauthorizedHandler) + // 已登录但身份不允许访问后台接口(例如玩家 token 打 /admin/**)→ 403 + JSON + .accessDeniedHandler((request, response, ex) -> { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":403,\"msg\":\"无权访问后台接口\"}"); + }) + ) + // Session 管理:无状态 + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + // 请求授权配置 + .authorizeHttpRequests(auth -> { + // 1. 动态配置允许匿名访问的 URL (来自 PermitAllUrlProperties) + permitAllUrl.getUrls().forEach(url -> auth.requestMatchers(url).permitAll()); + + // 2. 固定允许匿名访问的 URL + auth.requestMatchers("/login", "/register", "/captchaImage").permitAll() + // 静态资源 + .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", + "/**/*.css", "/**/*.js", "/profile/**", "/uploads/**", "/upload/**").permitAll() + // Swagger & Druid + .requestMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() + // WebSocket + .requestMatchers("/api/websocket/skins").permitAll() + .requestMatchers("/ws/**").permitAll() + // 后台接口:只认「后台 sys_user 账号」的 token(详见 checkAdminUser) + .requestMatchers("/admin/**").access(this::checkAdminUser) + // 其他所有请求都需要认证 + .anyRequest().authenticated(); + }) + // 登出配置 + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessHandler(logoutSuccessHandler) + ); + + // 添加 JWT 过滤器 (在 UsernamePasswordAuthenticationFilter 之前) + http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + + // 添加 CORS 过滤器 + // 注意:在 Spring Security 6 中,通常推荐使用 http.cors(Customizer.withDefaults()) 集成 CorsConfigurationSource + // 这里为了保持原有逻辑,继续手动添加 Filter + http.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); + http.addFilterBefore(corsFilter, LogoutFilter.class); + + return http.build(); + } + + /** + * /admin/** 的访问判定:只允许「后台账号(sys_user 表)登录」的 token。 + * + * 判定依据 LoginUser 的承载对象: + * - 后台登录(SysLoginService → sys_user)→ LoginUser.user 是 SysUser → 放行; + * - 玩家登录(ApiLoginServiceImpl,账号带 api_ 前缀)、推广/卡商登录(promo_ 前缀) + * → LoginUser.userData 是 UserData,LoginUser.user 为 null → 拒绝(403)。 + * 这样即使某个后台 Controller 忘了标 @PreAuthorize,也不会被玩家 token 调用。 + */ + private AuthorizationDecision checkAdminUser(Supplier authentication, + RequestAuthorizationContext context) { + Authentication auth = authentication.get(); + if (auth == null || !auth.isAuthenticated()) { + return new AuthorizationDecision(false); + } + Object principal = auth.getPrincipal(); + boolean isSysUser = principal instanceof LoginUser loginUser && loginUser.getUser() != null; + return new AuthorizationDecision(isSysUser); + } + + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java new file mode 100644 index 0000000..cd8fb09 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java @@ -0,0 +1,32 @@ +package com.ruoyi.framework.config; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 服务相关配置 + * + * @author ruoyi + */ +@Component +public class ServerConfig +{ + /** + * 获取完整的请求路径,包括:域名,端口,上下文访问路径 + * + * @return 服务地址 + */ + public String getUrl() + { + HttpServletRequest request = ServletUtils.getRequest(); + return getDomain(request); + } + + public static String getDomain(HttpServletRequest request) + { + StringBuffer url = request.getRequestURL(); + String contextPath = request.getServletContext().getContextPath(); + return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java new file mode 100644 index 0000000..c6e6588 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java @@ -0,0 +1,108 @@ +package com.ruoyi.framework.config; + +import com.ruoyi.common.utils.Threads; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置 + * + * @author ruoyi + **/ +@Configuration +public class ThreadPoolConfig { + private static final int SCHEDULED_POOL_SIZE = 50; + + private static final int VIRTUAL_THREAD_MAX_CONCURRENCY = 200; + + + @Bean(name = "threadPoolTaskExecutor") + public Executor threadPoolTaskExecutor() { + // 这是一个包装类,用于限制并发量 + return new Executor() { + // 核心限制:允许同时进行的最大并发数 + // 相当于传统线程池的 maxPoolSize,但用的是虚拟线程 + private final Semaphore semaphore = new Semaphore(VIRTUAL_THREAD_MAX_CONCURRENCY); + private final Executor delegate = Executors.newVirtualThreadPerTaskExecutor(); + + @Override + public void execute(Runnable command) { + // 尝试获取许可(非阻塞或超时可配置,这里演示阻塞获取) + // 如果不想阻塞,可以使用 semaphore.tryAcquire() 并处理失败情况 + try { + semaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("线程获取被中断", e); + } + + delegate.execute(() -> { + try { + command.run(); + } finally { + // 任务完成后释放许可 + semaphore.release(); + } + }); + } + }; + } + + /** + * 执行周期性或定时任务 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() { + return new ScheduledThreadPoolExecutor(SCHEDULED_POOL_SIZE, + Thread.ofVirtual().name("schedule-pool-", 0).factory(), + new ThreadPoolExecutor.CallerRunsPolicy()) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + Threads.printException(r, t); + } + }; + } + + /** + * 自定义线程池 + */ + @Bean(name = "customThreadPoolExecutor") + public Executor customThreadPoolExecutor() { + return new Executor() { + // 核心限制:允许同时进行的最大并发数 + // 相当于传统线程池的 maxPoolSize,但用的是虚拟线程 + private final Semaphore semaphore = new Semaphore(VIRTUAL_THREAD_MAX_CONCURRENCY); + private final Executor delegate = Executors.newVirtualThreadPerTaskExecutor(); + + @Override + public void execute(Runnable command) { + // 尝试获取许可(非阻塞或超时可配置,这里演示阻塞获取) + // 如果不想阻塞,可以使用 semaphore.tryAcquire() 并处理失败情况 + try { + semaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("线程获取被中断", e); + } + + delegate.execute(() -> { + try { + command.run(); + } finally { + // 任务完成后释放许可 + semaphore.release(); + } + }); + } + }; + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java new file mode 100644 index 0000000..c8a5c8a --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java @@ -0,0 +1,89 @@ +package com.ruoyi.framework.config.properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import com.alibaba.druid.pool.DruidDataSource; + +/** + * druid 配置属性 + * + * @author ruoyi + */ +@Configuration +public class DruidProperties +{ + @Value("${spring.datasource.druid.initialSize}") + private int initialSize; + + @Value("${spring.datasource.druid.minIdle}") + private int minIdle; + + @Value("${spring.datasource.druid.maxActive}") + private int maxActive; + + @Value("${spring.datasource.druid.maxWait}") + private int maxWait; + + @Value("${spring.datasource.druid.connectTimeout}") + private int connectTimeout; + + @Value("${spring.datasource.druid.socketTimeout}") + private int socketTimeout; + + @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") + private int timeBetweenEvictionRunsMillis; + + @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") + private int minEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") + private int maxEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.validationQuery}") + private String validationQuery; + + @Value("${spring.datasource.druid.testWhileIdle}") + private boolean testWhileIdle; + + @Value("${spring.datasource.druid.testOnBorrow}") + private boolean testOnBorrow; + + @Value("${spring.datasource.druid.testOnReturn}") + private boolean testOnReturn; + + public DruidDataSource dataSource(DruidDataSource datasource) + { + /** 配置初始化大小、最小、最大 */ + datasource.setInitialSize(initialSize); + datasource.setMaxActive(maxActive); + datasource.setMinIdle(minIdle); + + /** 配置获取连接等待超时的时间 */ + datasource.setMaxWait(maxWait); + + /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */ + datasource.setConnectTimeout(connectTimeout); + + /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */ + datasource.setSocketTimeout(socketTimeout); + + /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ + datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + + /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ + datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); + + /** + * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 + */ + datasource.setValidationQuery(validationQuery); + /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ + datasource.setTestWhileIdle(testWhileIdle); + /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnBorrow(testOnBorrow); + /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnReturn(testOnReturn); + return datasource; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java new file mode 100644 index 0000000..00b3d74 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java @@ -0,0 +1,69 @@ +package com.ruoyi.framework.config.properties; + +import com.ruoyi.common.annotation.Anonymous; +import org.apache.commons.lang3.RegExUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * 设置Anonymous注解允许匿名访问的url + * + * @author ruoyi + */ +@Configuration +public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware { + private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); + + private ApplicationContext applicationContext; + + private List urls = new ArrayList<>(); + + public String ASTERISK = "*"; + + @Override + public void afterPropertiesSet() { + RequestMappingHandlerMapping mapping = applicationContext.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class); + Map map = mapping.getHandlerMethods(); + + map.keySet().forEach(info -> { + HandlerMethod handlerMethod = map.get(info); + + // 获取方法上边的注解 替代path variable 为 * + Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class); + Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatterns()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url.getPatternString(), PATTERN.pattern(), ASTERISK)))); + + // 获取类上边的注解, 替代path variable 为 * + Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class); + Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatterns()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url.getPatternString(), PATTERN.pattern(), ASTERISK)))); + }); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.applicationContext = context; + } + + public List getUrls() { + return urls; + } + + public void setUrls(List urls) { + this.urls = urls; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java new file mode 100644 index 0000000..e70b8cf --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java @@ -0,0 +1,26 @@ +package com.ruoyi.framework.datasource; + +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +/** + * 动态数据源 + * + * @author ruoyi + */ +public class DynamicDataSource extends AbstractRoutingDataSource +{ + public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) + { + super.setDefaultTargetDataSource(defaultTargetDataSource); + super.setTargetDataSources(targetDataSources); + super.afterPropertiesSet(); + } + + @Override + protected Object determineCurrentLookupKey() + { + return DynamicDataSourceContextHolder.getDataSourceType(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java new file mode 100644 index 0000000..9770af6 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java @@ -0,0 +1,45 @@ +package com.ruoyi.framework.datasource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 数据源切换处理 + * + * @author ruoyi + */ +public class DynamicDataSourceContextHolder +{ + public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); + + /** + * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, + * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 + */ + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源的变量 + */ + public static void setDataSourceType(String dsType) + { + log.info("切换到{}数据源", dsType); + CONTEXT_HOLDER.set(dsType); + } + + /** + * 获得数据源的变量 + */ + public static String getDataSourceType() + { + return CONTEXT_HOLDER.get(); + } + + /** + * 清空数据源变量 + */ + public static void clearDataSourceType() + { + CONTEXT_HOLDER.remove(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java new file mode 100644 index 0000000..4e8d20f --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java @@ -0,0 +1,56 @@ +package com.ruoyi.framework.interceptor; + +import java.lang.reflect.Method; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 防止重复提交拦截器 + * + * @author ruoyi + */ +@Component +public abstract class RepeatSubmitInterceptor implements HandlerInterceptor +{ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + if (handler instanceof HandlerMethod) + { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); + if (annotation != null) + { + if (this.isRepeatSubmit(request, annotation)) + { + AjaxResult ajaxResult = AjaxResult.error(annotation.message()); + ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); + return false; + } + } + return true; + } + else + { + return true; + } + } + + /** + * 验证是否重复提交由子类实现具体的防重复提交的规则 + * + * @param request 请求信息 + * @param annotation 防重复注解参数 + * @return 结果 + * @throws Exception + */ + public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/UserPermissionInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/UserPermissionInterceptor.java new file mode 100644 index 0000000..06c3f0e --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/UserPermissionInterceptor.java @@ -0,0 +1,42 @@ +package com.ruoyi.framework.interceptor; + +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.utils.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * P1#8: @UserPermission 注解拦截器——检查标注了 @UserPermission 的方法是否用户已登录。 + * 此前注解在 10+ 个支付 Controller 上标注但无任何 Filter/Interceptor 读取,形同虚设。 + */ +@Component +public class UserPermissionInterceptor implements HandlerInterceptor { + private static final Logger log = LoggerFactory.getLogger(UserPermissionInterceptor.class); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (!(handler instanceof HandlerMethod)) return true; + HandlerMethod handlerMethod = (HandlerMethod) handler; + UserPermission permission = handlerMethod.getMethodAnnotation(UserPermission.class); + if (permission == null) return true; + // 检查用户是否登录 + try { + Long userId = SecurityUtils.getUserId(); + if (userId == null) { + response.setStatus(401); + return false; + } + } catch (Exception e) { + log.warn("[UserPermission] 未登录访问受保护接口: {}", request.getRequestURI()); + response.setStatus(401); + return false; + } + return true; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java new file mode 100644 index 0000000..c93df50 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java @@ -0,0 +1,110 @@ +package com.ruoyi.framework.interceptor.impl; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.filter.RepeatedlyRequestWrapper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpHelper; +import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 判断请求url和数据是否和上一次相同, + * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 + * + * @author ruoyi + */ +@Component +public class SameUrlDataInterceptor extends RepeatSubmitInterceptor +{ + public final String REPEAT_PARAMS = "repeatParams"; + + public final String REPEAT_TIME = "repeatTime"; + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + @Autowired + private RedisCache redisCache; + + @SuppressWarnings("unchecked") + @Override + public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) + { + String nowParams = ""; + if (request instanceof RepeatedlyRequestWrapper) + { + RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; + nowParams = HttpHelper.getBodyString(repeatedlyRequest); + } + + // body参数为空,获取Parameter的数据 + if (StringUtils.isEmpty(nowParams)) + { + nowParams = JSON.toJSONString(request.getParameterMap()); + } + Map nowDataMap = new HashMap(); + nowDataMap.put(REPEAT_PARAMS, nowParams); + nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); + + // 请求地址(作为存放cache的key值) + String url = request.getRequestURI(); + + // 唯一值(没有消息头则使用请求地址) + String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); + + // 唯一标识(指定key + url + 消息头) + String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey; + + Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); + if (sessionObj != null) + { + Map sessionMap = (Map) sessionObj; + if (sessionMap.containsKey(url)) + { + Map preDataMap = (Map) sessionMap.get(url); + if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) + { + return true; + } + } + } + Map cacheMap = new HashMap(); + cacheMap.put(url, nowDataMap); + redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); + return false; + } + + /** + * 判断参数是否相同 + */ + private boolean compareParams(Map nowMap, Map preMap) + { + String nowParams = (String) nowMap.get(REPEAT_PARAMS); + String preParams = (String) preMap.get(REPEAT_PARAMS); + return nowParams.equals(preParams); + } + + /** + * 判断两次间隔时间 + */ + private boolean compareTime(Map nowMap, Map preMap, int interval) + { + long time1 = (Long) nowMap.get(REPEAT_TIME); + long time2 = (Long) preMap.get(REPEAT_TIME); + if ((time1 - time2) < interval) + { + return true; + } + return false; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java new file mode 100644 index 0000000..3f5e478 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java @@ -0,0 +1,82 @@ +package com.ruoyi.framework.manager; + +import com.ruoyi.common.utils.Threads; +import com.ruoyi.common.utils.spring.SpringUtils; + +import java.util.TimerTask; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +/** + * 异步任务管理器 + * + * @author ruoyi + */ +public class AsyncManager { + /** + * 操作延迟10毫秒 + */ + private final int OPERATE_DELAY_TIME = 10; + + /** + * 异步操作任务调度线程池 + */ + private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService"); + + /** + * 虚拟线程池 + */ + private final Executor virtualThreadExecutor = SpringUtils.getBean("threadPoolTaskExecutor"); + + /** + * 单例模式 + */ + private AsyncManager() { + } + + private static AsyncManager me = new AsyncManager(); + + public static AsyncManager me() { + return me; + } + + /** + * 执行任务 + * + * @param task 任务 + */ + public void execute(TimerTask task) { + executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); + } + public void timeout(TimerTask task, long delay, TimeUnit unit) { + executor.schedule(task, delay, unit); + } + + public void run(Runnable runnable) { + CompletableFuture.runAsync(runnable, virtualThreadExecutor); + } + + public CompletableFuture supply(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier, virtualThreadExecutor); + } + + public static U getMs(CompletableFuture cf, long timeout) { + try { + return cf.get(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + } + + /** + * 停止任务线程池 + */ + public void shutdown() { + Threads.shutdownAndAwaitTermination(executor); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java new file mode 100644 index 0000000..095b865 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java @@ -0,0 +1,39 @@ +package com.ruoyi.framework.manager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import jakarta.annotation.PreDestroy; + +/** + * 确保应用退出时能关闭后台线程 + * + * @author ruoyi + */ +@Component +public class ShutdownManager +{ + private static final Logger logger = LoggerFactory.getLogger("sys-user"); + + @PreDestroy + public void destroy() + { + shutdownAsyncManager(); + } + + /** + * 停止异步执行任务 + */ + private void shutdownAsyncManager() + { + try + { + logger.info("====关闭后台任务任务线程池===="); + AsyncManager.me().shutdown(); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java new file mode 100644 index 0000000..267e305 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java @@ -0,0 +1,102 @@ +package com.ruoyi.framework.manager.factory; + +import java.util.TimerTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.LogUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.AddressUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.service.ISysLogininforService; +import com.ruoyi.system.service.ISysOperLogService; +import eu.bitwalker.useragentutils.UserAgent; + +/** + * 异步工厂(产生任务用) + * + * @author ruoyi + */ +public class AsyncFactory +{ + private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param args 列表 + * @return 任务task + */ + public static TimerTask recordLogininfor(final String username, final String status, final String message, + final Object... args) + { + final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + final String ip = IpUtils.getIpAddr(); + return new TimerTask() + { + @Override + public void run() + { + String address = AddressUtils.getRealAddressByIP(ip); + StringBuilder s = new StringBuilder(); + s.append(LogUtils.getBlock(ip)); + s.append(address); + s.append(LogUtils.getBlock(username)); + s.append(LogUtils.getBlock(status)); + s.append(LogUtils.getBlock(message)); + // 打印信息到日志 + sys_user_logger.info(s.toString(), args); + // 获取客户端操作系统 + String os = userAgent.getOperatingSystem().getName(); + // 获取客户端浏览器 + String browser = userAgent.getBrowser().getName(); + // 封装对象 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(ip); + logininfor.setLoginLocation(address); + logininfor.setBrowser(browser); + logininfor.setOs(os); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) + { + logininfor.setStatus(Constants.SUCCESS); + } + else if (Constants.LOGIN_FAIL.equals(status)) + { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor); + } + }; + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + * @return 任务task + */ + public static TimerTask recordOper(final SysOperLog operLog) + { + return new TimerTask() + { + @Override + public void run() + { + // 远程查询操作地点 + operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp())); + SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog); + } + }; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java new file mode 100644 index 0000000..6c776ce --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java @@ -0,0 +1,28 @@ +package com.ruoyi.framework.security.context; + +import org.springframework.security.core.Authentication; + +/** + * 身份验证信息 + * + * @author ruoyi + */ +public class AuthenticationContextHolder +{ + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static Authentication getContext() + { + return contextHolder.get(); + } + + public static void setContext(Authentication context) + { + contextHolder.set(context); + } + + public static void clearContext() + { + contextHolder.remove(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java new file mode 100644 index 0000000..5472f3d --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java @@ -0,0 +1,27 @@ +package com.ruoyi.framework.security.context; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import com.ruoyi.common.core.text.Convert; + +/** + * 权限信息 + * + * @author ruoyi + */ +public class PermissionContextHolder +{ + private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT"; + + public static void setContext(String permission) + { + RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission, + RequestAttributes.SCOPE_REQUEST); + } + + public static String getContext() + { + return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES, + RequestAttributes.SCOPE_REQUEST)); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..34444a6 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,72 @@ +package com.ruoyi.framework.security.filter; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.TokenService; + +/** + * token过滤器 验证token有效性 + * + * @author ruoyi + */ +@Slf4j +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter +{ + @Autowired + private TokenService tokenService; + + @Autowired + private RedisCache redisCache; + + /** + * 心跳排除名单:前端定时器自动轮询的接口(维护状态 2.5s/次、余额刷新、邮件红点、系统公告跑马灯), + * 不代表玩家主动操作,不刷新在线心跳,避免挂机用户永远显示在线 + */ + private static final Set HEARTBEAT_EXCLUDE_URIS = Set.of( + "/api/websiteSetup/siteMaintenance", + "/api/getAccountAmount", + "/api/announcement/countUnreadAnnouncement", + "/api/announcement/systemList"); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException + { + log.debug("请求信息:{}:{}{}",request.getRemoteAddr(),request.getLocalPort(),request.getRequestURI()); + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) + { + tokenService.verifyToken(loginUser); + // 玩家主动请求刷新在线心跳,供在线用户列表按活跃度过滤;自动轮询接口不续心跳 + String servletPath = request.getRequestURI().substring(request.getContextPath().length()); + if (!HEARTBEAT_EXCLUDE_URIS.contains(servletPath)) + { + redisCache.setCacheObject(CacheConstants.ONLINE_HEARTBEAT_KEY + loginUser.getToken(), "1", + CacheConstants.ONLINE_HEARTBEAT_EXPIRE_MINUTES, TimeUnit.MINUTES); + } + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + chain.doFilter(request, response); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..e1789f8 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java @@ -0,0 +1,34 @@ +package com.ruoyi.framework.security.handle; + +import java.io.IOException; +import java.io.Serializable; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 认证失败处理类 返回未授权 + * + * @author ruoyi + */ +@Component +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable +{ + private static final long serialVersionUID = -8970718410437077606L; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException + { + int code = HttpStatus.UNAUTHORIZED; + String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java new file mode 100644 index 0000000..9e4380e --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -0,0 +1,52 @@ +package com.ruoyi.framework.security.handle; + +import java.io.IOException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.web.service.TokenService; + +/** + * 自定义退出处理类 返回成功 + * + * @author ruoyi + */ +@Configuration +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler +{ + @Autowired + private TokenService tokenService; + + /** + * 退出处理 + * + * @return + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser)) + { + String userName = loginUser.getUsername(); + // 删除用户缓存记录 + tokenService.delLoginUser(loginUser.getToken()); + // 记录用户退出日志 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功")); + } + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功"))); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java new file mode 100644 index 0000000..63b03da --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java @@ -0,0 +1,240 @@ +package com.ruoyi.framework.web.domain; + +import java.net.UnknownHostException; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import com.ruoyi.common.utils.Arith; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.web.domain.server.Cpu; +import com.ruoyi.framework.web.domain.server.Jvm; +import com.ruoyi.framework.web.domain.server.Mem; +import com.ruoyi.framework.web.domain.server.Sys; +import com.ruoyi.framework.web.domain.server.SysFile; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.CentralProcessor.TickType; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.FileSystem; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.Util; + +/** + * 服务器相关信息 + * + * @author ruoyi + */ +public class Server +{ + private static final int OSHI_WAIT_SECOND = 1000; + + /** + * CPU相关信息 + */ + private Cpu cpu = new Cpu(); + + /** + * 內存相关信息 + */ + private Mem mem = new Mem(); + + /** + * JVM相关信息 + */ + private Jvm jvm = new Jvm(); + + /** + * 服务器相关信息 + */ + private Sys sys = new Sys(); + + /** + * 磁盘相关信息 + */ + private List sysFiles = new LinkedList(); + + public Cpu getCpu() + { + return cpu; + } + + public void setCpu(Cpu cpu) + { + this.cpu = cpu; + } + + public Mem getMem() + { + return mem; + } + + public void setMem(Mem mem) + { + this.mem = mem; + } + + public Jvm getJvm() + { + return jvm; + } + + public void setJvm(Jvm jvm) + { + this.jvm = jvm; + } + + public Sys getSys() + { + return sys; + } + + public void setSys(Sys sys) + { + this.sys = sys; + } + + public List getSysFiles() + { + return sysFiles; + } + + public void setSysFiles(List sysFiles) + { + this.sysFiles = sysFiles; + } + + public void copyTo() throws Exception + { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + + setCpuInfo(hal.getProcessor()); + + setMemInfo(hal.getMemory()); + + setSysInfo(); + + setJvmInfo(); + + setSysFiles(si.getOperatingSystem()); + } + + /** + * 设置CPU信息 + */ + private void setCpuInfo(CentralProcessor processor) + { + // CPU信息 + long[] prevTicks = processor.getSystemCpuLoadTicks(); + Util.sleep(OSHI_WAIT_SECOND); + long[] ticks = processor.getSystemCpuLoadTicks(); + long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; + long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; + long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; + long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; + long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; + long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; + long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; + long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + cpu.setCpuNum(processor.getLogicalProcessorCount()); + cpu.setTotal(totalCpu); + cpu.setSys(cSys); + cpu.setUsed(user); + cpu.setWait(iowait); + cpu.setFree(idle); + } + + /** + * 设置内存信息 + */ + private void setMemInfo(GlobalMemory memory) + { + mem.setTotal(memory.getTotal()); + mem.setUsed(memory.getTotal() - memory.getAvailable()); + mem.setFree(memory.getAvailable()); + } + + /** + * 设置服务器信息 + */ + private void setSysInfo() + { + Properties props = System.getProperties(); + sys.setComputerName(IpUtils.getHostName()); + sys.setComputerIp(IpUtils.getHostIp()); + sys.setOsName(props.getProperty("os.name")); + sys.setOsArch(props.getProperty("os.arch")); + sys.setUserDir(props.getProperty("user.dir")); + } + + /** + * 设置Java虚拟机 + */ + private void setJvmInfo() throws UnknownHostException + { + Properties props = System.getProperties(); + jvm.setTotal(Runtime.getRuntime().totalMemory()); + jvm.setMax(Runtime.getRuntime().maxMemory()); + jvm.setFree(Runtime.getRuntime().freeMemory()); + jvm.setVersion(props.getProperty("java.version")); + jvm.setHome(props.getProperty("java.home")); + } + + /** + * 设置磁盘信息 + */ + private void setSysFiles(OperatingSystem os) + { + FileSystem fileSystem = os.getFileSystem(); + List fsArray = fileSystem.getFileStores(); + for (OSFileStore fs : fsArray) + { + long free = fs.getUsableSpace(); + long total = fs.getTotalSpace(); + long used = total - free; + SysFile sysFile = new SysFile(); + sysFile.setDirName(fs.getMount()); + sysFile.setSysTypeName(fs.getType()); + sysFile.setTypeName(fs.getName()); + sysFile.setTotal(convertFileSize(total)); + sysFile.setFree(convertFileSize(free)); + sysFile.setUsed(convertFileSize(used)); + sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100)); + sysFiles.add(sysFile); + } + } + + /** + * 字节转换 + * + * @param size 字节大小 + * @return 转换后值 + */ + public String convertFileSize(long size) + { + long kb = 1024; + long mb = kb * 1024; + long gb = mb * 1024; + if (size >= gb) + { + return String.format("%.1f GB", (float) size / gb); + } + else if (size >= mb) + { + float f = (float) size / mb; + return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f); + } + else if (size >= kb) + { + float f = (float) size / kb; + return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f); + } + else + { + return String.format("%d B", size); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java new file mode 100644 index 0000000..a13a66c --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java @@ -0,0 +1,101 @@ +package com.ruoyi.framework.web.domain.server; + +import com.ruoyi.common.utils.Arith; + +/** + * CPU相关信息 + * + * @author ruoyi + */ +public class Cpu +{ + /** + * 核心数 + */ + private int cpuNum; + + /** + * CPU总的使用率 + */ + private double total; + + /** + * CPU系统使用率 + */ + private double sys; + + /** + * CPU用户使用率 + */ + private double used; + + /** + * CPU当前等待率 + */ + private double wait; + + /** + * CPU当前空闲率 + */ + private double free; + + public int getCpuNum() + { + return cpuNum; + } + + public void setCpuNum(int cpuNum) + { + this.cpuNum = cpuNum; + } + + public double getTotal() + { + return Arith.round(Arith.mul(total, 100), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getSys() + { + return Arith.round(Arith.mul(sys / total, 100), 2); + } + + public void setSys(double sys) + { + this.sys = sys; + } + + public double getUsed() + { + return Arith.round(Arith.mul(used / total, 100), 2); + } + + public void setUsed(double used) + { + this.used = used; + } + + public double getWait() + { + return Arith.round(Arith.mul(wait / total, 100), 2); + } + + public void setWait(double wait) + { + this.wait = wait; + } + + public double getFree() + { + return Arith.round(Arith.mul(free / total, 100), 2); + } + + public void setFree(double free) + { + this.free = free; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java new file mode 100644 index 0000000..1fdc6ac --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java @@ -0,0 +1,130 @@ +package com.ruoyi.framework.web.domain.server; + +import java.lang.management.ManagementFactory; +import com.ruoyi.common.utils.Arith; +import com.ruoyi.common.utils.DateUtils; + +/** + * JVM相关信息 + * + * @author ruoyi + */ +public class Jvm +{ + /** + * 当前JVM占用的内存总数(M) + */ + private double total; + + /** + * JVM最大可用内存总数(M) + */ + private double max; + + /** + * JVM空闲内存(M) + */ + private double free; + + /** + * JDK版本 + */ + private String version; + + /** + * JDK路径 + */ + private String home; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getMax() + { + return Arith.div(max, (1024 * 1024), 2); + } + + public void setMax(double max) + { + this.max = max; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024), 2); + } + + public void setFree(double free) + { + this.free = free; + } + + public double getUsed() + { + return Arith.div(total - free, (1024 * 1024), 2); + } + + public double getUsage() + { + return Arith.mul(Arith.div(total - free, total, 4), 100); + } + + /** + * 获取JDK名称 + */ + public String getName() + { + return ManagementFactory.getRuntimeMXBean().getVmName(); + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public String getHome() + { + return home; + } + + public void setHome(String home) + { + this.home = home; + } + + /** + * JDK启动时间 + */ + public String getStartTime() + { + return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate()); + } + + /** + * JDK运行时间 + */ + public String getRunTime() + { + return DateUtils.timeDistance(DateUtils.getNowDate(), DateUtils.getServerStartDate()); + } + + /** + * 运行参数 + */ + public String getInputArgs() + { + return ManagementFactory.getRuntimeMXBean().getInputArguments().toString(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java new file mode 100644 index 0000000..13eec52 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java @@ -0,0 +1,61 @@ +package com.ruoyi.framework.web.domain.server; + +import com.ruoyi.common.utils.Arith; + +/** + * 內存相关信息 + * + * @author ruoyi + */ +public class Mem +{ + /** + * 内存总量 + */ + private double total; + + /** + * 已用内存 + */ + private double used; + + /** + * 剩余内存 + */ + private double free; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024 * 1024), 2); + } + + public void setTotal(long total) + { + this.total = total; + } + + public double getUsed() + { + return Arith.div(used, (1024 * 1024 * 1024), 2); + } + + public void setUsed(long used) + { + this.used = used; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024 * 1024), 2); + } + + public void setFree(long free) + { + this.free = free; + } + + public double getUsage() + { + return Arith.mul(Arith.div(used, total, 4), 100); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java new file mode 100644 index 0000000..45d64d9 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java @@ -0,0 +1,84 @@ +package com.ruoyi.framework.web.domain.server; + +/** + * 系统相关信息 + * + * @author ruoyi + */ +public class Sys +{ + /** + * 服务器名称 + */ + private String computerName; + + /** + * 服务器Ip + */ + private String computerIp; + + /** + * 项目路径 + */ + private String userDir; + + /** + * 操作系统 + */ + private String osName; + + /** + * 系统架构 + */ + private String osArch; + + public String getComputerName() + { + return computerName; + } + + public void setComputerName(String computerName) + { + this.computerName = computerName; + } + + public String getComputerIp() + { + return computerIp; + } + + public void setComputerIp(String computerIp) + { + this.computerIp = computerIp; + } + + public String getUserDir() + { + return userDir; + } + + public void setUserDir(String userDir) + { + this.userDir = userDir; + } + + public String getOsName() + { + return osName; + } + + public void setOsName(String osName) + { + this.osName = osName; + } + + public String getOsArch() + { + return osArch; + } + + public void setOsArch(String osArch) + { + this.osArch = osArch; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java new file mode 100644 index 0000000..1320cde --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java @@ -0,0 +1,114 @@ +package com.ruoyi.framework.web.domain.server; + +/** + * 系统文件相关信息 + * + * @author ruoyi + */ +public class SysFile +{ + /** + * 盘符路径 + */ + private String dirName; + + /** + * 盘符类型 + */ + private String sysTypeName; + + /** + * 文件类型 + */ + private String typeName; + + /** + * 总大小 + */ + private String total; + + /** + * 剩余大小 + */ + private String free; + + /** + * 已经使用量 + */ + private String used; + + /** + * 资源的使用率 + */ + private double usage; + + public String getDirName() + { + return dirName; + } + + public void setDirName(String dirName) + { + this.dirName = dirName; + } + + public String getSysTypeName() + { + return sysTypeName; + } + + public void setSysTypeName(String sysTypeName) + { + this.sysTypeName = sysTypeName; + } + + public String getTypeName() + { + return typeName; + } + + public void setTypeName(String typeName) + { + this.typeName = typeName; + } + + public String getTotal() + { + return total; + } + + public void setTotal(String total) + { + this.total = total; + } + + public String getFree() + { + return free; + } + + public void setFree(String free) + { + this.free = free; + } + + public String getUsed() + { + return used; + } + + public void setUsed(String used) + { + this.used = used; + } + + public double getUsage() + { + return usage; + } + + public void setUsage(double usage) + { + this.usage = usage; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..86374db --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,165 @@ +package com.ruoyi.framework.web.exception; + +import jakarta.servlet.http.HttpServletRequest; + +import com.ruoyi.common.exception.user.UserPasswordNotMatchException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.exception.DemoModeException; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; + +/** + * 全局异常处理器 + * + * @author ruoyi + */ +@RestControllerAdvice +public class GlobalExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 权限校验异常 + */ + @ExceptionHandler(AccessDeniedException.class) + public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权"); + } + + /** + * 密码错误异常 + */ + @ExceptionHandler(UserPasswordNotMatchException.class) + public AjaxResult handleUserPasswordNotMatchException(UserPasswordNotMatchException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',错误信息:'{}'", requestURI, e.getMessage()); + return AjaxResult.error(500, "账号/密码错误。"); + } + + /** + * 请求方式不支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + return AjaxResult.error(e.getMessage()); + } + + /** + * 请求参数不支持 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public AjaxResult handleHttpMessageNotReadableException(HttpMessageNotReadableException e, + HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',检查请求参数是否正确", requestURI); + return AjaxResult.error(e.getMessage()); + } + + + /** + * 业务异常 + */ + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) + { + log.error(e.getMessage()); + Integer code = e.getCode(); + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 请求路径中缺少必需的路径变量 + */ + @ExceptionHandler(MissingPathVariableException.class) + public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName())); + } + + /** + * 请求参数类型不匹配 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求参数{}类型不匹配'{}',发生系统异常.",request.getQueryString(), requestURI); + return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getParameter(), e.getRequiredType().getName(), e.getValue())); + } + + /** + * 拦截未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'%s',发生未知异常. e is %s", requestURI, e.getMessage(), e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) + { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) + { + log.error(e.getMessage()); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 演示模式异常 + */ + @ExceptionHandler(DemoModeException.class) + public AjaxResult handleDemoModeException(DemoModeException e) + { + return AjaxResult.error("演示模式,不允许操作"); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java new file mode 100644 index 0000000..07d259a --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java @@ -0,0 +1,159 @@ +package com.ruoyi.framework.web.service; + +import java.util.Set; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.PermissionContextHolder; + +/** + * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母 + * + * @author ruoyi + */ +@Service("ss") +public class PermissionService +{ + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(String permission) + { + if (StringUtils.isEmpty(permission)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permission); + return hasPermissions(loginUser.getPermissions(), permission); + } + + /** + * 验证用户是否不具备某权限,与 hasPermi逻辑相反 + * + * @param permission 权限字符串 + * @return 用户是否不具备某权限 + */ + public boolean lacksPermi(String permission) + { + return hasPermi(permission) != true; + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以 PERMISSION_DELIMETER 为分隔符的权限列表 + * @return 用户是否具有以下任意一个权限 + */ + public boolean hasAnyPermi(String permissions) + { + if (StringUtils.isEmpty(permissions)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permissions); + Set authorities = loginUser.getPermissions(); + for (String permission : permissions.split(Constants.PERMISSION_DELIMETER)) + { + if (permission != null && hasPermissions(authorities, permission)) + { + return true; + } + } + return false; + } + + /** + * 判断用户是否拥有某个角色 + * + * @param role 角色字符串 + * @return 用户是否具备某角色 + */ + public boolean hasRole(String role) + { + if (StringUtils.isEmpty(role)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (SysRole sysRole : loginUser.getUser().getRoles()) + { + String roleKey = sysRole.getRoleKey(); + if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) + { + return true; + } + } + return false; + } + + /** + * 验证用户是否不具备某角色,与 isRole逻辑相反。 + * + * @param role 角色名称 + * @return 用户是否不具备某角色 + */ + public boolean lacksRole(String role) + { + return hasRole(role) != true; + } + + /** + * 验证用户是否具有以下任意一个角色 + * + * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表 + * @return 用户是否具有以下任意一个角色 + */ + public boolean hasAnyRoles(String roles) + { + if (StringUtils.isEmpty(roles)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (String role : roles.split(Constants.ROLE_DELIMETER)) + { + if (hasRole(role)) + { + return true; + } + } + return false; + } + + /** + * 判断是否包含权限 + * + * @param permissions 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + private boolean hasPermissions(Set permissions, String permission) + { + return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java new file mode 100644 index 0000000..3cf72a0 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -0,0 +1,191 @@ +package com.ruoyi.framework.web.service; + +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.exception.user.*; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * 登录校验方法 + * + * @author ruoyi + */ +@Slf4j +@Component +public class SysLoginService +{ + @Autowired + private TokenService tokenService; + + @Resource + private AuthenticationManager authenticationManager; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + /** + * 登录验证 + * + * @param username 用户名 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public String login(String username, String password, String code, String uuid) + { + // 验证码校验 + validateCaptcha(username, code, uuid); + // 登录前置校验 + loginPreCheck(username, password); + // 生成登录用户信息 + LoginUser loginUser = createLoginUser(username, password); + // 记录登录信息 + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + } + + /** + * 生成登录用户信息 + * + * @param username 用户名 + * @param password 密码 + * @return 结果 + */ + public LoginUser createLoginUser(String username, String password) { + // 用户验证 + Authentication authentication = null; + try { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + AuthenticationContextHolder.setContext(authenticationToken); + // 该方法会去调用ApiUserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager.authenticate(authenticationToken); + } catch (Exception e) { + log.error("验证错误", e); + if (e instanceof BadCredentialsException) { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } else { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + } + finally { + AuthenticationContextHolder.clearContext(); + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + return (LoginUser) authentication.getPrincipal(); + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + throw new CaptchaException(); + } + } + } + + /** + * 登录前置校验 + * @param username 用户名 + * @param password 用户密码 + */ + public void loginPreCheck(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))); + throw new UserNotExistsException(); + } + // 密码如果不在指定范围内 错误 + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // 用户名不在指定范围内 错误 + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + validateBlackIp(username); + } + + public void validateBlackIp(String username) + { + String blackStr = configService.selectConfigByKey("sys.login.blackIPList"); + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked"))); + throw new BlackListException(); + } + } + + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) + { + SysUser sysUser = new SysUser(); + sysUser.setUserId(userId); + sysUser.setLoginIp(IpUtils.getIpAddr()); + sysUser.setLoginDate(DateUtils.getNowDate()); + userService.updateUserProfile(sysUser); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java new file mode 100644 index 0000000..6ad91b0 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java @@ -0,0 +1,94 @@ +package com.ruoyi.framework.web.service; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.user.UserPasswordNotMatchException; +import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; + +/** + * 登录密码方法 + * + * @author ruoyi + */ +@Component +public class SysPasswordService +{ + @Autowired + private RedisCache redisCache; + + @Value(value = "${user.password.maxRetryCount}") + private int maxRetryCount; + + @Value(value = "${user.password.lockTime}") + private int lockTime; + + /** + * 登录账户密码错误次数缓存键名 + * + * @param username 用户名 + * @return 缓存键key + */ + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + public void validate(SysUser user) + { + Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); + String username = usernamePasswordAuthenticationToken.getName(); + String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime))); + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if (!matches(user, password)) + { + retryCount = retryCount + 1; + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.count", retryCount))); + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } + else + { + clearLoginRecordCache(username); + } + } + + public boolean matches(SysUser user, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + public void clearLoginRecordCache(String loginName) + { + if (redisCache.hasKey(getCacheKey(loginName))) + { + redisCache.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java new file mode 100644 index 0000000..d1fb4ed --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java @@ -0,0 +1,83 @@ +package com.ruoyi.framework.web.service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.system.service.ISysMenuService; +import com.ruoyi.system.service.ISysRoleService; + +/** + * 用户权限处理 + * + * @author ruoyi + */ +@Component +public class SysPermissionService +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysMenuService menuService; + + /** + * 获取角色数据权限 + * + * @param user 用户信息 + * @return 角色权限信息 + */ + public Set getRolePermission(SysUser user) + { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + roles.add("admin"); + } + else + { + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取菜单数据权限 + * + * @param user 用户信息 + * @return 菜单权限信息 + */ + public Set getMenuPermission(SysUser user) + { + Set perms = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + perms.add("*:*:*"); + } + else + { + List roles = user.getRoles(); + if (!CollectionUtils.isEmpty(roles)) + { + // 多角色设置permissions属性,以便数据权限匹配权限 + for (SysRole role : roles) + { + Set rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); + role.setPermissions(rolePerms); + perms.addAll(rolePerms); + } + } + else + { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + } + return perms; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java new file mode 100644 index 0000000..f2afe31 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java @@ -0,0 +1,115 @@ +package com.ruoyi.framework.web.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.RegisterBody; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.user.CaptchaException; +import com.ruoyi.common.exception.user.CaptchaExpireException; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 注册校验方法 + * + * @author ruoyi + */ +@Component +public class SysRegisterService +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private RedisCache redisCache; + + /** + * 注册 + */ + public String register(RegisterBody registerBody) + { + String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword(); + SysUser sysUser = new SysUser(); + sysUser.setUserName(username); + + // 验证码开关 + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + validateCaptcha(username, registerBody.getCode(), registerBody.getUuid()); + } + + if (StringUtils.isEmpty(username)) + { + msg = "用户名不能为空"; + } + else if (StringUtils.isEmpty(password)) + { + msg = "用户密码不能为空"; + } + else if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + msg = "账户长度必须在2到20个字符之间"; + } + else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + msg = "密码长度必须在5到20个字符之间"; + } + else if (!userService.checkUserNameUnique(sysUser)) + { + msg = "保存用户'" + username + "'失败,注册账号已存在"; + } + else + { + sysUser.setNickName(username); + sysUser.setPassword(SecurityUtils.encryptPassword(password)); + boolean regFlag = userService.registerUser(sysUser); + if (!regFlag) + { + msg = "注册失败,请联系系统管理人员"; + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"))); + } + } + return msg; + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) + { + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) + { + throw new CaptchaException(); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java new file mode 100644 index 0000000..a581bac --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java @@ -0,0 +1,237 @@ +package com.ruoyi.framework.web.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.AddressUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import eu.bitwalker.useragentutils.UserAgent; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +/** + * token验证处理 + * + * @author ruoyi + */ +@Component +public class TokenService +{ + private static final Logger log = LoggerFactory.getLogger(TokenService.class); + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + // 令牌秘钥 + @Value("${token.secret}") + private String secret; + + // 令牌有效期(24小时,后台专用) + @Value("${token.adminExpireTime:1440}") + private int expireTime; + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + private static final Long MILLIS_MINUTE_TEN = 8 * 60 * 60 * 1000L; + + @Autowired + private RedisCache redisCache; + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(HttpServletRequest request) + { + // 获取请求携带的令牌 + String token = getToken(request); + if (StringUtils.isNotEmpty(token)) + { + try + { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + LoginUser user = redisCache.getCacheObject(userKey); + return user; + } + catch (Exception e) + { + log.error("获取用户信息异常'{}'", e.getMessage()); + } + } + return null; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(LoginUser loginUser) + { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) + { + refreshToken(loginUser); + } + } + + /** + * 删除用户身份信息 + */ + public void delLoginUser(String token) + { + if (StringUtils.isNotEmpty(token)) + { + String userKey = getTokenKey(token); + redisCache.deleteObject(userKey); + } + } + + /** + * 创建令牌 + * + * @param loginUser 用户信息 + * @return 令牌 + */ + public String createToken(LoginUser loginUser) + { + String token = IdUtils.fastUUID(); + loginUser.setToken(token); + setUserAgent(loginUser); + refreshToken(loginUser); + + Map claims = new HashMap<>(); + claims.put(Constants.LOGIN_USER_KEY, token); + return createToken(claims); + } + + /** + * 验证令牌有效期,相差不足20分钟,自动刷新缓存 + * + * @param loginUser + * @return 令牌 + */ + public void verifyToken(LoginUser loginUser) + { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= MILLIS_MINUTE_TEN) + { + // 临期自动刷新前,先校验该Token在Redis中是否仍然存在: + // 若已被顶号(单端登录互斥)删除,则不再刷新,避免旧Token“复活”导致多端同时在线 + String userKey = getTokenKey(loginUser.getToken()); + if (Boolean.TRUE.equals(redisCache.hasKey(userKey))) + { + refreshToken(loginUser); + } + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(LoginUser loginUser) + { + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + } + + /** + * 设置用户代理信息 + * + * @param loginUser 登录信息 + */ + public void setUserAgent(LoginUser loginUser) + { + UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + String ip = IpUtils.getIpAddr(); + loginUser.setIpaddr(ip); + loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); + loginUser.setBrowser(userAgent.getBrowser().getName()); + loginUser.setOs(userAgent.getOperatingSystem().getName()); + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + private String createToken(Map claims) + { + String token = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS512, secret).compact(); + return token; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private Claims parseToken(String token) + { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) + { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + /** + * 获取请求token + * + * @param request + * @return token + */ + private String getToken(HttpServletRequest request) + { + String token = request.getHeader(header); + if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) + { + token = token.replace(Constants.TOKEN_PREFIX, ""); + } + return token; + } + + private String getTokenKey(String uuid) + { + return CacheConstants.LOGIN_TOKEN_KEY + uuid; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..5dcdf90 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java @@ -0,0 +1,66 @@ +package com.ruoyi.framework.web.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.UserStatus; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户验证处理 + * + * @author ruoyi + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService +{ + private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + @Autowired + private ISysUserService userService; + + @Autowired + private SysPasswordService passwordService; + + @Autowired + private SysPermissionService permissionService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException + { + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) + { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } + else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException(MessageUtils.message("user.password.delete")); + } + else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + + passwordService.validate(user); + + return createLoginUser(user); + } + + public UserDetails createLoginUser(SysUser user) + { + return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/config/WebSocketConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000..7074a79 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/config/WebSocketConfig.java @@ -0,0 +1,13 @@ +package com.ruoyi.framework.websocket.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +// @Configuration +public class WebSocketConfig { + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/HeartbeatManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/HeartbeatManager.java new file mode 100644 index 0000000..838f79e --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/HeartbeatManager.java @@ -0,0 +1,26 @@ +package com.ruoyi.framework.websocket.connector; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class HeartbeatManager { + + private final ConcurrentHashMap lastPingMap = new ConcurrentHashMap<>(); + + public void updateLastPing(Long userId) { + lastPingMap.put(userId, System.currentTimeMillis()); + } + + public boolean isUserAlive(Long userId) { + Long lastPing = lastPingMap.get(userId); + return lastPing != null && (System.currentTimeMillis() - lastPing) < 30_000; + } + + public void removeUser(Long userId) { + lastPingMap.remove(userId); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/MessageDispatcher.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/MessageDispatcher.java new file mode 100644 index 0000000..0651ba3 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/MessageDispatcher.java @@ -0,0 +1,30 @@ +package com.ruoyi.framework.websocket.connector; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class MessageDispatcher { + + private final ConcurrentHashMap handlers = new ConcurrentHashMap<>(); + + public void register(String key, MessageHandler handler) { + handlers.put(key, handler); + } + + public void dispatch(Long userId, String key, Object data) { + MessageHandler handler = handlers.get(key); + if (handler == null) { + log.warn("未找到消息处理器, userId={}, key={}", userId, key); + return; + } + try { + handler.handle(userId, key, data); + } catch (Exception e) { + log.error("处理消息异常, userId={}, key={}", userId, key, e); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/MessageHandler.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/MessageHandler.java new file mode 100644 index 0000000..b24c65d --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/MessageHandler.java @@ -0,0 +1,6 @@ +package com.ruoyi.framework.websocket.connector; + +@FunctionalInterface +public interface MessageHandler { + void handle(Long userId, String key, Object data); +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/PushService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/PushService.java new file mode 100644 index 0000000..a970991 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/PushService.java @@ -0,0 +1,157 @@ +package com.ruoyi.framework.websocket.connector; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.utils.AesUtils; +import jakarta.annotation.PostConstruct; +import jakarta.websocket.Session; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +@Slf4j +@Component +public class PushService { + + @Autowired + private SessionRegistry sessionRegistry; + @Autowired + private SubscriptionManager subscriptionManager; + + @Value("${ws.broadcast.timeout:5}") + private long broadcastTimeoutSeconds; + + @Value("${aes.key}") + private String aesKey; + + @Value("${aes.iv}") + private String aesIv; + + @Value("${aes.enabled:false}") + private boolean aesEnabled; + + private final Executor sendExecutor = Executors.newVirtualThreadPerTaskExecutor(); + + @PostConstruct + public void init() { + log.info("[AES-WS] PushService 初始化, aesEnabled={}", aesEnabled); + } + + public boolean sendToUser(Long userId, WsResult message) { + Session session = sessionRegistry.get(userId); + if (session == null || !session.isOpen()) { + return false; + } + return doSend(session, message); + } + + public void broadcastToChannel(String channel, WsResult message) { + Set subscribers = subscriptionManager.getSubscribers(channel); + if (subscribers.isEmpty()) { + return; + } + String msg = encryptIfEnabled(JSON.toJSONString(message)); + List> futures = new ArrayList<>(subscribers.size()); + for (Long userId : subscribers) { + futures.add(CompletableFuture.runAsync(() -> { + Session session = sessionRegistry.get(userId); + if (session != null) { + doSendRaw(session, msg); + } + }, sendExecutor)); + } + awaitAll(futures, "频道广播", "channel=" + channel + ", total=" + subscribers.size()); + } + + public void broadcastToAll(WsResult message) { + Set userIds = sessionRegistry.getOnlineUserIds(); + if (userIds.isEmpty()) { + return; + } + String msg = encryptIfEnabled(JSON.toJSONString(message)); + List> futures = new ArrayList<>(userIds.size()); + for (Long userId : userIds) { + futures.add(CompletableFuture.runAsync(() -> { + Session session = sessionRegistry.get(userId); + if (session != null) { + doSendRaw(session, msg); + } + }, sendExecutor)); + } + awaitAll(futures, "全局广播", "total=" + userIds.size()); + } + + public void closeChannel(String channel) { + Set subscribers = subscriptionManager.getSubscribers(channel); + if (subscribers.isEmpty()) { + return; + } + // 使用副本避免并发修改 + List copy = new ArrayList<>(subscribers); + // 先发送频道关闭通知 + WsResult closeMsg = WsResult.ok("CHANNEL_CLOSED", channel, "频道已关闭"); + for (Long userId : copy) { + try { + sendToUser(userId, closeMsg); + } catch (Exception ignored) {} + } + // 再取消订阅 + for (Long userId : copy) { + subscriptionManager.unsubscribe(userId, channel); + } + log.info("频道已关闭, channel={}, 已取消{}个订阅", channel, copy.size()); + } + + private void awaitAll(List> futures, String operation, String detail) { + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .get(broadcastTimeoutSeconds, TimeUnit.SECONDS); + } catch (TimeoutException e) { + log.warn("{}超时, {}", operation, detail); + } catch (Exception e) { + log.warn("{}等待异常, {}", operation, detail, e); + } + } + + private boolean doSend(Session session, Object message) { + String msg = encryptIfEnabled(JSON.toJSONString(message)); + return doSendRaw(session, msg); + } + + private boolean doSendRaw(Session session, String message) { + synchronized (session) { + if (!session.isOpen()) { + return false; + } + try { + session.getBasicRemote().sendText(message); + return true; + } catch (IOException e) { + log.warn("发送消息异常"); + return false; + } + } + } + + private String encryptIfEnabled(String plainText) { + if (!aesEnabled || plainText == null || plainText.isEmpty()) { + return plainText; + } + try { + return AesUtils.encrypt(plainText, aesKey, aesIv); + } catch (Exception e) { + log.warn("WebSocket 广播消息加密失败,按明文发送", e); + return plainText; + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/SessionRegistry.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/SessionRegistry.java new file mode 100644 index 0000000..6f09641 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/SessionRegistry.java @@ -0,0 +1,65 @@ +package com.ruoyi.framework.websocket.connector; + +import jakarta.websocket.Session; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class SessionRegistry { + + private final ConcurrentHashMap sessionMap = new ConcurrentHashMap<>(); + + public void registerSession(Long userId, Session session) { + Session old = sessionMap.put(userId, session); + if (old != null && old.isOpen()) { + try { + old.close(); + } catch (IOException e) { + log.warn("关闭旧连接异常, userId={}", userId); + } + } + } + + public Session get(Long userId) { + return sessionMap.get(userId); + } + + public void remove(Long userId) { + sessionMap.remove(userId); + } + + public boolean isOnline(Long userId) { + Session session = sessionMap.get(userId); + return session != null && session.isOpen(); + } + + public Set getOnlineUserIds() { + return Set.copyOf(sessionMap.keySet()); + } + + public int getOnlineCount() { + return sessionMap.size(); + } + + public boolean kickOut(Long userId, String reason) { + Session session = sessionMap.get(userId); + if (session == null || !session.isOpen()) { + return false; + } + try { + session.getBasicRemote().sendText( + com.alibaba.fastjson2.JSON.toJSONString( + WsResult.fail(WsProtocolKey.KICK_OUT, reason, "您已被踢下线"))); + session.close(); + } catch (IOException e) { + log.warn("踢出用户异常, userId={}", userId); + } + sessionMap.remove(userId); + return true; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/SubscriptionManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/SubscriptionManager.java new file mode 100644 index 0000000..dd3f2fa --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/SubscriptionManager.java @@ -0,0 +1,73 @@ +package com.ruoyi.framework.websocket.connector; + +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 频道订阅管理器 + * 维护 userId ↔ channel 的双向映射 + */ +@Component +public class SubscriptionManager { + + private final ConcurrentHashMap> channelSubscribers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> userChannels = new ConcurrentHashMap<>(); + + public static final String CHANNEL_HALL = "HALL"; + public static final String CHANNEL_FIGHT_PREFIX = "FIGHT:"; + public static final String CHANNEL_BINDBOX_PREFIX = "BINDBOX:"; + + public void subscribe(Long userId, String channel) { + channelSubscribers.computeIfAbsent(channel, k -> ConcurrentHashMap.newKeySet()).add(userId); + userChannels.computeIfAbsent(userId, k -> ConcurrentHashMap.newKeySet()).add(channel); + } + + public void unsubscribe(Long userId, String channel) { + Set subscribers = channelSubscribers.get(channel); + if (subscribers != null) { + subscribers.remove(userId); + } + Set channels = userChannels.get(userId); + if (channels != null) { + channels.remove(channel); + } + } + + public void unsubscribeAll(Long userId) { + Set channels = userChannels.remove(userId); + if (channels == null) { + return; + } + for (String channel : channels) { + Set subscribers = channelSubscribers.get(channel); + if (subscribers != null) { + subscribers.remove(userId); + } + } + } + + public Set getSubscribers(String channel) { + Set set = channelSubscribers.get(channel); + return set == null ? Collections.emptySet() : Set.copyOf(set); + } + + public Set getUserChannels(Long userId) { + Set set = userChannels.get(userId); + return set == null ? Collections.emptySet() : Set.copyOf(set); + } + + public static String hallChannel() { + return CHANNEL_HALL; + } + + public static String fightChannel(Long fightId) { + return CHANNEL_FIGHT_PREFIX + fightId; + } + + public static String bindboxChannel(Long boxId) { + return CHANNEL_BINDBOX_PREFIX + boxId; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsConnector.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsConnector.java new file mode 100644 index 0000000..b2c0d37 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsConnector.java @@ -0,0 +1,172 @@ +package com.ruoyi.framework.websocket.connector; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.AesUtils; +import jakarta.annotation.PostConstruct; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.PathParam; +import jakarta.websocket.server.ServerEndpoint; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@ServerEndpoint("/ws/connector/{userId}") +public class WsConnector { + + private static SessionRegistry sessionRegistry; + private static MessageDispatcher messageDispatcher; + private static SubscriptionManager subscriptionManager; + private static WsTokenService wsTokenService; + private static HeartbeatManager heartbeatManager; + private static RedisCache redisCache; + + private static String aesKey; + private static String aesIv; + private static boolean aesEnabled; + + /** session 属性键:登录 token uuid,用于 PING 时刷新在线心跳 */ + private static final String ATTR_LOGIN_TOKEN_UUID = "loginTokenUuid"; + + @Autowired + public void setDependencies(SessionRegistry sessionRegistry, + MessageDispatcher messageDispatcher, + SubscriptionManager subscriptionManager, + WsTokenService wsTokenService, + HeartbeatManager heartbeatManager, + RedisCache redisCache) { + WsConnector.sessionRegistry = sessionRegistry; + WsConnector.messageDispatcher = messageDispatcher; + WsConnector.subscriptionManager = subscriptionManager; + WsConnector.wsTokenService = wsTokenService; + WsConnector.heartbeatManager = heartbeatManager; + WsConnector.redisCache = redisCache; + } + + @Autowired + public void setAesConfig(@Value("${aes.key}") String aesKey, + @Value("${aes.iv}") String aesIv, + @Value("${aes.enabled:false}") boolean aesEnabled) { + WsConnector.aesKey = aesKey; + WsConnector.aesIv = aesIv; + WsConnector.aesEnabled = aesEnabled; + } + + @PostConstruct + public void init() { + log.info("[AES-WS] WsConnector 初始化, aesEnabled={}", aesEnabled); + } + + @OnOpen + public void onOpen(Session session, @PathParam("userId") Long userId) { + List tokenList = session.getRequestParameterMap().get("token"); + String token = (tokenList != null && !tokenList.isEmpty()) ? tokenList.get(0) : null; + String tokenUuid = (token != null) ? wsTokenService.validateAndGetTokenUuid(token, userId) : null; + if (tokenUuid == null) { + log.warn("Connector 鉴权失败, userId={}", userId); + try { + session.getBasicRemote().sendText( + JSON.toJSONString(WsResult.fail(WsProtocolKey.AUTH_FAILED, null, "Token 无效"))); + } catch (IOException ignored) { + } + try { + session.close(); + } catch (IOException ignored) { + } + return; + } + + // 缓存 token uuid 并立即写入在线心跳,后续随 PING 刷新,保证纯 WebSocket 玩法(对战/观战)期间不会被判离线 + session.getUserProperties().put(ATTR_LOGIN_TOKEN_UUID, tokenUuid); + refreshOnlineHeartbeat(tokenUuid); + + sessionRegistry.registerSession(userId, session); + log.info("用户{}连接Connector,当前在线{}", userId, sessionRegistry.getOnlineCount()); + + sendMessage(session, WsResult.ok(WsProtocolKey.CONNECTED, userId, "连接成功")); + } + + @OnClose + public void onClose(Session session, @PathParam("userId") Long userId) { + sessionRegistry.remove(userId); + subscriptionManager.unsubscribeAll(userId); + heartbeatManager.removeUser(userId); + log.info("用户{}断开Connector,当前在线{}", userId, sessionRegistry.getOnlineCount()); + } + + @OnMessage + public void onMessage(String message, Session session, @PathParam("userId") Long userId) { + try { + String decryptedMessage = decryptIfEnabled(message); + WsMessage wsMsg = JSON.parseObject(decryptedMessage, WsMessage.class); + if (WsProtocolKey.PING.name().equals(wsMsg.getKey())) { + heartbeatManager.updateLastPing(userId); + // WS PING 同步刷新在线心跳,与 HTTP 请求心跳(JwtAuthenticationTokenFilter)口径一致 + Object tokenUuid = session.getUserProperties().get(ATTR_LOGIN_TOKEN_UUID); + if (tokenUuid != null) { + refreshOnlineHeartbeat((String) tokenUuid); + } + sendMessage(session, WsResult.ok(WsProtocolKey.PONG, null, null)); + return; + } + messageDispatcher.dispatch(userId, wsMsg.getKey(), wsMsg.getData()); + } catch (Exception e) { + log.warn("解析消息失败, userId={}, message={}", userId, message, e); + } + } + + @OnError + public void onError(Session session, Throwable error, @PathParam("userId") Long userId) { + log.error("Connector异常, userId={}", userId, error); + } + + private void refreshOnlineHeartbeat(String tokenUuid) { + redisCache.setCacheObject(CacheConstants.ONLINE_HEARTBEAT_KEY + tokenUuid, "1", + CacheConstants.ONLINE_HEARTBEAT_EXPIRE_MINUTES, TimeUnit.MINUTES); + } + + private void sendMessage(Session session, Object message) { + try { + String text = JSON.toJSONString(message); + session.getBasicRemote().sendText(encryptIfEnabled(text)); + } catch (IOException e) { + log.warn("发送消息异常"); + } + } + + private static String encryptIfEnabled(String plainText) { + if (!aesEnabled || plainText == null || plainText.isEmpty()) { + return plainText; + } + try { + return AesUtils.encrypt(plainText, aesKey, aesIv); + } catch (Exception e) { + log.warn("WebSocket 响应加密失败,按明文发送"); + return plainText; + } + } + + private static String decryptIfEnabled(String cipherText) { + if (!aesEnabled || cipherText == null || cipherText.isEmpty()) { + return cipherText; + } + try { + return AesUtils.decrypt(cipherText, aesKey, aesIv); + } catch (Exception e) { + log.warn("WebSocket 消息解密失败,按明文解析"); + return cipherText; + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsMessage.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsMessage.java new file mode 100644 index 0000000..6f05939 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsMessage.java @@ -0,0 +1,9 @@ +package com.ruoyi.framework.websocket.connector; + +import lombok.Data; + +@Data +public class WsMessage { + private String key; + private Object data; +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsProtocolKey.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsProtocolKey.java new file mode 100644 index 0000000..ca29278 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsProtocolKey.java @@ -0,0 +1,20 @@ +package com.ruoyi.framework.websocket.connector; + +// WebSocket 框架级协议消息 key +public enum WsProtocolKey { + + // 连接成功 + CONNECTED, + + // 鉴权失败 + AUTH_FAILED, + + // 心跳响应(服务端 -> 客户端) + PONG, + + // 踢下线 + KICK_OUT, + + // 心跳请求(客户端 -> 服务端) + PING; +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsResult.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsResult.java new file mode 100644 index 0000000..7bf2c1b --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsResult.java @@ -0,0 +1,71 @@ +package com.ruoyi.framework.websocket.connector; + +import com.ruoyi.common.constant.HttpStatus; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class WsResult implements Serializable { + private static final long serialVersionUID = 1L; + + public static final int SUCCESS = HttpStatus.SUCCESS; + public static final int FAIL = HttpStatus.ERROR; + + private String key; + private T data; + private int code; + private String msg; + + public static WsResult ok() { + return new WsResult<>(null, null, SUCCESS, "操作成功"); + } + + public static WsResult ok(String key, T data) { + return new WsResult<>(key, data, SUCCESS, "操作成功"); + } + + public static WsResult ok(String key, T data, String msg) { + return new WsResult<>(key, data, SUCCESS, msg); + } + + public static WsResult ok(Enum key, T data) { + return new WsResult<>(key.name(), data, SUCCESS, "操作成功"); + } + + public static WsResult ok(Enum key, T data, String msg) { + return new WsResult<>(key.name(), data, SUCCESS, msg); + } + + public static WsResult ok(String msg) { + return new WsResult<>(null, null, SUCCESS, msg); + } + + public static WsResult fail() { + return new WsResult<>(null, null, FAIL, "操作失败"); + } + + public static WsResult fail(String key, T data, String msg) { + return new WsResult<>(key, data, FAIL, msg); + } + + public static WsResult fail(Enum key, T data, String msg) { + return new WsResult<>(key.name(), data, FAIL, msg); + } + + public static WsResult fail(String msg) { + return new WsResult<>(null, null, FAIL, msg); + } + + public static boolean isError(WsResult ret) { + return !isSuccess(ret); + } + + public static boolean isSuccess(WsResult ret) { + return WsResult.SUCCESS == ret.getCode(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsTokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsTokenService.java new file mode 100644 index 0000000..22062b5 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/websocket/connector/WsTokenService.java @@ -0,0 +1,61 @@ +package com.ruoyi.framework.websocket.connector; + +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.StringUtils; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class WsTokenService { + + @Autowired + private RedisCache redisCache; + + @Value("${token.secret}") + private String secret; + + public boolean validateToken(String token, Long expectedUserId) { + return validateAndGetTokenUuid(token, expectedUserId) != null; + } + + /** + * 校验 token 并返回其对应的登录 token uuid(login_tokens:{uuid} 的 uuid 部分),校验失败返回 null。 + * 用于 WebSocket 心跳刷新 online_heartbeat:{uuid}。 + */ + public String validateAndGetTokenUuid(String token, Long expectedUserId) { + if (token == null || token.isEmpty()) { + return null; + } + try { + Claims claims = parseToken(token); + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + if (StringUtils.isEmpty(uuid)) { + return null; + } + String userKey = CacheConstants.LOGIN_TOKEN_KEY + uuid; + LoginUser loginUser = redisCache.getCacheObject(userKey); + if (loginUser == null) { + return null; + } + return loginUser.getUserId().equals(expectedUserId) ? uuid : null; + } catch (Exception e) { + log.warn("WS Token 校验异常, userId={}", expectedUserId, e); + return null; + } + } + + private Claims parseToken(String token) { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/ruoyi-generator/pom.xml b/ruoyi-generator/pom.xml new file mode 100644 index 0000000..7e8e067 --- /dev/null +++ b/ruoyi-generator/pom.xml @@ -0,0 +1,46 @@ + + + + ruoyi + com.ruoyi + 4.8.2 + + 4.0.0 + + ruoyi-generator + + + generator代码生成 + + + + + + + org.apache.velocity + velocity-engine-core + + + + + commons-collections + commons-collections + + + + + com.ruoyi + ruoyi-common + + + + + com.alibaba + druid-spring-boot-4-starter + + + + + \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java new file mode 100644 index 0000000..cc4cd14 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java @@ -0,0 +1,73 @@ +package com.ruoyi.generator.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 读取代码生成相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "gen") +@PropertySource(value = { "classpath:generator.yml" }) +public class GenConfig +{ + /** 作者 */ + public static String author; + + /** 生成包路径 */ + public static String packageName; + + /** 自动去除表前缀,默认是false */ + public static boolean autoRemovePre; + + /** 表前缀(类名不会包含表前缀) */ + public static String tablePrefix; + + public static String getAuthor() + { + return author; + } + + @Value("${author}") + public void setAuthor(String author) + { + GenConfig.author = author; + } + + public static String getPackageName() + { + return packageName; + } + + @Value("${packageName}") + public void setPackageName(String packageName) + { + GenConfig.packageName = packageName; + } + + public static boolean getAutoRemovePre() + { + return autoRemovePre; + } + + @Value("${autoRemovePre}") + public void setAutoRemovePre(boolean autoRemovePre) + { + GenConfig.autoRemovePre = autoRemovePre; + } + + public static String getTablePrefix() + { + return tablePrefix; + } + + @Value("${tablePrefix}") + public void setTablePrefix(String tablePrefix) + { + GenConfig.tablePrefix = tablePrefix; + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java new file mode 100644 index 0000000..4ec1333 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java @@ -0,0 +1,214 @@ +package com.ruoyi.generator.controller; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.service.IGenTableColumnService; +import com.ruoyi.generator.service.IGenTableService; + +/** + * 代码生成 操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/tool/gen") +public class GenController extends BaseController +{ + @Autowired + private IGenTableService genTableService; + + @Autowired + private IGenTableColumnService genTableColumnService; + + /** + * 查询代码生成列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/list") + public TableDataInfo genList(GenTable genTable) + { + startPage(); + List list = genTableService.selectGenTableList(genTable); + return getDataTable(list); + } + + /** + * 修改代码生成业务 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:query')") + @GetMapping(value = "/{tableId}") + public AjaxResult getInfo(@PathVariable Long tableId) + { + GenTable table = genTableService.selectGenTableById(tableId); + List tables = genTableService.selectGenTableAll(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + Map map = new HashMap(); + map.put("info", table); + map.put("rows", list); + map.put("tables", tables); + return success(map); + } + + /** + * 查询数据库列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/db/list") + public TableDataInfo dataList(GenTable genTable) + { + startPage(); + List list = genTableService.selectDbTableList(genTable); + return getDataTable(list); + } + + /** + * 查询数据表字段列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping(value = "/column/{tableId}") + public TableDataInfo columnList(Long tableId) + { + TableDataInfo dataInfo = new TableDataInfo(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + dataInfo.setRows(list); + dataInfo.setTotal(list.size()); + return dataInfo; + } + + /** + * 导入表结构(保存) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:import')") + @Log(title = "代码生成", businessType = BusinessType.IMPORT) + @PostMapping("/importTable") + public AjaxResult importTableSave(String tables) + { + String[] tableNames = Convert.toStrArray(tables); + // 查询表信息 + List tableList = genTableService.selectDbTableListByNames(tableNames); + genTableService.importGenTable(tableList); + return success(); + } + + /** + * 修改保存代码生成业务 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult editSave(@Validated @RequestBody GenTable genTable) + { + genTableService.validateEdit(genTable); + genTableService.updateGenTable(genTable); + return success(); + } + + /** + * 删除代码生成 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:remove')") + @Log(title = "代码生成", businessType = BusinessType.DELETE) + @DeleteMapping("/{tableIds}") + public AjaxResult remove(@PathVariable Long[] tableIds) + { + genTableService.deleteGenTableByIds(tableIds); + return success(); + } + + /** + * 预览代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:preview')") + @GetMapping("/preview/{tableId}") + public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException + { + Map dataMap = genTableService.previewCode(tableId); + return success(dataMap); + } + + /** + * 生成代码(下载方式) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/download/{tableName}") + public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException + { + byte[] data = genTableService.downloadCode(tableName); + genCode(response, data); + } + + /** + * 生成代码(自定义路径) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/genCode/{tableName}") + public AjaxResult genCode(@PathVariable("tableName") String tableName) + { + genTableService.generatorCode(tableName); + return success(); + } + + /** + * 同步数据库 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @GetMapping("/synchDb/{tableName}") + public AjaxResult synchDb(@PathVariable("tableName") String tableName) + { + genTableService.synchDb(tableName); + return success(); + } + + /** + * 批量生成代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/batchGenCode") + public void batchGenCode(HttpServletResponse response, String tables) throws IOException + { + String[] tableNames = Convert.toStrArray(tables); + byte[] data = genTableService.downloadCode(tableNames); + genCode(response, data); + } + + /** + * 生成zip文件 + */ + private void genCode(HttpServletResponse response, byte[] data) throws IOException + { + response.reset(); + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\""); + response.addHeader("Content-Length", "" + data.length); + response.setContentType("application/octet-stream; charset=UTF-8"); + IOUtils.write(data, response.getOutputStream()); + } +} \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java new file mode 100644 index 0000000..53279dc --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java @@ -0,0 +1,372 @@ +package com.ruoyi.generator.domain; + +import java.util.List; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import org.apache.commons.lang3.ArrayUtils; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; + +/** + * 业务表 gen_table + * + * @author ruoyi + */ +public class GenTable extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long tableId; + + /** 表名称 */ + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** 表描述 */ + @NotBlank(message = "表描述不能为空") + private String tableComment; + + /** 关联父表的表名 */ + private String subTableName; + + /** 本表关联父表的外键名 */ + private String subTableFkName; + + /** 实体类名称(首字母大写) */ + @NotBlank(message = "实体类名称不能为空") + private String className; + + /** 使用的模板(crud单表操作 tree树表操作 sub主子表操作) */ + private String tplCategory; + + /** 生成包路径 */ + @NotBlank(message = "生成包路径不能为空") + private String packageName; + + /** 生成模块名 */ + @NotBlank(message = "生成模块名不能为空") + private String moduleName; + + /** 生成业务名 */ + @NotBlank(message = "生成业务名不能为空") + private String businessName; + + /** 生成功能名 */ + @NotBlank(message = "生成功能名不能为空") + private String functionName; + + /** 生成作者 */ + @NotBlank(message = "作者不能为空") + private String functionAuthor; + + /** 生成代码方式(0zip压缩包 1自定义路径) */ + private String genType; + + /** 生成路径(不填默认项目路径) */ + private String genPath; + + /** 主键信息 */ + private GenTableColumn pkColumn; + + /** 子表信息 */ + private GenTable subTable; + + /** 表列信息 */ + @Valid + private List columns; + + /** 其它生成选项 */ + private String options; + + /** 树编码字段 */ + private String treeCode; + + /** 树父编码字段 */ + private String treeParentCode; + + /** 树名称字段 */ + private String treeName; + + /** 上级菜单ID字段 */ + private String parentMenuId; + + /** 上级菜单名称字段 */ + private String parentMenuName; + + public Long getTableId() + { + return tableId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public String getTableName() + { + return tableName; + } + + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + public String getTableComment() + { + return tableComment; + } + + public void setTableComment(String tableComment) + { + this.tableComment = tableComment; + } + + public String getSubTableName() + { + return subTableName; + } + + public void setSubTableName(String subTableName) + { + this.subTableName = subTableName; + } + + public String getSubTableFkName() + { + return subTableFkName; + } + + public void setSubTableFkName(String subTableFkName) + { + this.subTableFkName = subTableFkName; + } + + public String getClassName() + { + return className; + } + + public void setClassName(String className) + { + this.className = className; + } + + public String getTplCategory() + { + return tplCategory; + } + + public void setTplCategory(String tplCategory) + { + this.tplCategory = tplCategory; + } + + public String getPackageName() + { + return packageName; + } + + public void setPackageName(String packageName) + { + this.packageName = packageName; + } + + public String getModuleName() + { + return moduleName; + } + + public void setModuleName(String moduleName) + { + this.moduleName = moduleName; + } + + public String getBusinessName() + { + return businessName; + } + + public void setBusinessName(String businessName) + { + this.businessName = businessName; + } + + public String getFunctionName() + { + return functionName; + } + + public void setFunctionName(String functionName) + { + this.functionName = functionName; + } + + public String getFunctionAuthor() + { + return functionAuthor; + } + + public void setFunctionAuthor(String functionAuthor) + { + this.functionAuthor = functionAuthor; + } + + public String getGenType() + { + return genType; + } + + public void setGenType(String genType) + { + this.genType = genType; + } + + public String getGenPath() + { + return genPath; + } + + public void setGenPath(String genPath) + { + this.genPath = genPath; + } + + public GenTableColumn getPkColumn() + { + return pkColumn; + } + + public void setPkColumn(GenTableColumn pkColumn) + { + this.pkColumn = pkColumn; + } + + public GenTable getSubTable() + { + return subTable; + } + + public void setSubTable(GenTable subTable) + { + this.subTable = subTable; + } + + public List getColumns() + { + return columns; + } + + public void setColumns(List columns) + { + this.columns = columns; + } + + public String getOptions() + { + return options; + } + + public void setOptions(String options) + { + this.options = options; + } + + public String getTreeCode() + { + return treeCode; + } + + public void setTreeCode(String treeCode) + { + this.treeCode = treeCode; + } + + public String getTreeParentCode() + { + return treeParentCode; + } + + public void setTreeParentCode(String treeParentCode) + { + this.treeParentCode = treeParentCode; + } + + public String getTreeName() + { + return treeName; + } + + public void setTreeName(String treeName) + { + this.treeName = treeName; + } + + public String getParentMenuId() + { + return parentMenuId; + } + + public void setParentMenuId(String parentMenuId) + { + this.parentMenuId = parentMenuId; + } + + public String getParentMenuName() + { + return parentMenuName; + } + + public void setParentMenuName(String parentMenuName) + { + this.parentMenuName = parentMenuName; + } + + public boolean isSub() + { + return isSub(this.tplCategory); + } + + public static boolean isSub(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_SUB, tplCategory); + } + + public boolean isTree() + { + return isTree(this.tplCategory); + } + + public static boolean isTree(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory); + } + + public boolean isCrud() + { + return isCrud(this.tplCategory); + } + + public static boolean isCrud(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory); + } + + public boolean isSuperColumn(String javaField) + { + return isSuperColumn(this.tplCategory, javaField); + } + + public static boolean isSuperColumn(String tplCategory, String javaField) + { + if (isTree(tplCategory)) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY)); + } + return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY); + } +} \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java new file mode 100644 index 0000000..3dc6d62 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java @@ -0,0 +1,373 @@ +package com.ruoyi.generator.domain; + +import jakarta.validation.constraints.NotBlank; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; + +/** + * 代码生成业务字段表 gen_table_column + * + * @author ruoyi + */ +public class GenTableColumn extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long columnId; + + /** 归属表编号 */ + private Long tableId; + + /** 列名称 */ + private String columnName; + + /** 列描述 */ + private String columnComment; + + /** 列类型 */ + private String columnType; + + /** JAVA类型 */ + private String javaType; + + /** JAVA字段名 */ + @NotBlank(message = "Java属性不能为空") + private String javaField; + + /** 是否主键(1是) */ + private String isPk; + + /** 是否自增(1是) */ + private String isIncrement; + + /** 是否必填(1是) */ + private String isRequired; + + /** 是否为插入字段(1是) */ + private String isInsert; + + /** 是否编辑字段(1是) */ + private String isEdit; + + /** 是否列表字段(1是) */ + private String isList; + + /** 是否查询字段(1是) */ + private String isQuery; + + /** 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) */ + private String queryType; + + /** 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) */ + private String htmlType; + + /** 字典类型 */ + private String dictType; + + /** 排序 */ + private Integer sort; + + public void setColumnId(Long columnId) + { + this.columnId = columnId; + } + + public Long getColumnId() + { + return columnId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public Long getTableId() + { + return tableId; + } + + public void setColumnName(String columnName) + { + this.columnName = columnName; + } + + public String getColumnName() + { + return columnName; + } + + public void setColumnComment(String columnComment) + { + this.columnComment = columnComment; + } + + public String getColumnComment() + { + return columnComment; + } + + public void setColumnType(String columnType) + { + this.columnType = columnType; + } + + public String getColumnType() + { + return columnType; + } + + public void setJavaType(String javaType) + { + this.javaType = javaType; + } + + public String getJavaType() + { + return javaType; + } + + public void setJavaField(String javaField) + { + this.javaField = javaField; + } + + public String getJavaField() + { + return javaField; + } + + public String getCapJavaField() + { + return StringUtils.capitalize(javaField); + } + + public void setIsPk(String isPk) + { + this.isPk = isPk; + } + + public String getIsPk() + { + return isPk; + } + + public boolean isPk() + { + return isPk(this.isPk); + } + + public boolean isPk(String isPk) + { + return isPk != null && StringUtils.equals("1", isPk); + } + + public String getIsIncrement() + { + return isIncrement; + } + + public void setIsIncrement(String isIncrement) + { + this.isIncrement = isIncrement; + } + + public boolean isIncrement() + { + return isIncrement(this.isIncrement); + } + + public boolean isIncrement(String isIncrement) + { + return isIncrement != null && StringUtils.equals("1", isIncrement); + } + + public void setIsRequired(String isRequired) + { + this.isRequired = isRequired; + } + + public String getIsRequired() + { + return isRequired; + } + + public boolean isRequired() + { + return isRequired(this.isRequired); + } + + public boolean isRequired(String isRequired) + { + return isRequired != null && StringUtils.equals("1", isRequired); + } + + public void setIsInsert(String isInsert) + { + this.isInsert = isInsert; + } + + public String getIsInsert() + { + return isInsert; + } + + public boolean isInsert() + { + return isInsert(this.isInsert); + } + + public boolean isInsert(String isInsert) + { + return isInsert != null && StringUtils.equals("1", isInsert); + } + + public void setIsEdit(String isEdit) + { + this.isEdit = isEdit; + } + + public String getIsEdit() + { + return isEdit; + } + + public boolean isEdit() + { + return isInsert(this.isEdit); + } + + public boolean isEdit(String isEdit) + { + return isEdit != null && StringUtils.equals("1", isEdit); + } + + public void setIsList(String isList) + { + this.isList = isList; + } + + public String getIsList() + { + return isList; + } + + public boolean isList() + { + return isList(this.isList); + } + + public boolean isList(String isList) + { + return isList != null && StringUtils.equals("1", isList); + } + + public void setIsQuery(String isQuery) + { + this.isQuery = isQuery; + } + + public String getIsQuery() + { + return isQuery; + } + + public boolean isQuery() + { + return isQuery(this.isQuery); + } + + public boolean isQuery(String isQuery) + { + return isQuery != null && StringUtils.equals("1", isQuery); + } + + public void setQueryType(String queryType) + { + this.queryType = queryType; + } + + public String getQueryType() + { + return queryType; + } + + public String getHtmlType() + { + return htmlType; + } + + public void setHtmlType(String htmlType) + { + this.htmlType = htmlType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getDictType() + { + return dictType; + } + + public void setSort(Integer sort) + { + this.sort = sort; + } + + public Integer getSort() + { + return sort; + } + + public boolean isSuperColumn() + { + return isSuperColumn(this.javaField); + } + + public static boolean isSuperColumn(String javaField) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + // BaseEntity + "createBy", "createTime", "updateBy", "updateTime", "remark", + // TreeEntity + "parentName", "parentId", "orderNum", "ancestors"); + } + + public boolean isUsableColumn() + { + return isUsableColumn(javaField); + } + + public static boolean isUsableColumn(String javaField) + { + // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单 + return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark"); + } + + public String readConverterExp() + { + String remarks = StringUtils.substringBetween(this.columnComment, "(", ")"); + StringBuffer sb = new StringBuffer(); + if (StringUtils.isNotEmpty(remarks)) + { + for (String value : remarks.split(" ")) + { + if (StringUtils.isNotEmpty(value)) + { + Object startStr = value.subSequence(0, 1); + String endStr = value.substring(1); + sb.append("").append(startStr).append("=").append(endStr).append(","); + } + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } + else + { + return this.columnComment; + } + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java new file mode 100644 index 0000000..951e166 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java @@ -0,0 +1,60 @@ +package com.ruoyi.generator.mapper; + +import java.util.List; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 业务字段 数据层 + * + * @author ruoyi + */ +public interface GenTableColumnMapper +{ + /** + * 根据表名称查询列信息 + * + * @param tableName 表名称 + * @return 列信息 + */ + public List selectDbTableColumnsByName(String tableName); + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段 + * + * @param genTableColumns 列数据 + * @return 结果 + */ + public int deleteGenTableColumns(List genTableColumns); + + /** + * 批量删除业务字段 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(Long[] ids); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java new file mode 100644 index 0000000..9b330df --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java @@ -0,0 +1,83 @@ +package com.ruoyi.generator.mapper; + +import java.util.List; +import com.ruoyi.generator.domain.GenTable; + +/** + * 业务 数据层 + * + * @author ruoyi + */ +public interface GenTableMapper +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询表ID业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 查询表名称业务信息 + * + * @param tableName 表名称 + * @return 业务信息 + */ + public GenTable selectGenTableByName(String tableName); + + /** + * 新增业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int insertGenTable(GenTable genTable); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int updateGenTable(GenTable genTable); + + /** + * 批量删除业务 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableByIds(Long[] ids); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java new file mode 100644 index 0000000..0679689 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java @@ -0,0 +1,68 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.mapper.GenTableColumnMapper; + +/** + * 业务字段 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableColumnServiceImpl implements IGenTableColumnService +{ + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + @Override + public List selectGenTableColumnListByTableId(Long tableId) + { + return genTableColumnMapper.selectGenTableColumnListByTableId(tableId); + } + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int insertGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.insertGenTableColumn(genTableColumn); + } + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int updateGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + + /** + * 删除业务字段对象 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteGenTableColumnByIds(String ids) + { + return genTableColumnMapper.deleteGenTableColumnByIds(Convert.toLongArray(ids)); + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java new file mode 100644 index 0000000..4889f81 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java @@ -0,0 +1,521 @@ +package com.ruoyi.generator.service; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.core.text.CharsetKit; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.mapper.GenTableColumnMapper; +import com.ruoyi.generator.mapper.GenTableMapper; +import com.ruoyi.generator.util.GenUtils; +import com.ruoyi.generator.util.VelocityInitializer; +import com.ruoyi.generator.util.VelocityUtils; + +/** + * 业务 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableServiceImpl implements IGenTableService +{ + private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class); + + @Autowired + private GenTableMapper genTableMapper; + + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + @Override + public GenTable selectGenTableById(Long id) + { + GenTable genTable = genTableMapper.selectGenTableById(id); + setTableFromOptions(genTable); + return genTable; + } + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + @Override + public List selectGenTableList(GenTable genTable) + { + return genTableMapper.selectGenTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + @Override + public List selectDbTableList(GenTable genTable) + { + return genTableMapper.selectDbTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + @Override + public List selectDbTableListByNames(String[] tableNames) + { + return genTableMapper.selectDbTableListByNames(tableNames); + } + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + @Override + public List selectGenTableAll() + { + return genTableMapper.selectGenTableAll(); + } + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + @Override + @Transactional + public void updateGenTable(GenTable genTable) + { + String options = JSON.toJSONString(genTable.getParams()); + genTable.setOptions(options); + int row = genTableMapper.updateGenTable(genTable); + if (row > 0) + { + for (GenTableColumn cenTableColumn : genTable.getColumns()) + { + genTableColumnMapper.updateGenTableColumn(cenTableColumn); + } + } + } + + /** + * 删除业务对象 + * + * @param tableIds 需要删除的数据ID + * @return 结果 + */ + @Override + @Transactional + public void deleteGenTableByIds(Long[] tableIds) + { + genTableMapper.deleteGenTableByIds(tableIds); + genTableColumnMapper.deleteGenTableColumnByIds(tableIds); + } + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + @Override + @Transactional + public void importGenTable(List tableList) + { + String operName = SecurityUtils.getUsername(); + try + { + for (GenTable table : tableList) + { + String tableName = table.getTableName(); + GenUtils.initTable(table, operName); + int row = genTableMapper.insertGenTable(table); + if (row > 0) + { + // 保存列信息 + List genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + for (GenTableColumn column : genTableColumns) + { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insertGenTableColumn(column); + } + } + } + } + catch (Exception e) + { + throw new ServiceException("导入失败:" + e.getMessage()); + } + } + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + @Override + public Map previewCode(Long tableId) + { + Map dataMap = new LinkedHashMap<>(); + // 查询表信息 + GenTable table = genTableMapper.selectGenTableById(tableId); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + dataMap.put(template, sw.toString()); + } + return dataMap; + } + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + @Override + public byte[] downloadCode(String tableName) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + generatorCode(tableName, zip); + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + */ + @Override + public void generatorCode(String tableName) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) + { + if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + String path = getGenPath(table, template); + FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8); + } + catch (IOException e) + { + throw new ServiceException("渲染模板失败,表名:" + table.getTableName()); + } + } + } + } + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + @Override + @Transactional + public void synchDb(String tableName) + { + GenTable table = genTableMapper.selectGenTableByName(tableName); + List tableColumns = table.getColumns(); + Map tableColumnMap = tableColumns.stream().collect(Collectors.toMap(GenTableColumn::getColumnName, Function.identity())); + + List dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + if (StringUtils.isEmpty(dbTableColumns)) + { + throw new ServiceException("同步数据失败,原表结构不存在"); + } + List dbTableColumnNames = dbTableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + dbTableColumns.forEach(column -> { + GenUtils.initColumnField(column, table); + if (tableColumnMap.containsKey(column.getColumnName())) + { + GenTableColumn prevColumn = tableColumnMap.get(column.getColumnName()); + column.setColumnId(prevColumn.getColumnId()); + if (column.isList()) + { + // 如果是列表,继续保留查询方式/字典类型选项 + column.setDictType(prevColumn.getDictType()); + column.setQueryType(prevColumn.getQueryType()); + } + if (StringUtils.isNotEmpty(prevColumn.getIsRequired()) && !column.isPk() + && (column.isInsert() || column.isEdit()) + && ((column.isUsableColumn()) || (!column.isSuperColumn()))) + { + // 如果是(新增/修改&非主键/非忽略及父属性),继续保留必填/显示类型选项 + column.setIsRequired(prevColumn.getIsRequired()); + column.setHtmlType(prevColumn.getHtmlType()); + } + genTableColumnMapper.updateGenTableColumn(column); + } + else + { + genTableColumnMapper.insertGenTableColumn(column); + } + }); + + List delColumns = tableColumns.stream().filter(column -> !dbTableColumnNames.contains(column.getColumnName())).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(delColumns)) + { + genTableColumnMapper.deleteGenTableColumns(delColumns); + } + } + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + @Override + public byte[] downloadCode(String[] tableNames) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + for (String tableName : tableNames) + { + generatorCode(tableName, zip); + } + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 查询表信息并生成代码 + */ + private void generatorCode(String tableName, ZipOutputStream zip) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + // 添加到zip + zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table))); + IOUtils.write(sw.toString(), zip, Constants.UTF8); + IOUtils.closeQuietly(sw); + zip.flush(); + zip.closeEntry(); + } + catch (IOException e) + { + log.error("渲染模板失败,表名:" + table.getTableName(), e); + } + } + } + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + @Override + public void validateEdit(GenTable genTable) + { + if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) + { + String options = JSON.toJSONString(genTable.getParams()); + JSONObject paramsObj = JSON.parseObject(options); + if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_CODE))) + { + throw new ServiceException("树编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_PARENT_CODE))) + { + throw new ServiceException("树父编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_NAME))) + { + throw new ServiceException("树名称字段不能为空"); + } + else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) + { + if (StringUtils.isEmpty(genTable.getSubTableName())) + { + throw new ServiceException("关联子表的表名不能为空"); + } + else if (StringUtils.isEmpty(genTable.getSubTableFkName())) + { + throw new ServiceException("子表关联的外键名不能为空"); + } + } + } + } + + /** + * 设置主键列信息 + * + * @param table 业务表信息 + */ + public void setPkColumn(GenTable table) + { + for (GenTableColumn column : table.getColumns()) + { + if (column.isPk()) + { + table.setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getPkColumn())) + { + table.setPkColumn(table.getColumns().get(0)); + } + if (GenConstants.TPL_SUB.equals(table.getTplCategory())) + { + for (GenTableColumn column : table.getSubTable().getColumns()) + { + if (column.isPk()) + { + table.getSubTable().setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getSubTable().getPkColumn())) + { + table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0)); + } + } + } + + /** + * 设置主子表信息 + * + * @param table 业务表信息 + */ + public void setSubTable(GenTable table) + { + String subTableName = table.getSubTableName(); + if (StringUtils.isNotEmpty(subTableName)) + { + table.setSubTable(genTableMapper.selectGenTableByName(subTableName)); + } + } + + /** + * 设置代码生成其他选项值 + * + * @param genTable 设置后的生成对象 + */ + public void setTableFromOptions(GenTable genTable) + { + JSONObject paramsObj = JSON.parseObject(genTable.getOptions()); + if (StringUtils.isNotNull(paramsObj)) + { + String treeCode = paramsObj.getString(GenConstants.TREE_CODE); + String treeParentCode = paramsObj.getString(GenConstants.TREE_PARENT_CODE); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + String parentMenuId = paramsObj.getString(GenConstants.PARENT_MENU_ID); + String parentMenuName = paramsObj.getString(GenConstants.PARENT_MENU_NAME); + + genTable.setTreeCode(treeCode); + genTable.setTreeParentCode(treeParentCode); + genTable.setTreeName(treeName); + genTable.setParentMenuId(parentMenuId); + genTable.setParentMenuName(parentMenuName); + } + } + + /** + * 获取代码生成地址 + * + * @param table 业务表信息 + * @param template 模板文件路径 + * @return 生成地址 + */ + public static String getGenPath(GenTable table, String template) + { + String genPath = table.getGenPath(); + if (StringUtils.equals(genPath, "/")) + { + return System.getProperty("user.dir") + File.separator + "src" + File.separator + VelocityUtils.getFileName(template, table); + } + return genPath + File.separator + VelocityUtils.getFileName(template, table); + } +} \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java new file mode 100644 index 0000000..3037f70 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java @@ -0,0 +1,44 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 业务字段 服务层 + * + * @author ruoyi + */ +public interface IGenTableColumnService +{ + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(String ids); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java new file mode 100644 index 0000000..9d53f95 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java @@ -0,0 +1,121 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import java.util.Map; +import com.ruoyi.generator.domain.GenTable; + +/** + * 业务 服务层 + * + * @author ruoyi + */ +public interface IGenTableService +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public void updateGenTable(GenTable genTable); + + /** + * 删除业务信息 + * + * @param tableIds 需要删除的表数据ID + * @return 结果 + */ + public void deleteGenTableByIds(Long[] tableIds); + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + public void importGenTable(List tableList); + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + public Map previewCode(Long tableId); + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + public byte[] downloadCode(String tableName); + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + * @return 数据 + */ + public void generatorCode(String tableName); + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + public void synchDb(String tableName); + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + public byte[] downloadCode(String[] tableNames); + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + public void validateEdit(GenTable genTable); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java new file mode 100644 index 0000000..e7ebc20 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java @@ -0,0 +1,257 @@ +package com.ruoyi.generator.util; + +import java.util.Arrays; +import org.apache.commons.lang3.RegExUtils; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.config.GenConfig; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 代码生成器 工具类 + * + * @author ruoyi + */ +public class GenUtils +{ + /** + * 初始化表信息 + */ + public static void initTable(GenTable genTable, String operName) + { + genTable.setClassName(convertClassName(genTable.getTableName())); + genTable.setPackageName(GenConfig.getPackageName()); + genTable.setModuleName(getModuleName(GenConfig.getPackageName())); + genTable.setBusinessName(getBusinessName(genTable.getTableName())); + genTable.setFunctionName(replaceText(genTable.getTableComment())); + genTable.setFunctionAuthor(GenConfig.getAuthor()); + genTable.setCreateBy(operName); + } + + /** + * 初始化列属性字段 + */ + public static void initColumnField(GenTableColumn column, GenTable table) + { + String dataType = getDbType(column.getColumnType()); + String columnName = column.getColumnName(); + column.setTableId(table.getTableId()); + column.setCreateBy(table.getCreateBy()); + // 设置java字段名 + column.setJavaField(StringUtils.toCamelCase(columnName)); + // 设置默认类型 + column.setJavaType(GenConstants.TYPE_STRING); + column.setQueryType(GenConstants.QUERY_EQ); + + if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) + { + // 字符串长度超过500设置为文本域 + Integer columnLength = getColumnLength(column.getColumnType()); + String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT; + column.setHtmlType(htmlType); + } + else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) + { + column.setJavaType(GenConstants.TYPE_DATE); + column.setHtmlType(GenConstants.HTML_DATETIME); + } + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType)) + { + column.setHtmlType(GenConstants.HTML_INPUT); + + // 如果是浮点型 统一用BigDecimal + String[] str = StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ","); + if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0) + { + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); + } + // 如果是整形 + else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10) + { + column.setJavaType(GenConstants.TYPE_INTEGER); + } + // 长整形 + else + { + column.setJavaType(GenConstants.TYPE_LONG); + } + } + + // 插入字段(默认所有字段都需要插入) + column.setIsInsert(GenConstants.REQUIRE); + + // 编辑字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk()) + { + column.setIsEdit(GenConstants.REQUIRE); + } + // 列表字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk()) + { + column.setIsList(GenConstants.REQUIRE); + } + // 查询字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) + { + column.setIsQuery(GenConstants.REQUIRE); + } + + // 查询字段类型 + if (StringUtils.endsWithIgnoreCase(columnName, "name")) + { + column.setQueryType(GenConstants.QUERY_LIKE); + } + // 状态字段设置单选框 + if (StringUtils.endsWithIgnoreCase(columnName, "status")) + { + column.setHtmlType(GenConstants.HTML_RADIO); + } + // 类型&性别字段设置下拉框 + else if (StringUtils.endsWithIgnoreCase(columnName, "type") + || StringUtils.endsWithIgnoreCase(columnName, "sex")) + { + column.setHtmlType(GenConstants.HTML_SELECT); + } + // 图片字段设置图片上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "image")) + { + column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD); + } + // 文件字段设置文件上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "file")) + { + column.setHtmlType(GenConstants.HTML_FILE_UPLOAD); + } + // 内容字段设置富文本控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "content")) + { + column.setHtmlType(GenConstants.HTML_EDITOR); + } + } + + /** + * 校验数组是否包含指定值 + * + * @param arr 数组 + * @param targetValue 值 + * @return 是否包含 + */ + public static boolean arraysContains(String[] arr, String targetValue) + { + return Arrays.asList(arr).contains(targetValue); + } + + /** + * 获取模块名 + * + * @param packageName 包名 + * @return 模块名 + */ + public static String getModuleName(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + int nameLength = packageName.length(); + return StringUtils.substring(packageName, lastIndex + 1, nameLength); + } + + /** + * 获取业务名 + * + * @param tableName 表名 + * @return 业务名 + */ + public static String getBusinessName(String tableName) + { + int lastIndex = tableName.lastIndexOf("_"); + int nameLength = tableName.length(); + return StringUtils.substring(tableName, lastIndex + 1, nameLength); + } + + /** + * 表名转换成Java类名 + * + * @param tableName 表名称 + * @return 类名 + */ + public static String convertClassName(String tableName) + { + boolean autoRemovePre = GenConfig.getAutoRemovePre(); + String tablePrefix = GenConfig.getTablePrefix(); + if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix)) + { + String[] searchList = StringUtils.split(tablePrefix, ","); + tableName = replaceFirst(tableName, searchList); + } + return StringUtils.convertToCamelCase(tableName); + } + + /** + * 批量替换前缀 + * + * @param replacementm 替换值 + * @param searchList 替换列表 + * @return + */ + public static String replaceFirst(String replacementm, String[] searchList) + { + String text = replacementm; + for (String searchString : searchList) + { + if (replacementm.startsWith(searchString)) + { + text = replacementm.replaceFirst(searchString, ""); + break; + } + } + return text; + } + + /** + * 关键字替换 + * + * @param text 需要被替换的名字 + * @return 替换后的名字 + */ + public static String replaceText(String text) + { + return RegExUtils.replaceAll(text, "(?:表|若依)", ""); + } + + /** + * 获取数据库类型字段 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static String getDbType(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + return StringUtils.substringBefore(columnType, "("); + } + else + { + return columnType; + } + } + + /** + * 获取字段长度 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static Integer getColumnLength(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + String length = StringUtils.substringBetween(columnType, "(", ")"); + return Integer.valueOf(length); + } + else + { + return 0; + } + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java new file mode 100644 index 0000000..9f69403 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java @@ -0,0 +1,34 @@ +package com.ruoyi.generator.util; + +import java.util.Properties; +import org.apache.velocity.app.Velocity; +import com.ruoyi.common.constant.Constants; + +/** + * VelocityEngine工厂 + * + * @author ruoyi + */ +public class VelocityInitializer +{ + /** + * 初始化vm方法 + */ + public static void initVelocity() + { + Properties p = new Properties(); + try + { + // 加载classpath目录下的vm文件 + p.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + // 定义字符集 + p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8); + // 初始化Velocity引擎,指定配置Properties + Velocity.init(p); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java new file mode 100644 index 0000000..7ede02d --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java @@ -0,0 +1,402 @@ +package com.ruoyi.generator.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.velocity.VelocityContext; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 模板处理工具类 + * + * @author ruoyi + */ +public class VelocityUtils +{ + /** 项目空间路径 */ + private static final String PROJECT_PATH = "main/java"; + + /** mybatis空间路径 */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** 默认上级菜单,系统工具 */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** + * 设置模板变量信息 + * + * @return 模板列表 + */ + public static VelocityContext prepareContext(GenTable genTable) + { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); + velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); + velocityContext.put("moduleName", genTable.getModuleName()); + velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName())); + velocityContext.put("businessName", genTable.getBusinessName()); + velocityContext.put("basePackage", getPackagePrefix(packageName)); + velocityContext.put("packageName", packageName); + velocityContext.put("author", genTable.getFunctionAuthor()); + velocityContext.put("datetime", DateUtils.getDate()); + velocityContext.put("pkColumn", genTable.getPkColumn()); + velocityContext.put("importList", getImportList(genTable)); + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); + velocityContext.put("columns", genTable.getColumns()); + velocityContext.put("table", genTable); + velocityContext.put("dicts", getDicts(genTable)); + setMenuVelocityContext(velocityContext, genTable); + if (GenConstants.TPL_TREE.equals(tplCategory)) + { + setTreeVelocityContext(velocityContext, genTable); + } + if (GenConstants.TPL_SUB.equals(tplCategory)) + { + setSubVelocityContext(velocityContext, genTable); + } + return velocityContext; + } + + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + context.put("tree_parent_code", paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + context.put("tree_name", paramsObj.getString(GenConstants.TREE_NAME)); + } + } + + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) + { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = genTable.getSubTable().getClassName(); + String subTableFkClassName = StringUtils.convertToCamelCase(subTableFkName); + + context.put("subTable", subTable); + context.put("subTableName", subTableName); + context.put("subTableFkName", subTableFkName); + context.put("subTableFkClassName", subTableFkClassName); + context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName)); + context.put("subClassName", subClassName); + context.put("subclassName", StringUtils.uncapitalize(subClassName)); + context.put("subImportList", getImportList(genTable.getSubTable())); + } + + /** + * 获取模板信息 + * + * @return 模板列表 + */ + public static List getTemplateList(String tplCategory) + { + List templates = new ArrayList(); + templates.add("vm/java/domain.java.vm"); + templates.add("vm/java/mapper.java.vm"); + templates.add("vm/java/service.java.vm"); + templates.add("vm/java/serviceImpl.java.vm"); + templates.add("vm/java/controller.java.vm"); + templates.add("vm/xml/mapper.xml.vm"); + templates.add("vm/sql/sql.vm"); + templates.add("vm/js/api.js.vm"); + if (GenConstants.TPL_CRUD.equals(tplCategory)) + { + templates.add("vm/vue/index.vue.vm"); + } + else if (GenConstants.TPL_TREE.equals(tplCategory)) + { + templates.add("vm/vue/index-tree.vue.vm"); + } + else if (GenConstants.TPL_SUB.equals(tplCategory)) + { + templates.add("vm/vue/index.vue.vm"); + templates.add("vm/java/sub-domain.java.vm"); + } + return templates; + } + + /** + * 获取文件名 + */ + public static String getFileName(String template, GenTable genTable) + { + // 文件名称 + String fileName = ""; + // 包路径 + String packageName = genTable.getPackageName(); + // 模块名 + String moduleName = genTable.getModuleName(); + // 大写类名 + String className = genTable.getClassName(); + // 业务名称 + String businessName = genTable.getBusinessName(); + + String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); + String mybatisPath = MYBATIS_PATH + "/" + moduleName; + String vuePath = "vue"; + + if (template.contains("domain.java.vm")) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, className); + } + if (template.contains("sub-domain.java.vm") && StringUtils.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } + else if (template.contains("mapper.java.vm")) + { + fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className); + } + else if (template.contains("service.java.vm")) + { + fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className); + } + else if (template.contains("serviceImpl.java.vm")) + { + fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className); + } + else if (template.contains("controller.java.vm")) + { + fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className); + } + else if (template.contains("mapper.xml.vm")) + { + fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className); + } + else if (template.contains("sql.vm")) + { + fileName = businessName + "Menu.sql"; + } + else if (template.contains("api.js.vm")) + { + fileName = StringUtils.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } + else if (template.contains("index.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("index-tree.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + return fileName; + } + + /** + * 获取包前缀 + * + * @param packageName 包名称 + * @return 包前缀名称 + */ + public static String getPackagePrefix(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + return StringUtils.substring(packageName, 0, lastIndex); + } + + /** + * 根据列类型获取导入包 + * + * @param genTable 业务表对象 + * @return 返回需要导入的包列表 + */ + public static HashSet getImportList(GenTable genTable) + { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + if (StringUtils.isNotNull(subGenTable)) + { + importList.add("java.util.List"); + } + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) + { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } + else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) + { + importList.add("java.math.BigDecimal"); + } + } + return importList; + } + + /** + * 根据列类型获取字典组 + * + * @param genTable 业务表对象 + * @return 返回字典组 + */ + public static String getDicts(GenTable genTable) + { + List columns = genTable.getColumns(); + Set dicts = new HashSet(); + addDicts(dicts, columns); + if (StringUtils.isNotNull(genTable.getSubTable())) + { + List subColumns = genTable.getSubTable().getColumns(); + addDicts(dicts, subColumns); + } + return StringUtils.join(dicts, ", "); + } + + /** + * 添加字典列表 + * + * @param dicts 字典列表 + * @param columns 列集合 + */ + public static void addDicts(Set dicts, List columns) + { + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny( + column.getHtmlType(), + new String[] { GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX })) + { + dicts.add("'" + column.getDictType() + "'"); + } + } + } + + /** + * 获取权限前缀 + * + * @param moduleName 模块名称 + * @param businessName 业务名称 + * @return 返回权限前缀 + */ + public static String getPermissionPrefix(String moduleName, String businessName) + { + return StringUtils.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID字段 + * + * @param paramsObj 生成其他选项 + * @return 上级菜单ID字段 + */ + public static String getParentMenuId(JSONObject paramsObj) + { + if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID) + && StringUtils.isNotEmpty(paramsObj.getString(GenConstants.PARENT_MENU_ID))) + { + return paramsObj.getString(GenConstants.PARENT_MENU_ID); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码 + * + * @param paramsObj 生成其他选项 + * @return 树编码 + */ + public static String getTreecode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树父编码 + * + * @param paramsObj 生成其他选项 + * @return 树父编码 + */ + public static String getTreeParentCode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树名称 + * + * @param paramsObj 生成其他选项 + * @return 树名称 + */ + public static String getTreeName(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_NAME)); + } + return StringUtils.EMPTY; + } + + /** + * 获取需要在哪一列上面显示展开按钮 + * + * @param genTable 业务表对象 + * @return 展开按钮列序号 + */ + public static int getExpandColumn(GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + int num = 0; + for (GenTableColumn column : genTable.getColumns()) + { + if (column.isList()) + { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) + { + break; + } + } + } + return num; + } +} diff --git a/ruoyi-generator/src/main/resources/generator.yml b/ruoyi-generator/src/main/resources/generator.yml new file mode 100644 index 0000000..7eae68e --- /dev/null +++ b/ruoyi-generator/src/main/resources/generator.yml @@ -0,0 +1,10 @@ +# 代码生成 +gen: + # 作者 + author: ruoyi + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: com.ruoyi.system + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: sys_ \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml b/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 0000000..66109de --- /dev/null +++ b/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select column_id, table_id, column_name, column_comment, column_type, java_type, java_field, is_pk, is_increment, is_required, is_insert, is_edit, is_list, is_query, query_type, html_type, dict_type, sort, create_by, create_time, update_by, update_time from gen_table_column + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() + ) + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() + + where column_id = #{columnId} + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml b/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml new file mode 100644 index 0000000..b605e90 --- /dev/null +++ b/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, create_by, create_time, update_by, update_time, remark from gen_table + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + package_name, + module_name, + business_name, + function_name, + function_author, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_author = #{functionAuthor}, + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where table_id = #{tableId} + + + + delete from gen_table where table_id in + + #{tableId} + + + + \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/vm/java/controller.java.vm b/ruoyi-generator/src/main/resources/vm/java/controller.java.vm new file mode 100644 index 0000000..bf88988 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/controller.java.vm @@ -0,0 +1,115 @@ +package ${packageName}.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.ruoyi.common.utils.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.page.TableDataInfo; +#elseif($table.tree) +#end + +/** + * ${functionName}Controller + * + * @author ${author} + * @date ${datetime} + */ +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController +{ + @Autowired + private I${ClassName}Service ${className}Service; + + /** + * 查询${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')") + @GetMapping("/list") +#if($table.crud || $table.sub) + public TableDataInfo list(${ClassName} ${className}) + { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } +#elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return success(list); + } +#end + + /** + * 导出${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')") + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * 获取${functionName}详细信息 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')") + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')") + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + /** + * 修改${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')") + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.update${ClassName}(${className})); + } + + /** + * 删除${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')") + @Log(title = "${functionName}", businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } +} diff --git a/ruoyi-generator/src/main/resources/vm/java/domain.java.vm b/ruoyi-generator/src/main/resources/vm/java/domain.java.vm new file mode 100644 index 0000000..bd51c17 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/domain.java.vm @@ -0,0 +1,105 @@ +package ${packageName}.domain; + +#foreach ($import in $importList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.domain.BaseEntity; +#elseif($table.tree) +import com.ruoyi.common.core.domain.TreeEntity; +#end + +/** + * ${functionName}对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +#if($table.crud || $table.sub) +#set($Entity="BaseEntity") +#elseif($table.tree) +#set($Entity="TreeEntity") +#end +public class ${ClassName} extends ${Entity} +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#if($table.sub) + /** $table.subTable.functionName信息 */ + private List<${subClassName}> ${subclassName}List; + +#end +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + +#if($table.sub) + public List<${subClassName}> get${subClassName}List() + { + return ${subclassName}List; + } + + public void set${subClassName}List(List<${subClassName}> ${subclassName}List) + { + this.${subclassName}List = ${subclassName}List; + } + +#end + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end +#if($table.sub) + .append("${subclassName}List", get${subClassName}List()) +#end + .toString(); + } +} diff --git a/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm b/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm new file mode 100644 index 0000000..7e7d7c2 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm @@ -0,0 +1,91 @@ +package ${packageName}.mapper; + +import java.util.List; +import ${packageName}.domain.${ClassName}; +#if($table.sub) +import ${packageName}.domain.${subClassName}; +#end + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface ${ClassName}Mapper +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +#if($table.sub) + + /** + * 批量删除${subTable.functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 批量新增${subTable.functionName} + * + * @param ${subclassName}List ${subTable.functionName}列表 + * @return 结果 + */ + public int batch${subClassName}(List<${subClassName}> ${subclassName}List); + + + /** + * 通过${functionName}主键删除${subTable.functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField}); +#end +} diff --git a/ruoyi-generator/src/main/resources/vm/java/service.java.vm b/ruoyi-generator/src/main/resources/vm/java/service.java.vm new file mode 100644 index 0000000..264882b --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/service.java.vm @@ -0,0 +1,61 @@ +package ${packageName}.service; + +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); +} diff --git a/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm b/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..14746e1 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm @@ -0,0 +1,169 @@ +package ${packageName}.service.impl; + +import java.util.List; +#foreach ($column in $columns) +#if($column.javaField == 'createTime' || $column.javaField == 'updateTime') +import com.ruoyi.common.utils.DateUtils; +#break +#end +#end +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +#if($table.sub) +import java.util.ArrayList; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.transaction.annotation.Transactional; +import ${packageName}.domain.${subClassName}; +#end +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service +{ + @Autowired + private ${ClassName}Mapper ${className}Mapper; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int insert${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'createTime') + ${className}.setCreateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + int rows = ${className}Mapper.insert${ClassName}(${className}); + insert${subClassName}(${className}); + return rows; +#else + return ${className}Mapper.insert${ClassName}(${className}); +#end + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int update${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'updateTime') + ${className}.setUpdateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}()); + insert${subClassName}(${className}); +#end + return ${className}Mapper.update${ClassName}(${className}); + } + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField}); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } +#if($table.sub) + + /** + * 新增${subTable.functionName}信息 + * + * @param ${className} ${functionName}对象 + */ + public void insert${subClassName}(${ClassName} ${className}) + { + List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List(); + ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}(); + if (StringUtils.isNotNull(${subclassName}List)) + { + List<${subClassName}> list = new ArrayList<${subClassName}>(); + for (${subClassName} ${subclassName} : ${subclassName}List) + { + ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField}); + list.add(${subclassName}); + } + if (list.size() > 0) + { + ${className}Mapper.batch${subClassName}(list); + } + } + } +#end +} diff --git a/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm b/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..a3f53eb --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm @@ -0,0 +1,76 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $subTable.columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end + .toString(); + } +} diff --git a/ruoyi-generator/src/main/resources/vm/js/api.js.vm b/ruoyi-generator/src/main/resources/vm/js/api.js.vm new file mode 100644 index 0000000..9295524 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/js/api.js.vm @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} diff --git a/ruoyi-generator/src/main/resources/vm/sql/sql.vm b/ruoyi-generator/src/main/resources/vm/sql/sql.vm new file mode 100644 index 0000000..0575583 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..4819c2a --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm @@ -0,0 +1,505 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 0000000..6296014 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,602 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..c54d62b --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,474 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..8b25665 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm @@ -0,0 +1,590 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/v3/readme.txt b/ruoyi-generator/src/main/resources/vm/vue/v3/readme.txt new file mode 100644 index 0000000..99239bb --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/v3/readme.txt @@ -0,0 +1 @@ +Èç¹ûʹÓõÄÊÇRuoYi-Vue3ǰ¶ˣ¬ÄÇôÐèҪ¸²¸ÇһÏ´ËĿ¼µÄģ°åindex.vue.vm¡¢index-tree.vue.vmÎļþµ½Éϼ¶vueĿ¼¡£ \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm b/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..0ceb3d8 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,135 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + \ No newline at end of file diff --git a/ruoyi-quartz/pom.xml b/ruoyi-quartz/pom.xml new file mode 100644 index 0000000..db0441f --- /dev/null +++ b/ruoyi-quartz/pom.xml @@ -0,0 +1,46 @@ + + + + ruoyi + com.ruoyi + 4.8.2 + + 4.0.0 + + ruoyi-quartz + + + quartz定时任务 + + + + + + + org.springframework.boot + spring-boot-starter-quartz + + + + + org.quartz-scheduler + quartz + + + com.mchange + c3p0 + + + + + + + com.ruoyi + ruoyi-common + + + + + \ No newline at end of file diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java new file mode 100644 index 0000000..a558170 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java @@ -0,0 +1,57 @@ +//package com.ruoyi.quartz.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.scheduling.quartz.SchedulerFactoryBean; +//import javax.sql.DataSource; +//import java.util.Properties; +// +///** +// * 定时任务配置(单机部署建议删除此类和qrtz数据库表,默认走内存会最高效) +// * +// * @author ruoyi +// */ +//@Configuration +//public class ScheduleConfig +//{ +// @Bean +// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) +// { +// SchedulerFactoryBean factory = new SchedulerFactoryBean(); +// factory.setDataSource(dataSource); +// +// // quartz参数 +// Properties prop = new Properties(); +// prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler"); +// prop.put("org.quartz.scheduler.instanceId", "AUTO"); +// // 线程池配置 +// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); +// prop.put("org.quartz.threadPool.threadCount", "20"); +// prop.put("org.quartz.threadPool.threadPriority", "5"); +// // JobStore配置 +// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); +// // 集群配置 +// prop.put("org.quartz.jobStore.isClustered", "true"); +// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); +// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1"); +// prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); +// +// // sqlserver 启用 +// // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?"); +// prop.put("org.quartz.jobStore.misfireThreshold", "12000"); +// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); +// factory.setQuartzProperties(prop); +// +// factory.setSchedulerName("RuoyiScheduler"); +// // 延时启动 +// factory.setStartupDelay(1); +// factory.setApplicationContextSchedulerContextKey("applicationContextKey"); +// // 可选,QuartzScheduler +// // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 +// factory.setOverwriteExistingJobs(true); +// // 设置自动启动,默认为true +// factory.setAutoStartup(true); +// +// return factory; +// } +//} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java new file mode 100644 index 0000000..e826dfd --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java @@ -0,0 +1,185 @@ +package com.ruoyi.quartz.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.service.ISysJobService; +import com.ruoyi.quartz.util.CronUtils; +import com.ruoyi.quartz.util.ScheduleUtils; + +/** + * 调度任务信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/job") +public class SysJobController extends BaseController +{ + @Autowired + private ISysJobService jobService; + + /** + * 查询定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJob sysJob) + { + startPage(); + List list = jobService.selectJobList(sysJob); + return getDataTable(list); + } + + /** + * 导出定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJob sysJob) + { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + util.exportExcel(response, list, "定时任务"); + } + + /** + * 获取定时任务详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) + { + return success(jobService.selectJobById(jobId)); + } + + /** + * 新增定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:add')") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setCreateBy(getUsername()); + return toAjax(jobService.insertJob(job)); + } + + /** + * 修改定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:edit')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setUpdateBy(getUsername()); + return toAjax(jobService.updateJob(job)); + } + + /** + * 定时任务状态修改 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException + { + SysJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** + * 定时任务立即执行一次 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public AjaxResult run(@RequestBody SysJob job) throws SchedulerException + { + boolean result = jobService.run(job); + return result ? success() : error("任务不存在或已过期!"); + } + + /** + * 删除定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException, TaskException + { + jobService.deleteJobByIds(jobIds); + return success(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java new file mode 100644 index 0000000..233361b --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java @@ -0,0 +1,92 @@ +package com.ruoyi.quartz.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 调度日志操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/jobLog") +public class SysJobLogController extends BaseController +{ + @Autowired + private ISysJobLogService jobLogService; + + /** + * 查询定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJobLog sysJobLog) + { + startPage(); + List list = jobLogService.selectJobLogList(sysJobLog); + return getDataTable(list); + } + + /** + * 导出定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "任务调度日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJobLog sysJobLog) + { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + util.exportExcel(response, list, "调度日志"); + } + + /** + * 根据调度编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobLogId}") + public AjaxResult getInfo(@PathVariable Long jobLogId) + { + return success(jobLogService.selectJobLogById(jobLogId)); + } + + + /** + * 删除定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobLogIds}") + public AjaxResult remove(@PathVariable Long[] jobLogIds) + { + return toAjax(jobLogService.deleteJobLogByIds(jobLogIds)); + } + + /** + * 清空定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "调度日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + jobLogService.cleanJobLog(); + return success(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java new file mode 100644 index 0000000..cea12dc --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java @@ -0,0 +1,171 @@ +package com.ruoyi.quartz.domain; + +import java.util.Date; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.quartz.util.CronUtils; + +/** + * 定时任务调度表 sys_job + * + * @author ruoyi + */ +public class SysJob extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 任务ID */ + @Excel(name = "任务序号", cellType = ColumnType.NUMERIC) + private Long jobId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** cron执行表达式 */ + @Excel(name = "执行表达式 ") + private String cronExpression; + + /** cron计划策略 */ + @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行") + private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT; + + /** 是否并发执行(0允许 1禁止) */ + @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止") + private String concurrent; + + /** 任务状态(0正常 1暂停) */ + @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停") + private String status; + + public Long getJobId() + { + return jobId; + } + + public void setJobId(Long jobId) + { + this.jobId = jobId; + } + + @NotBlank(message = "任务名称不能为空") + @Size(min = 0, max = 64, message = "任务名称不能超过64个字符") + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + @NotBlank(message = "调用目标字符串不能为空") + @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符") + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + @NotBlank(message = "Cron执行表达式不能为空") + @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符") + public String getCronExpression() + { + return cronExpression; + } + + public void setCronExpression(String cronExpression) + { + this.cronExpression = cronExpression; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + public Date getNextValidTime() + { + if (StringUtils.isNotEmpty(cronExpression)) + { + return CronUtils.getNextExecution(cronExpression); + } + return null; + } + + public String getMisfirePolicy() + { + return misfirePolicy; + } + + public void setMisfirePolicy(String misfirePolicy) + { + this.misfirePolicy = misfirePolicy; + } + + public String getConcurrent() + { + return concurrent; + } + + public void setConcurrent(String concurrent) + { + this.concurrent = concurrent; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobId", getJobId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("cronExpression", getCronExpression()) + .append("nextValidTime", getNextValidTime()) + .append("misfirePolicy", getMisfirePolicy()) + .append("concurrent", getConcurrent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java new file mode 100644 index 0000000..121c035 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java @@ -0,0 +1,155 @@ +package com.ruoyi.quartz.domain; + +import java.util.Date; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 定时任务调度日志表 sys_job_log + * + * @author ruoyi + */ +public class SysJobLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "日志序号") + private Long jobLogId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** 日志信息 */ + @Excel(name = "日志信息") + private String jobMessage; + + /** 执行状态(0正常 1失败) */ + @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败") + private String status; + + /** 异常信息 */ + @Excel(name = "异常信息") + private String exceptionInfo; + + /** 开始时间 */ + private Date startTime; + + /** 停止时间 */ + private Date stopTime; + + public Long getJobLogId() + { + return jobLogId; + } + + public void setJobLogId(Long jobLogId) + { + this.jobLogId = jobLogId; + } + + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + public String getJobMessage() + { + return jobMessage; + } + + public void setJobMessage(String jobMessage) + { + this.jobMessage = jobMessage; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getExceptionInfo() + { + return exceptionInfo; + } + + public void setExceptionInfo(String exceptionInfo) + { + this.exceptionInfo = exceptionInfo; + } + + public Date getStartTime() + { + return startTime; + } + + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + public Date getStopTime() + { + return stopTime; + } + + public void setStopTime(Date stopTime) + { + this.stopTime = stopTime; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobLogId", getJobLogId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("jobMessage", getJobMessage()) + .append("status", getStatus()) + .append("exceptionInfo", getExceptionInfo()) + .append("startTime", getStartTime()) + .append("stopTime", getStopTime()) + .toString(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java new file mode 100644 index 0000000..727d916 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java @@ -0,0 +1,64 @@ +package com.ruoyi.quartz.mapper; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJobLog; + +/** + * 调度任务日志信息 数据层 + * + * @author ruoyi + */ +public interface SysJobLogMapper +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 查询所有调度任务日志 + * + * @return 调度任务日志列表 + */ + public List selectJobLogAll(); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + * @return 结果 + */ + public int insertJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java new file mode 100644 index 0000000..20f45db --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java @@ -0,0 +1,67 @@ +package com.ruoyi.quartz.mapper; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 调度任务信息 数据层 + * + * @author ruoyi + */ +public interface SysJobMapper +{ + /** + * 查询调度任务日志集合 + * + * @param job 调度信息 + * @return 操作日志集合 + */ + public List selectJobList(SysJob job); + + /** + * 查询所有调度任务 + * + * @return 调度任务列表 + */ + public List selectJobAll(); + + /** + * 通过调度ID查询调度任务信息 + * + * @param jobId 调度ID + * @return 角色对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 通过调度ID删除调度任务信息 + * + * @param jobId 调度ID + * @return 结果 + */ + public int deleteJobById(Long jobId); + + /** + * 批量删除调度任务信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteJobByIds(Long[] ids); + + /** + * 修改调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int updateJob(SysJob job); + + /** + * 新增调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int insertJob(SysJob job); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java new file mode 100644 index 0000000..8546792 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java @@ -0,0 +1,56 @@ +package com.ruoyi.quartz.service; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJobLog; + +/** + * 定时任务调度日志信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobLogService +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + public void addJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的日志ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java new file mode 100644 index 0000000..437ade8 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java @@ -0,0 +1,102 @@ +package com.ruoyi.quartz.service; + +import java.util.List; +import org.quartz.SchedulerException; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务调度信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobService +{ + /** + * 获取quartz调度器的计划任务 + * + * @param job 调度信息 + * @return 调度任务集合 + */ + public List selectJobList(SysJob job); + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 暂停任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int pauseJob(SysJob job) throws SchedulerException; + + /** + * 恢复任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int resumeJob(SysJob job) throws SchedulerException; + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + * @return 结果 + */ + public int deleteJob(SysJob job) throws SchedulerException; + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + public void deleteJobByIds(Long[] jobIds) throws SchedulerException; + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + * @return 结果 + */ + public int changeStatus(SysJob job) throws SchedulerException; + + /** + * 立即运行任务 + * + * @param job 调度信息 + * @return 结果 + */ + public boolean run(SysJob job) throws SchedulerException; + + /** + * 新增任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int insertJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 更新任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int updateJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + public boolean checkCronExpressionIsValid(String cronExpression); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java new file mode 100644 index 0000000..812eed7 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java @@ -0,0 +1,87 @@ +package com.ruoyi.quartz.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.mapper.SysJobLogMapper; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 定时任务调度日志信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobLogServiceImpl implements ISysJobLogService +{ + @Autowired + private SysJobLogMapper jobLogMapper; + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + @Override + public List selectJobLogList(SysJobLog jobLog) + { + return jobLogMapper.selectJobLogList(jobLog); + } + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + @Override + public SysJobLog selectJobLogById(Long jobLogId) + { + return jobLogMapper.selectJobLogById(jobLogId); + } + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + @Override + public void addJobLog(SysJobLog jobLog) + { + jobLogMapper.insertJobLog(jobLog); + } + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteJobLogByIds(Long[] logIds) + { + return jobLogMapper.deleteJobLogByIds(logIds); + } + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + */ + @Override + public int deleteJobLogById(Long jobId) + { + return jobLogMapper.deleteJobLogById(jobId); + } + + /** + * 清空任务日志 + */ + @Override + public void cleanJobLog() + { + jobLogMapper.cleanJobLog(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java new file mode 100644 index 0000000..78ebef8 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java @@ -0,0 +1,261 @@ +package com.ruoyi.quartz.service.impl; + +import java.util.List; +import jakarta.annotation.PostConstruct; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.mapper.SysJobMapper; +import com.ruoyi.quartz.service.ISysJobService; +import com.ruoyi.quartz.util.CronUtils; +import com.ruoyi.quartz.util.ScheduleUtils; + +/** + * 定时任务调度信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobServiceImpl implements ISysJobService +{ + @Autowired + private Scheduler scheduler; + + @Autowired + private SysJobMapper jobMapper; + + /** + * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据) + */ + @PostConstruct + public void init() throws SchedulerException, TaskException + { + scheduler.clear(); + List jobList = jobMapper.selectJobAll(); + for (SysJob job : jobList) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + } + + /** + * 获取quartz调度器的计划任务列表 + * + * @param job 调度信息 + * @return + */ + @Override + public List selectJobList(SysJob job) + { + return jobMapper.selectJobList(job); + } + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + @Override + public SysJob selectJobById(Long jobId) + { + return jobMapper.selectJobById(jobId); + } + + /** + * 暂停任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int pauseJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 恢复任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int resumeJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.NORMAL.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + int rows = jobMapper.deleteJobById(jobId); + if (rows > 0) + { + scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJobByIds(Long[] jobIds) throws SchedulerException + { + for (Long jobId : jobIds) + { + SysJob job = jobMapper.selectJobById(jobId); + deleteJob(job); + } + } + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int changeStatus(SysJob job) throws SchedulerException + { + int rows = 0; + String status = job.getStatus(); + if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) + { + rows = resumeJob(job); + } + else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) + { + rows = pauseJob(job); + } + return rows; + } + + /** + * 立即运行任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean run(SysJob job) throws SchedulerException + { + boolean result = false; + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + SysJob properties = selectJobById(job.getJobId()); + // 参数 + JobDataMap dataMap = new JobDataMap(); + dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties); + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + result = true; + scheduler.triggerJob(jobKey, dataMap); + } + return result; + } + + /** + * 新增任务 + * + * @param job 调度信息 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertJob(SysJob job) throws SchedulerException, TaskException + { + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.insertJob(job); + if (rows > 0) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + return rows; + } + + /** + * 更新任务的时间表达式 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateJob(SysJob job) throws SchedulerException, TaskException + { + SysJob properties = selectJobById(job.getJobId()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + updateSchedulerJob(job, properties.getJobGroup()); + } + return rows; + } + + /** + * 更新任务 + * + * @param job 任务对象 + * @param jobGroup 任务组名 + */ + public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException + { + Long jobId = job.getJobId(); + // 判断是否存在 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(jobKey); + } + ScheduleUtils.createScheduleJob(scheduler, job); + } + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + @Override + public boolean checkCronExpressionIsValid(String cronExpression) + { + return CronUtils.isValid(cronExpression); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java new file mode 100644 index 0000000..853243b --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java @@ -0,0 +1,28 @@ +package com.ruoyi.quartz.task; + +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.StringUtils; + +/** + * 定时任务调度测试 + * + * @author ruoyi + */ +@Component("ryTask") +public class RyTask +{ + public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) + { + System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); + } + + public void ryParams(String params) + { + System.out.println("执行有参方法:" + params); + } + + public void ryNoParams() + { + System.out.println("执行无参方法"); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java new file mode 100644 index 0000000..731a5eb --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java @@ -0,0 +1,107 @@ +package com.ruoyi.quartz.util; + +import java.util.Date; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.utils.ExceptionUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 抽象quartz调用 + * + * @author ruoyi + */ +public abstract class AbstractQuartzJob implements Job +{ + private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class); + + /** + * 线程本地变量 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException + { + SysJob sysJob = new SysJob(); + BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); + try + { + before(context, sysJob); + if (sysJob != null) + { + doExecute(context, sysJob); + } + after(context, sysJob, null); + } + catch (Exception e) + { + log.error("任务执行异常 - :", e); + after(context, sysJob, e); + } + } + + /** + * 执行前 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void before(JobExecutionContext context, SysJob sysJob) + { + threadLocal.set(new Date()); + } + + /** + * 执行后 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void after(JobExecutionContext context, SysJob sysJob, Exception e) + { + Date startTime = threadLocal.get(); + threadLocal.remove(); + + final SysJobLog sysJobLog = new SysJobLog(); + sysJobLog.setJobName(sysJob.getJobName()); + sysJobLog.setJobGroup(sysJob.getJobGroup()); + sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); + sysJobLog.setStartTime(startTime); + sysJobLog.setStopTime(new Date()); + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + if (e != null) + { + sysJobLog.setStatus(Constants.FAIL); + String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); + sysJobLog.setExceptionInfo(errorMsg); + } + else + { + sysJobLog.setStatus(Constants.SUCCESS); + } + + // 写入数据库当中 + SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + } + + /** + * 执行方法,由子类重载 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + * @throws Exception 执行过程中的异常 + */ + protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception; +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java new file mode 100644 index 0000000..dd53839 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java @@ -0,0 +1,63 @@ +package com.ruoyi.quartz.util; + +import java.text.ParseException; +import java.util.Date; +import org.quartz.CronExpression; + +/** + * cron表达式工具类 + * + * @author ruoyi + * + */ +public class CronUtils +{ + /** + * 返回一个布尔值代表一个给定的Cron表达式的有效性 + * + * @param cronExpression Cron表达式 + * @return boolean 表达式是否有效 + */ + public static boolean isValid(String cronExpression) + { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 返回一个字符串值,表示该消息无效Cron表达式给出有效性 + * + * @param cronExpression Cron表达式 + * @return String 无效时返回表达式错误描述,如果有效返回null + */ + public static String getInvalidMessage(String cronExpression) + { + try + { + new CronExpression(cronExpression); + return null; + } + catch (ParseException pe) + { + return pe.getMessage(); + } + } + + /** + * 返回下一个执行时间根据给定的Cron表达式 + * + * @param cronExpression Cron表达式 + * @return Date 下次Cron表达式执行时间 + */ + public static Date getNextExecution(String cronExpression) + { + try + { + CronExpression cron = new CronExpression(cronExpression); + return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis())); + } + catch (ParseException e) + { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java new file mode 100644 index 0000000..e3dc62c --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java @@ -0,0 +1,182 @@ +package com.ruoyi.quartz.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 任务执行工具 + * + * @author ruoyi + */ +public class JobInvokeUtil +{ + /** + * 执行方法 + * + * @param sysJob 系统任务 + */ + public static void invokeMethod(SysJob sysJob) throws Exception + { + String invokeTarget = sysJob.getInvokeTarget(); + String beanName = getBeanName(invokeTarget); + String methodName = getMethodName(invokeTarget); + List methodParams = getMethodParams(invokeTarget); + + if (!isValidClassName(beanName)) + { + Object bean = SpringUtils.getBean(beanName); + invokeMethod(bean, methodName, methodParams); + } + else + { + Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance(); + invokeMethod(bean, methodName, methodParams); + } + } + + /** + * 调用任务方法 + * + * @param bean 目标对象 + * @param methodName 方法名称 + * @param methodParams 方法参数 + */ + private static void invokeMethod(Object bean, String methodName, List methodParams) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException + { + if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) + { + Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams)); + method.invoke(bean, getMethodParamsValue(methodParams)); + } + else + { + Method method = bean.getClass().getMethod(methodName); + method.invoke(bean); + } + } + + /** + * 校验是否为为class包名 + * + * @param invokeTarget 名称 + * @return true是 false否 + */ + public static boolean isValidClassName(String invokeTarget) + { + return StringUtils.countMatches(invokeTarget, ".") > 1; + } + + /** + * 获取bean名称 + * + * @param invokeTarget 目标字符串 + * @return bean名称 + */ + public static String getBeanName(String invokeTarget) + { + String beanName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringBeforeLast(beanName, "."); + } + + /** + * 获取bean方法 + * + * @param invokeTarget 目标字符串 + * @return method方法 + */ + public static String getMethodName(String invokeTarget) + { + String methodName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringAfterLast(methodName, "."); + } + + /** + * 获取method方法参数相关列表 + * + * @param invokeTarget 目标字符串 + * @return method方法相关参数列表 + */ + public static List getMethodParams(String invokeTarget) + { + String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")"); + if (StringUtils.isEmpty(methodStr)) + { + return null; + } + String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); + List classs = new LinkedList<>(); + for (int i = 0; i < methodParams.length; i++) + { + String str = StringUtils.trimToEmpty(methodParams[i]); + // String字符串类型,以'或"开头 + if (StringUtils.startsWithAny(str, "'", "\"")) + { + classs.add(new Object[] { StringUtils.substring(str, 1, str.length() - 1), String.class }); + } + // boolean布尔类型,等于true或者false + else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) + { + classs.add(new Object[] { Boolean.valueOf(str), Boolean.class }); + } + // long长整形,以L结尾 + else if (StringUtils.endsWith(str, "L")) + { + classs.add(new Object[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class }); + } + // double浮点类型,以D结尾 + else if (StringUtils.endsWith(str, "D")) + { + classs.add(new Object[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class }); + } + // 其他类型归类为整形 + else + { + classs.add(new Object[] { Integer.valueOf(str), Integer.class }); + } + } + return classs; + } + + /** + * 获取参数类型 + * + * @param methodParams 参数相关列表 + * @return 参数类型列表 + */ + public static Class[] getMethodParamsType(List methodParams) + { + Class[] classs = new Class[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Class) os[1]; + index++; + } + return classs; + } + + /** + * 获取参数值 + * + * @param methodParams 参数相关列表 + * @return 参数值列表 + */ + public static Object[] getMethodParamsValue(List methodParams) + { + Object[] classs = new Object[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Object) os[0]; + index++; + } + return classs; + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java new file mode 100644 index 0000000..5e13558 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java @@ -0,0 +1,21 @@ +package com.ruoyi.quartz.util; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务处理(禁止并发执行) + * + * @author ruoyi + * + */ +@DisallowConcurrentExecution +public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java new file mode 100644 index 0000000..e975326 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java @@ -0,0 +1,19 @@ +package com.ruoyi.quartz.util; + +import org.quartz.JobExecutionContext; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务处理(允许并发执行) + * + * @author ruoyi + * + */ +public class QuartzJobExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java new file mode 100644 index 0000000..21fedae --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java @@ -0,0 +1,141 @@ +package com.ruoyi.quartz.util; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.exception.job.TaskException.Code; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务工具类 + * + * @author ruoyi + * + */ +public class ScheduleUtils +{ + /** + * 得到quartz任务类 + * + * @param sysJob 执行计划 + * @return 具体执行任务类 + */ + private static Class getQuartzJobClass(SysJob sysJob) + { + boolean isConcurrent = "0".equals(sysJob.getConcurrent()); + return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class; + } + + /** + * 构建任务触发对象 + */ + public static TriggerKey getTriggerKey(Long jobId, String jobGroup) + { + return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 构建任务键对象 + */ + public static JobKey getJobKey(Long jobId, String jobGroup) + { + return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 创建定时任务 + */ + public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException + { + Class jobClass = getQuartzJobClass(job); + // 构建job信息 + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build(); + + // 表达式调度构建器 + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); + cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup)) + .withSchedule(cronScheduleBuilder).build(); + + // 放入参数,运行时的方法可以获取 + jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); + + // 判断是否存在 + if (scheduler.checkExists(getJobKey(jobId, jobGroup))) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(getJobKey(jobId, jobGroup)); + } + + // 判断任务是否过期 + if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) + { + // 执行调度任务 + scheduler.scheduleJob(jobDetail, trigger); + } + + // 暂停任务 + if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + } + + /** + * 设置定时任务策略 + */ + public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb) + throws TaskException + { + switch (job.getMisfirePolicy()) + { + case ScheduleConstants.MISFIRE_DEFAULT: + return cb; + case ScheduleConstants.MISFIRE_IGNORE_MISFIRES: + return cb.withMisfireHandlingInstructionIgnoreMisfires(); + case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED: + return cb.withMisfireHandlingInstructionFireAndProceed(); + case ScheduleConstants.MISFIRE_DO_NOTHING: + return cb.withMisfireHandlingInstructionDoNothing(); + default: + throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() + + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR); + } + } + + /** + * 检查包名是否为白名单配置 + * + * @param invokeTarget 目标字符串 + * @return 结果 + */ + public static boolean whiteList(String invokeTarget) + { + String packageName = StringUtils.substringBefore(invokeTarget, "("); + int count = StringUtils.countMatches(packageName, "."); + if (count > 1) + { + return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR); + } + Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]); + String beanPackageName = obj.getClass().getPackage().getName(); + return StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_WHITELIST_STR) + && !StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_ERROR_STR); + } +} diff --git a/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml new file mode 100644 index 0000000..e608e42 --- /dev/null +++ b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time + from sys_job_log + + + + + + + + + + delete from sys_job_log where job_log_id = #{jobLogId} + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + truncate table sys_job_log + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml new file mode 100644 index 0000000..5605c44 --- /dev/null +++ b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark + from sys_job + + + + + + + + + + delete from sys_job where job_id = #{jobId} + + + + delete from sys_job where job_id in + + #{jobId} + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml new file mode 100644 index 0000000..8c61a01 --- /dev/null +++ b/ruoyi-system/pom.xml @@ -0,0 +1,28 @@ + + + + ruoyi + com.ruoyi + 4.8.2 + + 4.0.0 + + ruoyi-system + + + system系统模块 + + + + + + + com.ruoyi + ruoyi-common + + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/config/ArchiveDaysConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/config/ArchiveDaysConfig.java new file mode 100644 index 0000000..4571e07 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/config/ArchiveDaysConfig.java @@ -0,0 +1,42 @@ +package com.ruoyi.system.config; + +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 流水归档保留天数配置读取工具。 + *

+ * 通过 sys_config 键 {@code flowArchiveRetainDays} 动态读取归档保留天数, + * 空/异常/非正数时兜底返回 7 天。供归档写入侧(FlowArchiveTask)与 + * 读取侧(各查询是否包含归档表的判定)统一使用,避免硬编码。 + */ +public final class ArchiveDaysConfig { + + /** 兜底保留天数 */ + private static final int DEFAULT_RETAIN_DAYS = 7; + + /** sys_config 配置键 */ + private static final String CONFIG_KEY = "flowArchiveRetainDays"; + + private ArchiveDaysConfig() { + } + + /** + * 获取流水归档保留天数。 + * + * @return 配置值(>0);空、解析异常或 ≤0 时返回兜底值 7 + */ + public static int getRetainDays() { + try { + ISysConfigService configService = SpringUtils.getBean(ISysConfigService.class); + String value = configService.selectConfigByKey(CONFIG_KEY); + if (value == null || value.trim().isEmpty()) { + return DEFAULT_RETAIN_DAYS; + } + int days = Integer.parseInt(value.trim()); + return days > 0 ? days : DEFAULT_RETAIN_DAYS; + } catch (Exception e) { + return DEFAULT_RETAIN_DAYS; + } + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java new file mode 100644 index 0000000..83f0703 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java @@ -0,0 +1,81 @@ +package com.ruoyi.system.domain; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 缓存信息 + * + * @author ruoyi + */ +public class SysCache +{ + /** 缓存名称 */ + private String cacheName = ""; + + /** 缓存键名 */ + private String cacheKey = ""; + + /** 缓存内容 */ + private String cacheValue = ""; + + /** 备注 */ + private String remark = ""; + + public SysCache() + { + + } + + public SysCache(String cacheName, String remark) + { + this.cacheName = cacheName; + this.remark = remark; + } + + public SysCache(String cacheName, String cacheKey, String cacheValue) + { + this.cacheName = StringUtils.replace(cacheName, ":", ""); + this.cacheKey = StringUtils.replace(cacheKey, cacheName, ""); + this.cacheValue = cacheValue; + } + + public String getCacheName() + { + return cacheName; + } + + public void setCacheName(String cacheName) + { + this.cacheName = cacheName; + } + + public String getCacheKey() + { + return cacheKey; + } + + public void setCacheKey(String cacheKey) + { + this.cacheKey = cacheKey; + } + + public String getCacheValue() + { + return cacheValue; + } + + public void setCacheValue(String cacheValue) + { + this.cacheValue = cacheValue; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java new file mode 100644 index 0000000..cae07be --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java @@ -0,0 +1,111 @@ +package com.ruoyi.system.domain; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 参数配置表 sys_config + * + * @author ruoyi + */ +public class SysConfig extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 参数主键 */ + @Excel(name = "参数主键", cellType = ColumnType.NUMERIC) + private Long configId; + + /** 参数名称 */ + @Excel(name = "参数名称") + private String configName; + + /** 参数键名 */ + @Excel(name = "参数键名") + private String configKey; + + /** 参数键值 */ + @Excel(name = "参数键值") + private String configValue; + + /** 系统内置(Y是 N否) */ + @Excel(name = "系统内置", readConverterExp = "Y=是,N=否") + private String configType; + + public Long getConfigId() + { + return configId; + } + + public void setConfigId(Long configId) + { + this.configId = configId; + } + + @NotBlank(message = "参数名称不能为空") + @Size(min = 0, max = 100, message = "参数名称不能超过100个字符") + public String getConfigName() + { + return configName; + } + + public void setConfigName(String configName) + { + this.configName = configName; + } + + @NotBlank(message = "参数键名长度不能为空") + @Size(min = 0, max = 100, message = "参数键名长度不能超过100个字符") + public String getConfigKey() + { + return configKey; + } + + public void setConfigKey(String configKey) + { + this.configKey = configKey; + } + + @NotBlank(message = "参数键值不能为空") + @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符") + public String getConfigValue() + { + return configValue; + } + + public void setConfigValue(String configValue) + { + this.configValue = configValue; + } + + public String getConfigType() + { + return configType; + } + + public void setConfigType(String configType) + { + this.configType = configType; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("configId", getConfigId()) + .append("configName", getConfigName()) + .append("configKey", getConfigKey()) + .append("configValue", getConfigValue()) + .append("configType", getConfigType()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java new file mode 100644 index 0000000..7fdea30 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java @@ -0,0 +1,144 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 系统访问记录表 sys_logininfor + * + * @author ruoyi + */ +public class SysLogininfor extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "序号", cellType = ColumnType.NUMERIC) + private Long infoId; + + /** 用户账号 */ + @Excel(name = "用户账号") + private String userName; + + /** 登录状态 0成功 1失败 */ + @Excel(name = "登录状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** 登录IP地址 */ + @Excel(name = "登录地址") + private String ipaddr; + + /** 登录地点 */ + @Excel(name = "登录地点") + private String loginLocation; + + /** 浏览器类型 */ + @Excel(name = "浏览器") + private String browser; + + /** 操作系统 */ + @Excel(name = "操作系统") + private String os; + + /** 提示消息 */ + @Excel(name = "提示消息") + private String msg; + + /** 访问时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date loginTime; + + public Long getInfoId() + { + return infoId; + } + + public void setInfoId(Long infoId) + { + this.infoId = infoId; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public Date getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Date loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java new file mode 100644 index 0000000..8603ad8 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java @@ -0,0 +1,102 @@ +package com.ruoyi.system.domain; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.xss.Xss; + +/** + * 通知公告表 sys_notice + * + * @author ruoyi + */ +public class SysNotice extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 公告ID */ + private Long noticeId; + + /** 公告标题 */ + private String noticeTitle; + + /** 公告类型(1通知 2公告) */ + private String noticeType; + + /** 公告内容 */ + private String noticeContent; + + /** 公告状态(0正常 1关闭) */ + private String status; + + public Long getNoticeId() + { + return noticeId; + } + + public void setNoticeId(Long noticeId) + { + this.noticeId = noticeId; + } + + public void setNoticeTitle(String noticeTitle) + { + this.noticeTitle = noticeTitle; + } + + @Xss(message = "公告标题不能包含脚本字符") + @NotBlank(message = "公告标题不能为空") + @Size(min = 0, max = 50, message = "公告标题不能超过50个字符") + public String getNoticeTitle() + { + return noticeTitle; + } + + public void setNoticeType(String noticeType) + { + this.noticeType = noticeType; + } + + public String getNoticeType() + { + return noticeType; + } + + public void setNoticeContent(String noticeContent) + { + this.noticeContent = noticeContent; + } + + public String getNoticeContent() + { + return noticeContent; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("noticeId", getNoticeId()) + .append("noticeTitle", getNoticeTitle()) + .append("noticeType", getNoticeType()) + .append("noticeContent", getNoticeContent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java new file mode 100644 index 0000000..f6761df --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java @@ -0,0 +1,269 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 操作日志记录表 oper_log + * + * @author ruoyi + */ +public class SysOperLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 日志主键 */ + @Excel(name = "操作序号", cellType = ColumnType.NUMERIC) + private Long operId; + + /** 操作模块 */ + @Excel(name = "操作模块") + private String title; + + /** 业务类型(0其它 1新增 2修改 3删除) */ + @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据") + private Integer businessType; + + /** 业务类型数组 */ + private Integer[] businessTypes; + + /** 请求方法 */ + @Excel(name = "请求方法") + private String method; + + /** 请求方式 */ + @Excel(name = "请求方式") + private String requestMethod; + + /** 操作类别(0其它 1后台用户 2手机端用户) */ + @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户") + private Integer operatorType; + + /** 操作人员 */ + @Excel(name = "操作人员") + private String operName; + + /** 部门名称 */ + @Excel(name = "部门名称") + private String deptName; + + /** 请求url */ + @Excel(name = "请求地址") + private String operUrl; + + /** 操作地址 */ + @Excel(name = "操作地址") + private String operIp; + + /** 操作地点 */ + @Excel(name = "操作地点") + private String operLocation; + + /** 请求参数 */ + @Excel(name = "请求参数") + private String operParam; + + /** 返回参数 */ + @Excel(name = "返回参数") + private String jsonResult; + + /** 操作状态(0正常 1异常) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=异常") + private Integer status; + + /** 错误消息 */ + @Excel(name = "错误消息") + private String errorMsg; + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date operTime; + + /** 消耗时间 */ + @Excel(name = "消耗时间", suffix = "毫秒") + private Long costTime; + + public Long getOperId() + { + return operId; + } + + public void setOperId(Long operId) + { + this.operId = operId; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public Integer getBusinessType() + { + return businessType; + } + + public void setBusinessType(Integer businessType) + { + this.businessType = businessType; + } + + public Integer[] getBusinessTypes() + { + return businessTypes; + } + + public void setBusinessTypes(Integer[] businessTypes) + { + this.businessTypes = businessTypes; + } + + public String getMethod() + { + return method; + } + + public void setMethod(String method) + { + this.method = method; + } + + public String getRequestMethod() + { + return requestMethod; + } + + public void setRequestMethod(String requestMethod) + { + this.requestMethod = requestMethod; + } + + public Integer getOperatorType() + { + return operatorType; + } + + public void setOperatorType(Integer operatorType) + { + this.operatorType = operatorType; + } + + public String getOperName() + { + return operName; + } + + public void setOperName(String operName) + { + this.operName = operName; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getOperUrl() + { + return operUrl; + } + + public void setOperUrl(String operUrl) + { + this.operUrl = operUrl; + } + + public String getOperIp() + { + return operIp; + } + + public void setOperIp(String operIp) + { + this.operIp = operIp; + } + + public String getOperLocation() + { + return operLocation; + } + + public void setOperLocation(String operLocation) + { + this.operLocation = operLocation; + } + + public String getOperParam() + { + return operParam; + } + + public void setOperParam(String operParam) + { + this.operParam = operParam; + } + + public String getJsonResult() + { + return jsonResult; + } + + public void setJsonResult(String jsonResult) + { + this.jsonResult = jsonResult; + } + + public Integer getStatus() + { + return status; + } + + public void setStatus(Integer status) + { + this.status = status; + } + + public String getErrorMsg() + { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) + { + this.errorMsg = errorMsg; + } + + public Date getOperTime() + { + return operTime; + } + + public void setOperTime(Date operTime) + { + this.operTime = operTime; + } + + public Long getCostTime() + { + return costTime; + } + + public void setCostTime(Long costTime) + { + this.costTime = costTime; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java new file mode 100644 index 0000000..62dbd47 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java @@ -0,0 +1,124 @@ +package com.ruoyi.system.domain; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 岗位表 sys_post + * + * @author ruoyi + */ +public class SysPost extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 岗位序号 */ + @Excel(name = "岗位序号", cellType = ColumnType.NUMERIC) + private Long postId; + + /** 岗位编码 */ + @Excel(name = "岗位编码") + private String postCode; + + /** 岗位名称 */ + @Excel(name = "岗位名称") + private String postName; + + /** 岗位排序 */ + @Excel(name = "岗位排序") + private Integer postSort; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 用户是否存在此岗位标识 默认不存在 */ + private boolean flag = false; + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @NotBlank(message = "岗位编码不能为空") + @Size(min = 0, max = 64, message = "岗位编码长度不能超过64个字符") + public String getPostCode() + { + return postCode; + } + + public void setPostCode(String postCode) + { + this.postCode = postCode; + } + + @NotBlank(message = "岗位名称不能为空") + @Size(min = 0, max = 50, message = "岗位名称长度不能超过50个字符") + public String getPostName() + { + return postName; + } + + public void setPostName(String postName) + { + this.postName = postName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getPostSort() + { + return postSort; + } + + public void setPostSort(Integer postSort) + { + this.postSort = postSort; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("postId", getPostId()) + .append("postCode", getPostCode()) + .append("postName", getPostName()) + .append("postSort", getPostSort()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java new file mode 100644 index 0000000..47b21bf --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和部门关联 sys_role_dept + * + * @author ruoyi + */ +public class SysRoleDept +{ + /** 角色ID */ + private Long roleId; + + /** 部门ID */ + private Long deptId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("deptId", getDeptId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java new file mode 100644 index 0000000..de10a74 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和菜单关联 sys_role_menu + * + * @author ruoyi + */ +public class SysRoleMenu +{ + /** 角色ID */ + private Long roleId; + + /** 菜单ID */ + private Long menuId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("menuId", getMenuId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java new file mode 100644 index 0000000..2bbd318 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java @@ -0,0 +1,113 @@ +package com.ruoyi.system.domain; + +/** + * 当前在线会话 + * + * @author ruoyi + */ +public class SysUserOnline +{ + /** 会话编号 */ + private String tokenId; + + /** 部门名称 */ + private String deptName; + + /** 用户名称 */ + private String userName; + + /** 登录IP地址 */ + private String ipaddr; + + /** 登录地址 */ + private String loginLocation; + + /** 浏览器类型 */ + private String browser; + + /** 操作系统 */ + private String os; + + /** 登录时间 */ + private Long loginTime; + + public String getTokenId() + { + return tokenId; + } + + public void setTokenId(String tokenId) + { + this.tokenId = tokenId; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java new file mode 100644 index 0000000..6e8c416 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和岗位关联 sys_user_post + * + * @author ruoyi + */ +public class SysUserPost +{ + /** 用户ID */ + private Long userId; + + /** 岗位ID */ + private Long postId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("postId", getPostId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java new file mode 100644 index 0000000..4d15810 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和角色关联 sys_user_role + * + * @author ruoyi + */ +public class SysUserRole +{ + /** 用户ID */ + private Long userId; + + /** 角色ID */ + private Long roleId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("roleId", getRoleId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java new file mode 100644 index 0000000..a5d5fdc --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java @@ -0,0 +1,106 @@ +package com.ruoyi.system.domain.vo; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 路由显示信息 + * + * @author ruoyi + */ +public class MetaVo +{ + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + /** + * 内链地址(http(s)://开头) + */ + private String link; + + public MetaVo() + { + } + + public MetaVo(String title, String icon) + { + this.title = title; + this.icon = icon; + } + + public MetaVo(String title, String icon, boolean noCache) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + + public MetaVo(String title, String icon, String link) + { + this.title = title; + this.icon = icon; + this.link = link; + } + + public MetaVo(String title, String icon, boolean noCache, String link) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + if (StringUtils.ishttp(link)) + { + this.link = link; + } + } + + public boolean isNoCache() + { + return noCache; + } + + public void setNoCache(boolean noCache) + { + this.noCache = noCache; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public String getLink() + { + return link; + } + + public void setLink(String link) + { + this.link = link; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java new file mode 100644 index 0000000..afff8c9 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java @@ -0,0 +1,148 @@ +package com.ruoyi.system.domain.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; + +/** + * 路由配置信息 + * + * @author ruoyi + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVo +{ + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 路由参数:如 {"id": 1, "name": "ry"} + */ + private String query; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVo meta; + + /** + * 子路由 + */ + private List children; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public boolean getHidden() + { + return hidden; + } + + public void setHidden(boolean hidden) + { + this.hidden = hidden; + } + + public String getRedirect() + { + return redirect; + } + + public void setRedirect(String redirect) + { + this.redirect = redirect; + } + + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public Boolean getAlwaysShow() + { + return alwaysShow; + } + + public void setAlwaysShow(Boolean alwaysShow) + { + this.alwaysShow = alwaysShow; + } + + public MetaVo getMeta() + { + return meta; + } + + public void setMeta(MetaVo meta) + { + this.meta = meta; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java new file mode 100644 index 0000000..13d49d6 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java @@ -0,0 +1,76 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysConfig; + +/** + * 参数配置 数据层 + * + * @author ruoyi + */ +public interface SysConfigMapper +{ + /** + * 查询参数配置信息 + * + * @param config 参数配置信息 + * @return 参数配置信息 + */ + public SysConfig selectConfig(SysConfig config); + + /** + * 通过ID查询配置 + * + * @param configId 参数ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数配置信息 + */ + public SysConfig checkConfigKeyUnique(String configKey); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 删除参数配置 + * + * @param configId 参数ID + * @return 结果 + */ + public int deleteConfigById(Long configId); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public int deleteConfigByIds(Long[] configIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java new file mode 100644 index 0000000..384a9b6 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java @@ -0,0 +1,118 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysDept; + +/** + * 部门管理 数据层 + * + * @author ruoyi + */ +public interface SysDeptMapper +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @param deptCheckStrictly 部门树选择项是否关联显示 + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(@Param("roleId") Long roleId, @Param("deptCheckStrictly") boolean deptCheckStrictly); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门 + * + * @param deptId 部门ID + * @return 部门列表 + */ + public List selectChildrenDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public int hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 + */ + public int checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param deptName 部门名称 + * @param parentId 父部门ID + * @return 结果 + */ + public SysDept checkDeptNameUnique(@Param("deptName") String deptName, @Param("parentId") Long parentId); + + /** + * 新增部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 修改所在部门正常状态 + * + * @param deptIds 部门ID组 + */ + public void updateDeptStatusNormal(Long[] deptIds); + + /** + * 修改子元素关系 + * + * @param depts 子元素 + * @return 结果 + */ + public int updateDeptChildren(@Param("depts") List depts); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java new file mode 100644 index 0000000..25e7848 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java @@ -0,0 +1,106 @@ +package com.ruoyi.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysDictData; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +@Mapper +public interface SysDictDataMapper +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型批量查询字典数据 + * + * @param dictTypes 字典类型集合 + * @return 字典数据集合信息 + */ + public List selectDictDataByTypes(@Param("dictTypes") List dictTypes); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(@Param("dictType") String dictType, @Param("dictValue") String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据 + */ + public int countDictDataByType(String dictType); + + /** + * 通过字典ID删除字典数据信息 + * + * @param dictCode 字典数据ID + * @return 结果 + */ + public int deleteDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + public int deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); + + /** + * 同步修改字典类型 + * + * @param oldDictType 旧字典类型 + * @param newDictType 新旧字典类型 + * @return 结果 + */ + public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java new file mode 100644 index 0000000..5fb48fb --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java @@ -0,0 +1,83 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictType; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +public interface SysDictTypeMapper +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 通过字典ID删除字典信息 + * + * @param dictId 字典ID + * @return 结果 + */ + public int deleteDictTypeById(Long dictId); + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + public int deleteDictTypeByIds(Long[] dictIds); + + /** + * 新增字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public SysDictType checkDictTypeUnique(String dictType); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java new file mode 100644 index 0000000..629866f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java @@ -0,0 +1,42 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 数据层 + * + * @author ruoyi + */ +public interface SysLogininforMapper +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + * + * @return 结果 + */ + public int cleanLogininfor(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..99c0c50 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java @@ -0,0 +1,125 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysMenu; + +/** + * 菜单表 数据层 + * + * @author ruoyi + */ +public interface SysMenuMapper +{ + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu); + + /** + * 根据用户所有权限 + * + * @return 权限列表 + */ + public List selectMenuPerms(); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuListByUserId(SysMenu menu); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public List selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public List selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单 + * + * @return 菜单列表 + */ + public List selectMenuTreeAll(); + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @param menuCheckStrictly 菜单树选择项是否关联显示 + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int hasChildByMenuId(Long menuId); + + /** + * 新增菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menuName 菜单名称 + * @param parentId 父菜单ID + * @return 结果 + */ + public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java new file mode 100644 index 0000000..c34f0a2 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysNotice; + +/** + * 通知公告表 数据层 + * + * @author ruoyi + */ +public interface SysNoticeMapper +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 批量删除公告 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java new file mode 100644 index 0000000..2ae6457 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志 数据层 + * + * @author ruoyi + */ +public interface SysOperLogMapper +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java new file mode 100644 index 0000000..19be227 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java @@ -0,0 +1,99 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysPost; + +/** + * 岗位信息 数据层 + * + * @author ruoyi + */ +public interface SysPostMapper +{ + /** + * 查询岗位数据集合 + * + * @param post 岗位信息 + * @return 岗位数据集合 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public List selectPostsByUserName(String userName); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 修改岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); + + /** + * 新增岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 校验岗位名称 + * + * @param postName 岗位名称 + * @return 结果 + */ + public SysPost checkPostNameUnique(String postName); + + /** + * 校验岗位编码 + * + * @param postCode 岗位编码 + * @return 结果 + */ + public SysPost checkPostCodeUnique(String postCode); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java new file mode 100644 index 0000000..f9d3a2f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysRoleDept; + +/** + * 角色与部门关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleDeptMapper +{ + /** + * 通过角色ID删除角色和部门关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleDeptByRoleId(Long roleId); + + /** + * 批量删除角色部门关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleDept(Long[] ids); + + /** + * 查询部门使用数量 + * + * @param deptId 部门ID + * @return 结果 + */ + public int selectCountRoleDeptByDeptId(Long deptId); + + /** + * 批量新增角色部门信息 + * + * @param roleDeptList 角色部门列表 + * @return 结果 + */ + public int batchRoleDept(List roleDeptList); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..cf2bd8c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java @@ -0,0 +1,107 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysRole; + +/** + * 角色表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMapper +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 根据用户ID查询角色 + * + * @param userName 用户名 + * @return 角色列表 + */ + public List selectRolesByUserName(String userName); + + /** + * 校验角色名称是否唯一 + * + * @param roleName 角色名称 + * @return 角色信息 + */ + public SysRole checkRoleNameUnique(String roleName); + + /** + * 校验角色权限是否唯一 + * + * @param roleKey 角色权限 + * @return 角色信息 + */ + public SysRole checkRoleKeyUnique(String roleKey); + + /** + * 修改角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 新增角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..e92d9fb --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,52 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysRoleMenu; + +/** + * 角色与菜单关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMenuMapper +{ + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int checkMenuExistRole(Long menuId); + + /** + * 通过角色ID删除角色和菜单关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleMenuByRoleId(Long roleId); + + /** + * 通过菜单ID删除角色和菜单关联 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteRoleMenuByMenuId(Long menuId); + + /** + * 批量删除角色菜单关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleMenu(Long[] ids); + + /** + * 批量新增角色菜单信息 + * + * @param roleMenuList 角色菜单列表 + * @return 结果 + */ + public int batchRoleMenu(List roleMenuList); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..76e1c79 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java @@ -0,0 +1,127 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 用户表 数据层 + * + * @author ruoyi + */ +public interface SysUserMapper +{ + /** + * 根据条件分页查询用户列表 + * + * @param sysUser 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser sysUser); + + /** + * 根据条件分页查询已配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 修改用户头像 + * + * @param userName 用户名 + * @param avatar 头像地址 + * @return 结果 + */ + public int updateUserAvatar(@Param("userName") String userName, @Param("avatar") String avatar); + + /** + * 重置用户密码 + * + * @param userName 用户名 + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(@Param("userName") String userName, @Param("password") String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public SysUser checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param phonenumber 手机号码 + * @return 结果 + */ + public SysUser checkPhoneUnique(String phonenumber); + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + public SysUser checkEmailUnique(String email); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java new file mode 100644 index 0000000..e08991d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysUserPost; + +/** + * 用户与岗位关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserPostMapper +{ + /** + * 通过用户ID删除用户和岗位关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserPostByUserId(Long userId); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 批量删除用户和岗位关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserPost(Long[] ids); + + /** + * 批量新增用户岗位信息 + * + * @param userPostList 用户角色列表 + * @return 结果 + */ + public int batchUserPost(List userPostList); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..3143ec8 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,62 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.system.domain.SysUserRole; + +/** + * 用户与角色关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserRoleMapper +{ + /** + * 通过用户ID删除用户和角色关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserRoleByUserId(Long userId); + + /** + * 批量删除用户和角色关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserRole(Long[] ids); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 批量新增用户角色信息 + * + * @param userRoleList 用户角色列表 + * @return 结果 + */ + public int batchUserRole(List userRoleList); + + /** + * 删除用户和角色关联信息 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteUserRoleInfo(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java new file mode 100644 index 0000000..b307776 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java @@ -0,0 +1,89 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysConfig; + +/** + * 参数配置 服务层 + * + * @author ruoyi + */ +public interface ISysConfigService +{ + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数键值 + */ + public String selectConfigByKey(String configKey); + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + public boolean selectCaptchaEnabled(); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + public void deleteConfigByIds(Long[] configIds); + + /** + * 加载参数缓存数据 + */ + public void loadingConfigCache(); + + /** + * 清空参数缓存数据 + */ + public void clearConfigCache(); + + /** + * 重置参数缓存数据 + */ + public void resetConfigCache(); + + /** + * 校验参数键名是否唯一 + * + * @param config 参数信息 + * @return 结果 + */ + public boolean checkConfigKeyUnique(SysConfig config); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java new file mode 100644 index 0000000..f228208 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java @@ -0,0 +1,124 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysDept; + +/** + * 部门管理 服务层 + * + * @author ruoyi + */ +public interface ISysDeptService +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + public List selectDeptTreeList(SysDept dept); + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + public List buildDeptTree(List depts); + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + public List buildDeptTreeSelect(List depts); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(Long roleId); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在部门子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public boolean hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + public boolean checkDeptNameUnique(SysDept dept); + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + public void checkDeptDataScope(Long deptId); + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java new file mode 100644 index 0000000..9bc4f13 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictData; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictDataService +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(String dictType, String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + public void deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java new file mode 100644 index 0000000..01c1c1d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java @@ -0,0 +1,98 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.domain.entity.SysDictType; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictTypeService +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 批量删除字典信息 + * + * @param dictIds 需要删除的字典ID + */ + public void deleteDictTypeByIds(Long[] dictIds); + + /** + * 加载字典缓存数据 + */ + public void loadingDictCache(); + + /** + * 清空字典缓存数据 + */ + public void clearDictCache(); + + /** + * 重置字典缓存数据 + */ + public void resetDictCache(); + + /** + * 新增保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public boolean checkDictTypeUnique(SysDictType dictType); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java new file mode 100644 index 0000000..ce3151d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java @@ -0,0 +1,40 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 服务层 + * + * @author ruoyi + */ +public interface ISysLogininforService +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + */ + public void cleanLogininfor(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java new file mode 100644 index 0000000..7d60696 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java @@ -0,0 +1,144 @@ +package com.ruoyi.system.service; + +import java.util.List; +import java.util.Set; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.system.domain.vo.RouterVo; + +/** + * 菜单 业务层 + * + * @author ruoyi + */ +public interface ISysMenuService +{ + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(Long userId); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu, Long userId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectMenuPermsByUserId(Long userId); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public Set selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询菜单树信息 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(Long roleId); + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + public List buildMenus(List menus); + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildMenuTree(List menus); + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + public List buildMenuTreeSelect(List menus); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean hasChildByMenuId(Long menuId); + + /** + * 查询菜单是否存在角色 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkMenuExistRole(Long menuId); + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean checkMenuNameUnique(SysMenu menu); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java new file mode 100644 index 0000000..47ce1b7 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysNotice; + +/** + * 公告 服务层 + * + * @author ruoyi + */ +public interface ISysNoticeService +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 删除公告信息 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java new file mode 100644 index 0000000..4fd8e5a --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志 服务层 + * + * @author ruoyi + */ +public interface ISysOperLogService +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java new file mode 100644 index 0000000..84779bf --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java @@ -0,0 +1,99 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysPost; + +/** + * 岗位信息 服务层 + * + * @author ruoyi + */ +public interface ISysPostService +{ + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位列表 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 校验岗位名称 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostNameUnique(SysPost post); + + /** + * 校验岗位编码 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostCodeUnique(SysPost post); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java new file mode 100644 index 0000000..6c29f09 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java @@ -0,0 +1,173 @@ +package com.ruoyi.system.service; + +import java.util.List; +import java.util.Set; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.system.domain.SysUserRole; + +/** + * 角色业务层 + * + * @author ruoyi + */ +public interface ISysRoleService +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色列表 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolesByUserId(Long userId); + + /** + * 根据用户ID查询角色权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleNameUnique(SysRole role); + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleKeyUnique(SysRole role); + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + public void checkRoleAllowed(SysRole role); + + /** + * 校验角色是否有数据权限 + * + * @param roleId 角色id + */ + public void checkRoleDataScope(Long roleId); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRoleStatus(SysRole role); + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int authDataScope(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteAuthUser(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + public int deleteAuthUsers(Long roleId, Long[] userIds); + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int insertAuthUsers(Long roleId, Long[] userIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java new file mode 100644 index 0000000..8eb5448 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.service; + +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.system.domain.SysUserOnline; + +/** + * 在线用户 服务层 + * + * @author ruoyi + */ +public interface ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user); + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user); + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user); + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + public SysUserOnline loginUserToUserOnline(LoginUser user); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java new file mode 100644 index 0000000..10bc2ab --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java @@ -0,0 +1,206 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 用户 业务层 + * + * @author ruoyi + */ +public interface ISysUserService +{ + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser user); + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserRoleGroup(String userName); + + /** + * 根据用户ID查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserPostGroup(String userName); + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkUserNameUnique(SysUser user); + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkPhoneUnique(SysUser user); + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkEmailUnique(SysUser user); + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + public void checkUserAllowed(SysUser user); + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + public void checkUserDataScope(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean registerUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserAuth(Long userId, Long[] roleIds); + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserStatus(SysUser user); + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserProfile(SysUser user); + + /** + * 修改用户头像 + * + * @param userName 用户名 + * @param avatar 头像地址 + * @return 结果 + */ + public boolean updateUserAvatar(String userName, String avatar); + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + public int resetPwd(SysUser user); + + /** + * 重置用户密码 + * + * @param userName 用户名 + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(String userName, String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + public String importUser(List userList, Boolean isUpdateSupport, String operName); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..83750e7 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,232 @@ +package com.ruoyi.system.service.impl; + +import java.util.Collection; +import java.util.List; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.annotation.DataSource; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.DataSourceType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.mapper.SysConfigMapper; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 参数配置 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysConfigServiceImpl implements ISysConfigService +{ + @Autowired + private SysConfigMapper configMapper; + + @Autowired + private RedisCache redisCache; + + /** + * 项目启动时,初始化参数到缓存 + */ + @PostConstruct + public void init() + { + loadingConfigCache(); + } + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + @Override + @DataSource(DataSourceType.MASTER) + public SysConfig selectConfigById(Long configId) + { + SysConfig config = new SysConfig(); + config.setConfigId(configId); + return configMapper.selectConfig(config); + } + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数key + * @return 参数键值 + */ + @Override + public String selectConfigByKey(String configKey) + { + String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey))); + if (StringUtils.isNotEmpty(configValue)) + { + return configValue; + } + SysConfig config = new SysConfig(); + config.setConfigKey(configKey); + SysConfig retConfig = configMapper.selectConfig(config); + if (StringUtils.isNotNull(retConfig)) + { + redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue()); + return retConfig.getConfigValue(); + } + return StringUtils.EMPTY; + } + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + @Override + public boolean selectCaptchaEnabled() + { + String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled"); + if (StringUtils.isEmpty(captchaEnabled)) + { + return true; + } + return Convert.toBool(captchaEnabled); + } + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + @Override + public List selectConfigList(SysConfig config) + { + return configMapper.selectConfigList(config); + } + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int insertConfig(SysConfig config) + { + int row = configMapper.insertConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int updateConfig(SysConfig config) + { + SysConfig temp = configMapper.selectConfigById(config.getConfigId()); + if (!StringUtils.equals(temp.getConfigKey(), config.getConfigKey())) + { + redisCache.deleteObject(getCacheKey(temp.getConfigKey())); + } + + int row = configMapper.updateConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + @Override + public void deleteConfigByIds(Long[] configIds) + { + for (Long configId : configIds) + { + SysConfig config = selectConfigById(configId); + if (StringUtils.equals(UserConstants.YES, config.getConfigType())) + { + throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey())); + } + configMapper.deleteConfigById(configId); + redisCache.deleteObject(getCacheKey(config.getConfigKey())); + } + } + + /** + * 加载参数缓存数据 + */ + @Override + public void loadingConfigCache() + { + List configsList = configMapper.selectConfigList(new SysConfig()); + for (SysConfig config : configsList) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + } + + /** + * 清空参数缓存数据 + */ + @Override + public void clearConfigCache() + { + Collection keys = redisCache.keys(CacheConstants.SYS_CONFIG_KEY + "*"); + redisCache.deleteObject(keys); + } + + /** + * 重置参数缓存数据 + */ + @Override + public void resetConfigCache() + { + clearConfigCache(); + loadingConfigCache(); + } + + /** + * 校验参数键名是否唯一 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public boolean checkConfigKeyUnique(SysConfig config) + { + Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId(); + SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey()); + if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + private String getCacheKey(String configKey) + { + return CacheConstants.SYS_CONFIG_KEY + configKey; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java new file mode 100644 index 0000000..f7fb088 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,338 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.mapper.SysDeptMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.service.ISysDeptService; + +/** + * 部门管理 服务实现 + * + * @author ruoyi + */ +@Service +public class SysDeptServiceImpl implements ISysDeptService +{ + @Autowired + private SysDeptMapper deptMapper; + + @Autowired + private SysRoleMapper roleMapper; + + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + @Override + @DataScope(deptAlias = "d") + public List selectDeptList(SysDept dept) + { + return deptMapper.selectDeptList(dept); + } + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + @Override + public List selectDeptTreeList(SysDept dept) + { + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + return buildDeptTreeSelect(depts); + } + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + @Override + public List buildDeptTree(List depts) + { + List returnList = new ArrayList(); + List tempList = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList()); + for (SysDept dept : depts) + { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(dept.getParentId())) + { + recursionFn(depts, dept); + returnList.add(dept); + } + } + if (returnList.isEmpty()) + { + returnList = depts; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + @Override + public List buildDeptTreeSelect(List depts) + { + List deptTrees = buildDeptTree(depts); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + @Override + public List selectDeptListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly()); + } + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + @Override + public SysDept selectDeptById(Long deptId) + { + return deptMapper.selectDeptById(deptId); + } + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + @Override + public int selectNormalChildrenDeptById(Long deptId) + { + return deptMapper.selectNormalChildrenDeptById(deptId); + } + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public boolean hasChildByDeptId(Long deptId) + { + int result = deptMapper.hasChildByDeptId(deptId); + return result > 0; + } + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + @Override + public boolean checkDeptExistUser(Long deptId) + { + int result = deptMapper.checkDeptExistUser(deptId); + return result > 0; + } + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public boolean checkDeptNameUnique(SysDept dept) + { + Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId(); + SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId()); + if (StringUtils.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + @Override + public void checkDeptDataScope(Long deptId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + SysDept dept = new SysDept(); + dept.setDeptId(deptId); + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + if (StringUtils.isEmpty(depts)) + { + throw new ServiceException("没有权限访问部门数据!"); + } + } + } + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int insertDept(SysDept dept) + { + SysDept info = deptMapper.selectDeptById(dept.getParentId()); + // 如果父节点不为正常状态,则不允许新增子节点 + if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())) + { + throw new ServiceException("部门停用,不允许新增"); + } + dept.setAncestors(info.getAncestors() + "," + dept.getParentId()); + return deptMapper.insertDept(dept); + } + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int updateDept(SysDept dept) + { + SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId()); + SysDept oldDept = deptMapper.selectDeptById(dept.getDeptId()); + if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept)) + { + String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId(); + String oldAncestors = oldDept.getAncestors(); + dept.setAncestors(newAncestors); + updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors); + } + int result = deptMapper.updateDept(dept); + if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors()) + && !StringUtils.equals("0", dept.getAncestors())) + { + // 如果该部门是启用状态,则启用该部门的所有上级部门 + updateParentDeptStatusNormal(dept); + } + return result; + } + + /** + * 修改该部门的父级部门状态 + * + * @param dept 当前部门 + */ + private void updateParentDeptStatusNormal(SysDept dept) + { + String ancestors = dept.getAncestors(); + Long[] deptIds = Convert.toLongArray(ancestors); + deptMapper.updateDeptStatusNormal(deptIds); + } + + /** + * 修改子元素关系 + * + * @param deptId 被修改的部门ID + * @param newAncestors 新的父ID集合 + * @param oldAncestors 旧的父ID集合 + */ + public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors) + { + List children = deptMapper.selectChildrenDeptById(deptId); + for (SysDept child : children) + { + child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + } + if (children.size() > 0) + { + deptMapper.updateDeptChildren(children); + } + } + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public int deleteDeptById(Long deptId) + { + return deptMapper.deleteDeptById(deptId); + } + + /** + * 递归列表 + */ + private void recursionFn(List list, SysDept t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysDept tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysDept t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysDept n = (SysDept) it.next(); + if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysDept t) + { + return getChildList(list, t).size() > 0; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java new file mode 100644 index 0000000..fced569 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java @@ -0,0 +1,111 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.service.ISysDictDataService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictDataServiceImpl implements ISysDictDataService +{ + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataList(SysDictData dictData) + { + return dictDataMapper.selectDictDataList(dictData); + } + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + @Override + public String selectDictLabel(String dictType, String dictValue) + { + return dictDataMapper.selectDictLabel(dictType, dictValue); + } + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + @Override + public SysDictData selectDictDataById(Long dictCode) + { + return dictDataMapper.selectDictDataById(dictCode); + } + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + @Override + public void deleteDictDataByIds(Long[] dictCodes) + { + for (Long dictCode : dictCodes) + { + SysDictData data = selectDictDataById(dictCode); + dictDataMapper.deleteDictDataById(dictCode); + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + } + + /** + * 新增保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int insertDictData(SysDictData data) + { + int row = dictDataMapper.insertDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } + + /** + * 修改保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int updateDictData(SysDictData data) + { + int row = dictDataMapper.updateDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java new file mode 100644 index 0000000..37928c4 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java @@ -0,0 +1,223 @@ +package com.ruoyi.system.service.impl; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.domain.entity.SysDictType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.mapper.SysDictTypeMapper; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictTypeServiceImpl implements ISysDictTypeService +{ + @Autowired + private SysDictTypeMapper dictTypeMapper; + + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 项目启动时,初始化字典到缓存 + */ + @PostConstruct + public void init() + { + loadingDictCache(); + } + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeList(SysDictType dictType) + { + return dictTypeMapper.selectDictTypeList(dictType); + } + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeAll() + { + return dictTypeMapper.selectDictTypeAll(); + } + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataByType(String dictType) + { + List dictDatas = DictUtils.getDictCache(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + return dictDatas; + } + dictDatas = dictDataMapper.selectDictDataByType(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + DictUtils.setDictCache(dictType, dictDatas); + return dictDatas; + } + return null; + } + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeById(Long dictId) + { + return dictTypeMapper.selectDictTypeById(dictId); + } + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeByType(String dictType) + { + return dictTypeMapper.selectDictTypeByType(dictType); + } + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + */ + @Override + public void deleteDictTypeByIds(Long[] dictIds) + { + for (Long dictId : dictIds) + { + SysDictType dictType = selectDictTypeById(dictId); + if (dictDataMapper.countDictDataByType(dictType.getDictType()) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", dictType.getDictName())); + } + dictTypeMapper.deleteDictTypeById(dictId); + DictUtils.removeDictCache(dictType.getDictType()); + } + } + + /** + * 加载字典缓存数据 + */ + @Override + public void loadingDictCache() + { + SysDictData dictData = new SysDictData(); + dictData.setStatus("0"); + Map> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType)); + for (Map.Entry> entry : dictDataMap.entrySet()) + { + DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList())); + } + } + + /** + * 清空字典缓存数据 + */ + @Override + public void clearDictCache() + { + DictUtils.clearDictCache(); + } + + /** + * 重置字典缓存数据 + */ + @Override + public void resetDictCache() + { + clearDictCache(); + loadingDictCache(); + } + + /** + * 新增保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + public int insertDictType(SysDictType dict) + { + int row = dictTypeMapper.insertDictType(dict); + if (row > 0) + { + DictUtils.setDictCache(dict.getDictType(), null); + } + return row; + } + + /** + * 修改保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + @Transactional + public int updateDictType(SysDictType dict) + { + SysDictType oldDict = dictTypeMapper.selectDictTypeById(dict.getDictId()); + dictDataMapper.updateDictDataType(oldDict.getDictType(), dict.getDictType()); + int row = dictTypeMapper.updateDictType(dict); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(dict.getDictType()); + DictUtils.setDictCache(dict.getDictType(), dictDatas); + } + return row; + } + + /** + * 校验字典类型称是否唯一 + * + * @param dict 字典类型 + * @return 结果 + */ + @Override + public boolean checkDictTypeUnique(SysDictType dict) + { + Long dictId = StringUtils.isNull(dict.getDictId()) ? -1L : dict.getDictId(); + SysDictType dictType = dictTypeMapper.checkDictTypeUnique(dict.getDictType()); + if (StringUtils.isNotNull(dictType) && dictType.getDictId().longValue() != dictId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java new file mode 100644 index 0000000..216aecb --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java @@ -0,0 +1,65 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.mapper.SysLogininforMapper; +import com.ruoyi.system.service.ISysLogininforService; + +/** + * 系统访问日志情况信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysLogininforServiceImpl implements ISysLogininforService +{ + + @Autowired + private SysLogininforMapper logininforMapper; + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + @Override + public void insertLogininfor(SysLogininfor logininfor) + { + logininforMapper.insertLogininfor(logininfor); + } + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + @Override + public List selectLogininforList(SysLogininfor logininfor) + { + return logininforMapper.selectLogininforList(logininfor); + } + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + @Override + public int deleteLogininforByIds(Long[] infoIds) + { + return logininforMapper.deleteLogininforByIds(infoIds); + } + + /** + * 清空系统登录日志 + */ + @Override + public void cleanLogininfor() + { + logininforMapper.cleanLogininfor(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..6b16015 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,533 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.vo.MetaVo; +import com.ruoyi.system.domain.vo.RouterVo; +import com.ruoyi.system.mapper.SysMenuMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysRoleMenuMapper; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 菜单 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysMenuServiceImpl implements ISysMenuService +{ + public static final String PREMISSION_STRING = "perms[\"{0}\"]"; + + @Autowired + private SysMenuMapper menuMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + @Override + public List selectMenuList(Long userId) + { + return selectMenuList(new SysMenu(), userId); + } + + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + @Override + public List selectMenuList(SysMenu menu, Long userId) + { + List menuList = null; + // 管理员显示所有菜单信息 + if (SysUser.isAdmin(userId)) + { + menuList = menuMapper.selectMenuList(menu); + } + else + { + menu.getParams().put("userId", userId); + menuList = menuMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByUserId(Long userId) + { + List perms = menuMapper.selectMenuPermsByUserId(userId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByRoleId(Long roleId) + { + List perms = menuMapper.selectMenuPermsByRoleId(roleId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户名称 + * @return 菜单列表 + */ + @Override + public List selectMenuTreeByUserId(Long userId) + { + List menus = null; + if (SecurityUtils.isAdmin(userId)) + { + menus = menuMapper.selectMenuTreeAll(); + } + else + { + menus = menuMapper.selectMenuTreeByUserId(userId); + } + return getChildPerms(menus, 0); + } + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + @Override + public List selectMenuListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return menuMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly()); + } + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + @Override + public List buildMenus(List menus) + { + List routers = new LinkedList(); + for (SysMenu menu : menus) + { + RouterVo router = new RouterVo(); + router.setHidden("1".equals(menu.getVisible())); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setQuery(menu.getQuery()); + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + List cMenus = menu.getChildren(); + if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType())) + { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } + else if (isMenuFrame(menu)) + { + router.setMeta(null); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(StringUtils.capitalize(menu.getPath())); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + children.setQuery(menu.getQuery()); + childrenList.add(children); + router.setChildren(childrenList); + } + else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) + { + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon())); + router.setPath("/"); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + String routerPath = innerLinkReplaceEach(menu.getPath()); + children.setPath(routerPath); + children.setComponent(UserConstants.INNER_LINK); + children.setName(StringUtils.capitalize(routerPath)); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildMenuTree(List menus) + { + List returnList = new ArrayList(); + List tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList()); + for (Iterator iterator = menus.iterator(); iterator.hasNext();) + { + SysMenu menu = (SysMenu) iterator.next(); + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(menu.getParentId())) + { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) + { + returnList = menus; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + @Override + public List buildMenuTreeSelect(List menus) + { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + @Override + public SysMenu selectMenuById(Long menuId) + { + return menuMapper.selectMenuById(menuId); + } + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean hasChildByMenuId(Long menuId) + { + int result = menuMapper.hasChildByMenuId(menuId); + return result > 0; + } + + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean checkMenuExistRole(Long menuId) + { + int result = roleMenuMapper.checkMenuExistRole(menuId); + return result > 0; + } + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int insertMenu(SysMenu menu) + { + return menuMapper.insertMenu(menu); + } + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int updateMenu(SysMenu menu) + { + return menuMapper.updateMenu(menu); + } + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public int deleteMenuById(Long menuId) + { + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByMenuId(menuId); + return menuMapper.deleteMenuById(menuId); + } + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public boolean checkMenuNameUnique(SysMenu menu) + { + Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId()); + if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 获取路由名称 + * + * @param menu 菜单信息 + * @return 路由名称 + */ + public String getRouteName(SysMenu menu) + { + String routerName = StringUtils.capitalize(menu.getPath()); + // 非外链并且是一级目录(类型为目录) + if (isMenuFrame(menu)) + { + routerName = StringUtils.EMPTY; + } + return routerName; + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) + { + String routerPath = menu.getPath(); + // 内链打开外网方式 + if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + routerPath = innerLinkReplaceEach(routerPath); + } + // 非外链并且是一级目录(类型为目录) + if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType()) + && UserConstants.NO_FRAME.equals(menu.getIsFrame())) + { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) + { + routerPath = "/"; + } + return routerPath; + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) + { + String component = UserConstants.LAYOUT; + if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) + { + component = menu.getComponent(); + } + else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + component = UserConstants.INNER_LINK; + } + else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) + { + component = UserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) + { + return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType()) + && menu.getIsFrame().equals(UserConstants.NO_FRAME); + } + + /** + * 是否为内链组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isInnerLink(SysMenu menu) + { + return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath()); + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) + { + return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()); + } + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, int parentId) + { + List returnList = new ArrayList(); + for (Iterator iterator = list.iterator(); iterator.hasNext();) + { + SysMenu t = (SysMenu) iterator.next(); + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (t.getParentId() == parentId) + { + recursionFn(list, t); + returnList.add(t); + } + } + return returnList; + } + + /** + * 递归列表 + * + * @param list 分类表 + * @param t 子节点 + */ + private void recursionFn(List list, SysMenu t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysMenu t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysMenu n = (SysMenu) it.next(); + if (n.getParentId().longValue() == t.getMenuId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysMenu t) + { + return getChildList(list, t).size() > 0; + } + + /** + * 内链域名特殊字符替换 + * + * @return 替换后的内链域名 + */ + public String innerLinkReplaceEach(String path) + { + return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, "." }, + new String[] { "", "", "", "/" }); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java new file mode 100644 index 0000000..765438b --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java @@ -0,0 +1,92 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysNotice; +import com.ruoyi.system.mapper.SysNoticeMapper; +import com.ruoyi.system.service.ISysNoticeService; + +/** + * 公告 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysNoticeServiceImpl implements ISysNoticeService +{ + @Autowired + private SysNoticeMapper noticeMapper; + + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + @Override + public SysNotice selectNoticeById(Long noticeId) + { + return noticeMapper.selectNoticeById(noticeId); + } + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + @Override + public List selectNoticeList(SysNotice notice) + { + return noticeMapper.selectNoticeList(notice); + } + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int insertNotice(SysNotice notice) + { + return noticeMapper.insertNotice(notice); + } + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int updateNotice(SysNotice notice) + { + return noticeMapper.updateNotice(notice); + } + + /** + * 删除公告对象 + * + * @param noticeId 公告ID + * @return 结果 + */ + @Override + public int deleteNoticeById(Long noticeId) + { + return noticeMapper.deleteNoticeById(noticeId); + } + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + @Override + public int deleteNoticeByIds(Long[] noticeIds) + { + return noticeMapper.deleteNoticeByIds(noticeIds); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java new file mode 100644 index 0000000..5489815 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java @@ -0,0 +1,76 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.mapper.SysOperLogMapper; +import com.ruoyi.system.service.ISysOperLogService; + +/** + * 操作日志 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysOperLogServiceImpl implements ISysOperLogService +{ + @Autowired + private SysOperLogMapper operLogMapper; + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + @Override + public void insertOperlog(SysOperLog operLog) + { + operLogMapper.insertOperlog(operLog); + } + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + @Override + public List selectOperLogList(SysOperLog operLog) + { + return operLogMapper.selectOperLogList(operLog); + } + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + @Override + public int deleteOperLogByIds(Long[] operIds) + { + return operLogMapper.deleteOperLogByIds(operIds); + } + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + @Override + public SysOperLog selectOperLogById(Long operId) + { + return operLogMapper.selectOperLogById(operId); + } + + /** + * 清空操作日志 + */ + @Override + public void cleanOperLog() + { + operLogMapper.cleanOperLog(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java new file mode 100644 index 0000000..5e5fe06 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java @@ -0,0 +1,178 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.mapper.SysPostMapper; +import com.ruoyi.system.mapper.SysUserPostMapper; +import com.ruoyi.system.service.ISysPostService; + +/** + * 岗位信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysPostServiceImpl implements ISysPostService +{ + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位信息集合 + */ + @Override + public List selectPostList(SysPost post) + { + return postMapper.selectPostList(post); + } + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + @Override + public List selectPostAll() + { + return postMapper.selectPostAll(); + } + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + @Override + public SysPost selectPostById(Long postId) + { + return postMapper.selectPostById(postId); + } + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + @Override + public List selectPostListByUserId(Long userId) + { + return postMapper.selectPostListByUserId(userId); + } + + /** + * 校验岗位名称是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostNameUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostNameUnique(post.getPostName()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验岗位编码是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostCodeUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostCodeUnique(post.getPostCode()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int countUserPostById(Long postId) + { + return userPostMapper.countUserPostById(postId); + } + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int deletePostById(Long postId) + { + return postMapper.deletePostById(postId); + } + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + @Override + public int deletePostByIds(Long[] postIds) + { + for (Long postId : postIds) + { + SysPost post = selectPostById(postId); + if (countUserPostById(postId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName())); + } + } + return postMapper.deletePostByIds(postIds); + } + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int insertPost(SysPost post) + { + return postMapper.insertPost(post); + } + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int updatePost(SysPost post) + { + return postMapper.updatePost(post); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..d6cee80 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,424 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysRoleDept; +import com.ruoyi.system.domain.SysRoleMenu; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.mapper.SysRoleDeptMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysRoleMenuMapper; +import com.ruoyi.system.mapper.SysUserRoleMapper; +import com.ruoyi.system.service.ISysRoleService; + +/** + * 角色 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysRoleServiceImpl implements ISysRoleService +{ + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysRoleDeptMapper roleDeptMapper; + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + @Override + @DataScope(deptAlias = "d") + public List selectRoleList(SysRole role) + { + return roleMapper.selectRoleList(role); + } + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + @Override + public List selectRolesByUserId(Long userId) + { + List userRoles = roleMapper.selectRolePermissionByUserId(userId); + List roles = selectRoleAll(); + for (SysRole role : roles) + { + for (SysRole userRole : userRoles) + { + if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) + { + role.setFlag(true); + break; + } + } + } + return roles; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectRolePermissionByUserId(Long userId) + { + List perms = roleMapper.selectRolePermissionByUserId(userId); + Set permsSet = new HashSet<>(); + for (SysRole perm : perms) + { + if (StringUtils.isNotNull(perm)) + { + permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(","))); + } + } + return permsSet; + } + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + @Override + public List selectRoleAll() + { + return SpringUtils.getAopProxy(this).selectRoleList(new SysRole()); + } + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + @Override + public List selectRoleListByUserId(Long userId) + { + return roleMapper.selectRoleListByUserId(userId); + } + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + @Override + public SysRole selectRoleById(Long roleId) + { + return roleMapper.selectRoleById(roleId); + } + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleNameUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleKeyUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + @Override + public void checkRoleAllowed(SysRole role) + { + if (StringUtils.isNotNull(role.getRoleId()) && role.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员角色"); + } + } + + /** + * 校验角色是否有数据权限 + * + * @param roleId 角色id + */ + @Override + public void checkRoleDataScope(Long roleId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + SysRole role = new SysRole(); + role.setRoleId(roleId); + List roles = SpringUtils.getAopProxy(this).selectRoleList(role); + if (StringUtils.isEmpty(roles)) + { + throw new ServiceException("没有权限访问角色数据!"); + } + } + } + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + public int countUserRoleByRoleId(Long roleId) + { + return userRoleMapper.countUserRoleByRoleId(roleId); + } + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int insertRole(SysRole role) + { + // 新增角色信息 + roleMapper.insertRole(role); + return insertRoleMenu(role); + } + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int updateRole(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId()); + return insertRoleMenu(role); + } + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public int updateRoleStatus(SysRole role) + { + return roleMapper.updateRole(role); + } + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int authDataScope(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId()); + // 新增角色和部门信息(数据权限) + return insertRoleDept(role); + } + + /** + * 新增角色菜单信息 + * + * @param role 角色对象 + */ + public int insertRoleMenu(SysRole role) + { + int rows = 1; + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long menuId : role.getMenuIds()) + { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(role.getRoleId()); + rm.setMenuId(menuId); + list.add(rm); + } + if (list.size() > 0) + { + rows = roleMenuMapper.batchRoleMenu(list); + } + return rows; + } + + /** + * 新增角色部门信息(数据权限) + * + * @param role 角色对象 + */ + public int insertRoleDept(SysRole role) + { + int rows = 1; + // 新增角色与部门(数据权限)管理 + List list = new ArrayList(); + for (Long deptId : role.getDeptIds()) + { + SysRoleDept rd = new SysRoleDept(); + rd.setRoleId(role.getRoleId()); + rd.setDeptId(deptId); + list.add(rd); + } + if (list.size() > 0) + { + rows = roleDeptMapper.batchRoleDept(list); + } + return rows; + } + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleById(Long roleId) + { + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(roleId); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(roleId); + return roleMapper.deleteRoleById(roleId); + } + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleByIds(Long[] roleIds) + { + for (Long roleId : roleIds) + { + checkRoleAllowed(new SysRole(roleId)); + checkRoleDataScope(roleId); + SysRole role = selectRoleById(roleId); + if (countUserRoleByRoleId(roleId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenu(roleIds); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDept(roleIds); + return roleMapper.deleteRoleByIds(roleIds); + } + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + @Override + public int deleteAuthUser(SysUserRole userRole) + { + return userRoleMapper.deleteUserRoleInfo(userRole); + } + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + @Override + public int deleteAuthUsers(Long roleId, Long[] userIds) + { + return userRoleMapper.deleteUserRoleInfos(roleId, userIds); + } + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要授权的用户数据ID + * @return 结果 + */ + @Override + public int insertAuthUsers(Long roleId, Long[] userIds) + { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long userId : userIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + return userRoleMapper.batchUserRole(list); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java new file mode 100644 index 0000000..f80a877 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java @@ -0,0 +1,96 @@ +package com.ruoyi.system.service.impl; + +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysUserOnline; +import com.ruoyi.system.service.ISysUserOnlineService; + +/** + * 在线用户 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserOnlineServiceImpl implements ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user) + { + if (StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + @Override + public SysUserOnline loginUserToUserOnline(LoginUser user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUser())) + { + return null; + } + SysUserOnline sysUserOnline = new SysUserOnline(); + sysUserOnline.setTokenId(user.getToken()); + sysUserOnline.setUserName(user.getUsername()); + sysUserOnline.setIpaddr(user.getIpaddr()); + sysUserOnline.setLoginLocation(user.getLoginLocation()); + sysUserOnline.setBrowser(user.getBrowser()); + sysUserOnline.setOs(user.getOs()); + sysUserOnline.setLoginTime(user.getLoginTime()); + if (StringUtils.isNotNull(user.getUser().getDept())) + { + sysUserOnline.setDeptName(user.getUser().getDept().getDeptName()); + } + return sysUserOnline; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..1d22c23 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,544 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import jakarta.validation.Validator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanValidators; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.domain.SysUserPost; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.mapper.SysPostMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysUserMapper; +import com.ruoyi.system.mapper.SysUserPostMapper; +import com.ruoyi.system.mapper.SysUserRoleMapper; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserServiceImpl implements ISysUserService +{ + private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class); + + @Autowired + private SysUserMapper userMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + protected Validator validator; + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUserList(SysUser user) + { + return userMapper.selectUserList(user); + } + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectAllocatedList(SysUser user) + { + return userMapper.selectAllocatedList(user); + } + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUnallocatedList(SysUser user) + { + return userMapper.selectUnallocatedList(user); + } + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + @Override + public SysUser selectUserByUserName(String userName) + { + return userMapper.selectUserByUserName(userName); + } + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + @Override + public SysUser selectUserById(Long userId) + { + return userMapper.selectUserById(userId); + } + + /** + * 查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserRoleGroup(String userName) + { + List list = roleMapper.selectRolesByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(",")); + } + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserPostGroup(String userName) + { + List list = postMapper.selectPostsByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysPost::getPostName).collect(Collectors.joining(",")); + } + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean checkUserNameUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkUserNameUnique(user.getUserName()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkPhoneUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkEmailUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkEmailUnique(user.getEmail()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + @Override + public void checkUserAllowed(SysUser user) + { + if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员用户"); + } + } + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + @Override + public void checkUserDataScope(Long userId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + SysUser user = new SysUser(); + user.setUserId(userId); + List users = SpringUtils.getAopProxy(this).selectUserList(user); + if (StringUtils.isEmpty(users)) + { + throw new ServiceException("没有权限访问用户数据!"); + } + } + } + + /** + * 新增保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int insertUser(SysUser user) + { + // 新增用户信息 + int rows = userMapper.insertUser(user); + // 新增用户岗位关联 + insertUserPost(user); + // 新增用户与角色管理 + insertUserRole(user); + return rows; + } + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean registerUser(SysUser user) + { + return userMapper.insertUser(user) > 0; + } + + /** + * 修改保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int updateUser(SysUser user) + { + Long userId = user.getUserId(); + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 新增用户与角色管理 + insertUserRole(user); + // 删除用户与岗位关联 + userPostMapper.deleteUserPostByUserId(userId); + // 新增用户与岗位管理 + insertUserPost(user); + return userMapper.updateUser(user); + } + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + @Override + @Transactional + public void insertUserAuth(Long userId, Long[] roleIds) + { + userRoleMapper.deleteUserRoleByUserId(userId); + insertUserRole(userId, roleIds); + } + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserStatus(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserProfile(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 修改用户头像 + * + * @param userName 用户名 + * @param avatar 头像地址 + * @return 结果 + */ + @Override + public boolean updateUserAvatar(String userName, String avatar) + { + return userMapper.updateUserAvatar(userName, avatar) > 0; + } + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int resetPwd(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 重置用户密码 + * + * @param userName 用户名 + * @param password 密码 + * @return 结果 + */ + @Override + public int resetUserPwd(String userName, String password) + { + return userMapper.resetUserPwd(userName, password); + } + + /** + * 新增用户角色信息 + * + * @param user 用户对象 + */ + public void insertUserRole(SysUser user) + { + this.insertUserRole(user.getUserId(), user.getRoleIds()); + } + + /** + * 新增用户岗位信息 + * + * @param user 用户对象 + */ + public void insertUserPost(SysUser user) + { + Long[] posts = user.getPostIds(); + if (StringUtils.isNotEmpty(posts)) + { + // 新增用户与岗位管理 + List list = new ArrayList(posts.length); + for (Long postId : posts) + { + SysUserPost up = new SysUserPost(); + up.setUserId(user.getUserId()); + up.setPostId(postId); + list.add(up); + } + userPostMapper.batchUserPost(list); + } + } + + /** + * 新增用户角色信息 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserRole(Long userId, Long[] roleIds) + { + if (StringUtils.isNotEmpty(roleIds)) + { + // 新增用户与角色管理 + List list = new ArrayList(roleIds.length); + for (Long roleId : roleIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + userRoleMapper.batchUserRole(list); + } + } + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserById(Long userId) + { + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 删除用户与岗位表 + userPostMapper.deleteUserPostByUserId(userId); + return userMapper.deleteUserById(userId); + } + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserByIds(Long[] userIds) + { + for (Long userId : userIds) + { + checkUserAllowed(new SysUser(userId)); + checkUserDataScope(userId); + } + // 删除用户与角色关联 + userRoleMapper.deleteUserRole(userIds); + // 删除用户与岗位关联 + userPostMapper.deleteUserPost(userIds); + return userMapper.deleteUserByIds(userIds); + } + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + @Override + public String importUser(List userList, Boolean isUpdateSupport, String operName) + { + if (StringUtils.isNull(userList) || userList.size() == 0) + { + throw new ServiceException("导入用户数据不能为空!"); + } + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + String password = configService.selectConfigByKey("sys.user.initPassword"); + for (SysUser user : userList) + { + try + { + // 验证是否存在这个用户 + SysUser u = userMapper.selectUserByUserName(user.getUserName()); + if (StringUtils.isNull(u)) + { + BeanValidators.validateWithException(validator, user); + user.setPassword(SecurityUtils.encryptPassword(password)); + user.setCreateBy(operName); + userMapper.insertUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); + } + else if (isUpdateSupport) + { + BeanValidators.validateWithException(validator, user); + checkUserAllowed(u); + checkUserDataScope(u.getUserId()); + user.setUserId(u.getUserId()); + user.setUpdateBy(operName); + userMapper.updateUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); + } + else + { + failureNum++; + failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); + } + } + catch (Exception e) + { + failureNum++; + String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 导入失败:"; + failureMsg.append(msg + e.getMessage()); + log.error(msg, e); + } + } + if (failureNum > 0) + { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + throw new ServiceException(failureMsg.toString()); + } + else + { + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } +} diff --git a/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml new file mode 100644 index 0000000..ca39f47 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark + from sys_config + + + + + + + and config_id = #{configId} + + + and config_key = #{configKey} + + + + + + + + + + + + + + insert into sys_config ( + config_name, + config_key, + config_value, + config_type, + create_by, + remark, + create_time + )values( + #{configName}, + #{configKey}, + #{configValue}, + #{configType}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_config + + config_name = #{configName}, + config_key = #{configKey}, + config_value = #{configValue}, + config_type = #{configType}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where config_id = #{configId} + + + + delete from sys_config where config_id = #{configId} + + + + delete from sys_config where config_id in + + #{configId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml new file mode 100644 index 0000000..cf439f6 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time + from sys_dept d + + + + + + + + + + + + + + + + + + + + insert into sys_dept( + dept_id, + parent_id, + dept_name, + ancestors, + order_num, + leader, + phone, + email, + status, + create_by, + create_time + )values( + #{deptId}, + #{parentId}, + #{deptName}, + #{ancestors}, + #{orderNum}, + #{leader}, + #{phone}, + #{email}, + #{status}, + #{createBy}, + sysdate() + ) + + + + update sys_dept + + parent_id = #{parentId}, + dept_name = #{deptName}, + ancestors = #{ancestors}, + order_num = #{orderNum}, + leader = #{leader}, + phone = #{phone}, + email = #{email}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where dept_id = #{deptId} + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + + + update sys_dept set status = '0' where dept_id in + + #{deptId} + + + + + update sys_dept set del_flag = '2' where dept_id = #{deptId} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml new file mode 100644 index 0000000..6f24cb1 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark + from sys_dict_data + + + + + + + + + + + + + + + + delete from sys_dict_data where dict_code = #{dictCode} + + + + delete from sys_dict_data where dict_code in + + #{dictCode} + + + + + update sys_dict_data + + dict_sort = #{dictSort}, + dict_label = #{dictLabel}, + dict_value = #{dictValue}, + dict_type = #{dictType}, + css_class = #{cssClass}, + list_class = #{listClass}, + is_default = #{isDefault}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_code = #{dictCode} + + + + update sys_dict_data set dict_type = #{newDictType} where dict_type = #{oldDictType} + + + + insert into sys_dict_data( + dict_sort, + dict_label, + dict_value, + dict_type, + css_class, + list_class, + is_default, + status, + remark, + create_by, + create_time + )values( + #{dictSort}, + #{dictLabel}, + #{dictValue}, + #{dictType}, + #{cssClass}, + #{listClass}, + #{isDefault}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml new file mode 100644 index 0000000..55b4075 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + select dict_id, dict_name, dict_type, status, create_by, create_time, remark + from sys_dict_type + + + + + + + + + + + + + + delete from sys_dict_type where dict_id = #{dictId} + + + + delete from sys_dict_type where dict_id in + + #{dictId} + + + + + update sys_dict_type + + dict_name = #{dictName}, + dict_type = #{dictType}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_id = #{dictId} + + + + insert into sys_dict_type( + dict_name, + dict_type, + status, + remark, + create_by, + create_time + )values( + #{dictName}, + #{dictType}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml new file mode 100644 index 0000000..822d665 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) + values (#{userName}, #{status}, #{ipaddr}, #{loginLocation}, #{browser}, #{os}, #{msg}, sysdate()) + + + + + + delete from sys_logininfor where info_id in + + #{infoId} + + + + + truncate table sys_logininfor + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml new file mode 100644 index 0000000..6762007 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time + from sys_menu + + + + + + + + + + + + + + + + + + + + + + + + + + update sys_menu + + menu_name = #{menuName}, + parent_id = #{parentId}, + order_num = #{orderNum}, + path = #{path}, + component = #{component}, + `query` = #{query}, + is_frame = #{isFrame}, + is_cache = #{isCache}, + menu_type = #{menuType}, + visible = #{visible}, + status = #{status}, + perms = #{perms}, + icon = #{icon}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where menu_id = #{menuId} + + + + insert into sys_menu( + menu_id, + parent_id, + menu_name, + order_num, + path, + component, + `query`, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + remark, + create_by, + create_time + )values( + #{menuId}, + #{parentId}, + #{menuName}, + #{orderNum}, + #{path}, + #{component}, + #{query}, + #{isFrame}, + #{isCache}, + #{menuType}, + #{visible}, + #{status}, + #{perms}, + #{icon}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_menu where menu_id = #{menuId} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml new file mode 100644 index 0000000..65d3079 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark + from sys_notice + + + + + + + + insert into sys_notice ( + notice_title, + notice_type, + notice_content, + status, + remark, + create_by, + create_time + )values( + #{noticeTitle}, + #{noticeType}, + #{noticeContent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_notice + + notice_title = #{noticeTitle}, + notice_type = #{noticeType}, + notice_content = #{noticeContent}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id in + + #{noticeId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml new file mode 100644 index 0000000..201db07 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time, cost_time + from sys_oper_log + + + + insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, cost_time, oper_time) + values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, #{costTime}, sysdate()) + + + + + + delete from sys_oper_log where oper_id in + + #{operId} + + + + + + + truncate table sys_oper_log + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml new file mode 100644 index 0000000..227c459 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + select post_id, post_code, post_name, post_sort, status, create_by, create_time, remark + from sys_post + + + + + + + + + + + + + + + + + + update sys_post + + post_code = #{postCode}, + post_name = #{postName}, + post_sort = #{postSort}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where post_id = #{postId} + + + + insert into sys_post( + post_id, + post_code, + post_name, + post_sort, + status, + remark, + create_by, + create_time + )values( + #{postId}, + #{postCode}, + #{postName}, + #{postSort}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_post where post_id = #{postId} + + + + delete from sys_post where post_id in + + #{postId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml new file mode 100644 index 0000000..7c4139b --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_role_dept where role_id=#{roleId} + + + + + + delete from sys_role_dept where role_id in + + #{roleId} + + + + + insert into sys_role_dept(role_id, dept_id) values + + (#{item.roleId},#{item.deptId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml new file mode 100644 index 0000000..52306c2 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, + r.status, r.del_flag, r.create_time, r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + + + + + + + + + + + insert into sys_role( + role_id, + role_name, + role_key, + role_sort, + data_scope, + menu_check_strictly, + dept_check_strictly, + status, + remark, + create_by, + create_time + )values( + #{roleId}, + #{roleName}, + #{roleKey}, + #{roleSort}, + #{dataScope}, + #{menuCheckStrictly}, + #{deptCheckStrictly}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_role + + role_name = #{roleName}, + role_key = #{roleKey}, + role_sort = #{roleSort}, + data_scope = #{dataScope}, + menu_check_strictly = #{menuCheckStrictly}, + dept_check_strictly = #{deptCheckStrictly}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id in + + #{roleId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml new file mode 100644 index 0000000..92279f0 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + delete from sys_role_menu where role_id=#{roleId} + + + + delete from sys_role_menu where menu_id=#{menuId} + + + + delete from sys_role_menu where role_id in + + #{roleId} + + + + + insert into sys_role_menu(role_id, menu_id) values + + (#{item.roleId},#{item.menuId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml new file mode 100644 index 0000000..eca3694 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, + d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, + r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status + from sys_user u + left join sys_dept d on u.dept_id = d.dept_id + left join sys_user_role ur on u.user_id = ur.user_id + left join sys_role r on r.role_id = ur.role_id + + + + + + + + + + + + + + + + + + + + insert into sys_user( + user_id, + dept_id, + user_name, + nick_name, + email, + avatar, + phonenumber, + sex, + password, + status, + create_by, + remark, + create_time + )values( + #{userId}, + #{deptId}, + #{userName}, + #{nickName}, + #{email}, + #{avatar}, + #{phonenumber}, + #{sex}, + #{password}, + #{status}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_user + + dept_id = #{deptId}, + user_name = #{userName}, + nick_name = #{nickName}, + email = #{email}, + phonenumber = #{phonenumber}, + sex = #{sex}, + avatar = #{avatar}, + password = #{password}, + status = #{status}, + login_ip = #{loginIp}, + login_date = #{loginDate}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where user_id = #{userId} + + + + update sys_user set status = #{status} where user_id = #{userId} + + + + update sys_user set avatar = #{avatar} where user_name = #{userName} + + + + update sys_user set password = #{password} where user_name = #{userName} + + + + update sys_user set del_flag = '2' where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id in + + #{userId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml new file mode 100644 index 0000000..2b90bc4 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_user_post where user_id=#{userId} + + + + + + delete from sys_user_post where user_id in + + #{userId} + + + + + insert into sys_user_post(user_id, post_id) values + + (#{item.userId},#{item.postId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml new file mode 100644 index 0000000..dd72689 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + delete from sys_user_role where user_id=#{userId} + + + + + + delete from sys_user_role where user_id in + + #{userId} + + + + + insert into sys_user_role(user_id, role_id) values + + (#{item.userId},#{item.roleId}) + + + + + delete from sys_user_role where user_id=#{userId} and role_id=#{roleId} + + + + delete from sys_user_role where role_id=#{roleId} and user_id in + + #{userId} + + + \ No newline at end of file diff --git a/ruoyi-ui/.editorconfig b/ruoyi-ui/.editorconfig new file mode 100644 index 0000000..7034f9b --- /dev/null +++ b/ruoyi-ui/.editorconfig @@ -0,0 +1,22 @@ +# 告诉EditorConfig插件,这是根文件,不用继续往上查找 +root = true + +# 匹配全部文件 +[*] +# 设置字符集 +charset = utf-8 +# 缩进风格,可选space、tab +indent_style = space +# 缩进的空格数 +indent_size = 2 +# 结尾换行符,可选lf、cr、crlf +end_of_line = lf +# 在文件结尾插入新行 +insert_final_newline = true +# 删除一行中的前后空格 +trim_trailing_whitespace = true + +# 匹配md结尾的文件 +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development new file mode 100644 index 0000000..49d0d97 --- /dev/null +++ b/ruoyi-ui/.env.development @@ -0,0 +1,14 @@ +# 页面标题 +VUE_APP_TITLE = 后台管理系统 + +# 开发环境配置 +ENV = 'development' + +# skins管理系统/开发环境 +VUE_APP_BASE_API = 'http://localhost:28081' + +# 路由懒加载 +VUE_CLI_BABEL_TRANSPILE_MODULES = true + +# AES 加密开关(开发环境默认关闭以方便调试) +VUE_APP_USE_AES_ENCRYPT = false diff --git a/ruoyi-ui/.env.production b/ruoyi-ui/.env.production new file mode 100644 index 0000000..f6560c2 --- /dev/null +++ b/ruoyi-ui/.env.production @@ -0,0 +1,12 @@ +# 页面标题 +VUE_APP_TITLE = 常州项目 + +# 生产环境配置 +ENV = 'production' + +# skins管理系统/生产环境 +# VUE_APP_BASE_API = 'http://154.12.94.69:28081' + VUE_APP_BASE_API = 'http://103.108.67.60:28081' + +# AES 加密开关(生产环境开启) +VUE_APP_USE_AES_ENCRYPT = false diff --git a/ruoyi-ui/.env.staging b/ruoyi-ui/.env.staging new file mode 100644 index 0000000..fa4a123 --- /dev/null +++ b/ruoyi-ui/.env.staging @@ -0,0 +1,13 @@ +# 页面标题 +VUE_APP_TITLE = 后台管理系统 + +NODE_ENV = production + +# 测试环境配置 +ENV = 'staging' + +# skins 管理系统/测试环境 +VUE_APP_BASE_API = '/stage-api' + +# AES 加密开关(测试环境建议关闭) +VUE_APP_USE_AES_ENCRYPT = false diff --git a/ruoyi-ui/.eslintignore b/ruoyi-ui/.eslintignore new file mode 100644 index 0000000..89be6f6 --- /dev/null +++ b/ruoyi-ui/.eslintignore @@ -0,0 +1,10 @@ +# 忽略build目录下类型为js的文件的语法检查 +build/*.js +# 忽略src/assets目录下文件的语法检查 +src/assets +# 忽略public目录下文件的语法检查 +public +# 忽略当前目录下为js的文件的语法检查 +*.js +# 忽略当前目录下为vue的文件的语法检查 +*.vue \ No newline at end of file diff --git a/ruoyi-ui/.eslintrc.js b/ruoyi-ui/.eslintrc.js new file mode 100644 index 0000000..82bbdee --- /dev/null +++ b/ruoyi-ui/.eslintrc.js @@ -0,0 +1,199 @@ +// ESlint 检查配置 +module.exports = { + root: true, + parserOptions: { + parser: 'babel-eslint', + sourceType: 'module' + }, + env: { + browser: true, + node: true, + es6: true, + }, + extends: ['plugin:vue/recommended', 'eslint:recommended'], + + // add your custom rules here + //it is base on https://github.com/vuejs/eslint-config-vue + rules: { + "vue/max-attributes-per-line": [2, { + "singleline": 10, + "multiline": { + "max": 1, + "allowFirstLine": false + } + }], + "vue/singleline-html-element-content-newline": "off", + "vue/multiline-html-element-content-newline":"off", + "vue/name-property-casing": ["error", "PascalCase"], + "vue/no-v-html": "off", + 'accessor-pairs': 2, + 'arrow-spacing': [2, { + 'before': true, + 'after': true + }], + 'block-spacing': [2, 'always'], + 'brace-style': [2, '1tbs', { + 'allowSingleLine': true + }], + 'camelcase': [0, { + 'properties': 'always' + }], + 'comma-dangle': [2, 'never'], + 'comma-spacing': [2, { + 'before': false, + 'after': true + }], + 'comma-style': [2, 'last'], + 'constructor-super': 2, + 'curly': [2, 'multi-line'], + 'dot-location': [2, 'property'], + 'eol-last': 2, + 'eqeqeq': ["error", "always", {"null": "ignore"}], + 'generator-star-spacing': [2, { + 'before': true, + 'after': true + }], + 'handle-callback-err': [2, '^(err|error)$'], + 'indent': [2, 2, { + 'SwitchCase': 1 + }], + 'jsx-quotes': [2, 'prefer-single'], + 'key-spacing': [2, { + 'beforeColon': false, + 'afterColon': true + }], + 'keyword-spacing': [2, { + 'before': true, + 'after': true + }], + 'new-cap': [2, { + 'newIsCap': true, + 'capIsNew': false + }], + 'new-parens': 2, + 'no-array-constructor': 2, + 'no-caller': 2, + 'no-console': 'off', + 'no-class-assign': 2, + 'no-cond-assign': 2, + 'no-const-assign': 2, + 'no-control-regex': 0, + 'no-delete-var': 2, + 'no-dupe-args': 2, + 'no-dupe-class-members': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-empty-character-class': 2, + 'no-empty-pattern': 2, + 'no-eval': 2, + 'no-ex-assign': 2, + 'no-extend-native': 2, + 'no-extra-bind': 2, + 'no-extra-boolean-cast': 2, + 'no-extra-parens': [2, 'functions'], + 'no-fallthrough': 2, + 'no-floating-decimal': 2, + 'no-func-assign': 2, + 'no-implied-eval': 2, + 'no-inner-declarations': [2, 'functions'], + 'no-invalid-regexp': 2, + 'no-irregular-whitespace': 2, + 'no-iterator': 2, + 'no-label-var': 2, + 'no-labels': [2, { + 'allowLoop': false, + 'allowSwitch': false + }], + 'no-lone-blocks': 2, + 'no-mixed-spaces-and-tabs': 2, + 'no-multi-spaces': 2, + 'no-multi-str': 2, + 'no-multiple-empty-lines': [2, { + 'max': 1 + }], + 'no-native-reassign': 2, + 'no-negated-in-lhs': 2, + 'no-new-object': 2, + 'no-new-require': 2, + 'no-new-symbol': 2, + 'no-new-wrappers': 2, + 'no-obj-calls': 2, + 'no-octal': 2, + 'no-octal-escape': 2, + 'no-path-concat': 2, + 'no-proto': 2, + 'no-redeclare': 2, + 'no-regex-spaces': 2, + 'no-return-assign': [2, 'except-parens'], + 'no-self-assign': 2, + 'no-self-compare': 2, + 'no-sequences': 2, + 'no-shadow-restricted-names': 2, + 'no-spaced-func': 2, + 'no-sparse-arrays': 2, + 'no-this-before-super': 2, + 'no-throw-literal': 2, + 'no-trailing-spaces': 2, + 'no-undef': 2, + 'no-undef-init': 2, + 'no-unexpected-multiline': 2, + 'no-unmodified-loop-condition': 2, + 'no-unneeded-ternary': [2, { + 'defaultAssignment': false + }], + 'no-unreachable': 2, + 'no-unsafe-finally': 2, + 'no-unused-vars': [2, { + 'vars': 'all', + 'args': 'none' + }], + 'no-useless-call': 2, + 'no-useless-computed-key': 2, + 'no-useless-constructor': 2, + 'no-useless-escape': 0, + 'no-whitespace-before-property': 2, + 'no-with': 2, + 'one-var': [2, { + 'initialized': 'never' + }], + 'operator-linebreak': [2, 'after', { + 'overrides': { + '?': 'before', + ':': 'before' + } + }], + 'padded-blocks': [2, 'never'], + 'quotes': [2, 'single', { + 'avoidEscape': true, + 'allowTemplateLiterals': true + }], + 'semi': [2, 'never'], + 'semi-spacing': [2, { + 'before': false, + 'after': true + }], + 'space-before-blocks': [2, 'always'], + 'space-before-function-paren': [2, 'never'], + 'space-in-parens': [2, 'never'], + 'space-infix-ops': 2, + 'space-unary-ops': [2, { + 'words': true, + 'nonwords': false + }], + 'spaced-comment': [2, 'always', { + 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] + }], + 'template-curly-spacing': [2, 'never'], + 'use-isnan': 2, + 'valid-typeof': 2, + 'wrap-iife': [2, 'any'], + 'yield-star-spacing': [2, 'both'], + 'yoda': [2, 'never'], + 'prefer-const': 2, + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + 'object-curly-spacing': [2, 'always', { + objectsInObjects: false + }], + 'array-bracket-spacing': [2, 'never'] + } +} diff --git a/ruoyi-ui/.gitignore b/ruoyi-ui/.gitignore new file mode 100644 index 0000000..78a752d --- /dev/null +++ b/ruoyi-ui/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +**/*.log + +tests/**/coverage/ +tests/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +package-lock.json +yarn.lock diff --git a/ruoyi-ui/README.md b/ruoyi-ui/README.md new file mode 100644 index 0000000..58c11e2 --- /dev/null +++ b/ruoyi-ui/README.md @@ -0,0 +1,3 @@ +node v22.22.0 + +npm 10.9.4 \ No newline at end of file diff --git a/ruoyi-ui/babel.config.js b/ruoyi-ui/babel.config.js new file mode 100644 index 0000000..c8267b2 --- /dev/null +++ b/ruoyi-ui/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: [ + // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app + '@vue/cli-plugin-babel/preset' + ], + 'env': { + 'development': { + // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). + // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. + 'plugins': ['dynamic-import-node'] + } + } +} \ No newline at end of file diff --git a/ruoyi-ui/bin/build.bat b/ruoyi-ui/bin/build.bat new file mode 100644 index 0000000..dda590d --- /dev/null +++ b/ruoyi-ui/bin/build.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [ÐÅϢ] ´ò°üWeb¹¤³̣¬Éú³ÉdistÎļþ¡£ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm run build:prod + +pause \ No newline at end of file diff --git a/ruoyi-ui/bin/package.bat b/ruoyi-ui/bin/package.bat new file mode 100644 index 0000000..0e5bc0f --- /dev/null +++ b/ruoyi-ui/bin/package.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [ÐÅϢ] °²װWeb¹¤³̣¬Éú³Énode_modulesÎļþ¡£ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm install --registry=https://registry.npmmirror.com + +pause \ No newline at end of file diff --git a/ruoyi-ui/bin/run-web.bat b/ruoyi-ui/bin/run-web.bat new file mode 100644 index 0000000..d30deae --- /dev/null +++ b/ruoyi-ui/bin/run-web.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [ÐÅϢ] ʹÓà Vue CLI ÃüÁîÔËÐÐ Web ¹¤³̡£ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm run dev + +pause \ No newline at end of file diff --git a/ruoyi-ui/build/index.js b/ruoyi-ui/build/index.js new file mode 100644 index 0000000..0c57de2 --- /dev/null +++ b/ruoyi-ui/build/index.js @@ -0,0 +1,35 @@ +const { run } = require('runjs') +const chalk = require('chalk') +const config = require('../vue.config.js') +const rawArgv = process.argv.slice(2) +const args = rawArgv.join(' ') + +if (process.env.npm_config_preview || rawArgv.includes('--preview')) { + const report = rawArgv.includes('--report') + + run(`vue-cli-service build ${args}`) + + const port = 9526 + const publicPath = config.publicPath + + var connect = require('connect') + var serveStatic = require('serve-static') + const app = connect() + + app.use( + publicPath, + serveStatic('./dist', { + index: ['index.html', '/'] + }) + ) + + app.listen(port, function () { + console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) + if (report) { + console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) + } + + }) +} else { + run(`vue-cli-service build ${args}`) +} diff --git a/ruoyi-ui/main.py b/ruoyi-ui/main.py new file mode 100644 index 0000000..bcee4e5 --- /dev/null +++ b/ruoyi-ui/main.py @@ -0,0 +1,51 @@ +tables = [ + "game_sugar_record", + "game_sugar_user", + "game_wheel_round_bet", + "game_wheel_user", + "tt_announcement_read", + "tt_attendance_record", + "tt_bonus_receive_record", + "tt_box_records", + "tt_commission_record", + "tt_delivery_record", + "tt_exponent_user", + "tt_exponent_user_box", + "tt_fight", + "tt_fight_user", + "tt_notice", + "tt_order", + "tt_promotion_record", + "tt_recharge_record", + "tt_red_packet", + "tt_red_packet_record", + "tt_roll", + "tt_roll_user", + "tt_task_center_user", + "tt_time_roll_user", + "tt_upgrade_record", + "tt_user_blend_ercash", + "tt_welfare_record", + "tt_recharge_record", + "game_sugar_win", + "game_wheel_round", + "tt_recharge_card", +] + + +def clear_data(): + for table in tables: + print( + f"""CREATE TABLE {table}_new LIKE {table}; + RENAME TABLE {table} TO {table}_old, {table}_new TO {table};""" + ) + + +def clear(): + for table in tables: + print(f"DROP TABLE IF EXISTS {table}_old;") + print(f"DROP TABLE IF EXISTS {table}_new;") + + +# clear() +clear_data() diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json new file mode 100644 index 0000000..8ad1e60 --- /dev/null +++ b/ruoyi-ui/package.json @@ -0,0 +1,92 @@ +{ + "name": "ruoyi", + "version": "3.8.6", + "description": "管理系统", + "author": "linh", + "license": "MIT", + "scripts": { + "dev": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve", + "build:prod": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build", + "build:stage": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build --mode staging", + "build:test": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build --mode test", + "preview": "set NODE_OPTIONS=--openssl-legacy-provider && node build/index.js --preview", + "lint": "eslint --ext .js,.vue src" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,vue}": [ + "eslint --fix", + "git add" + ] + }, + "keywords": [ + "vue", + "admin", + "dashboard", + "element-ui", + "boilerplate", + "admin-template", + "management-system" + ], + "repository": { + "type": "git", + "url": "https://gitee.com/y_project/RuoYi-Vue.git" + }, + "dependencies": { + "@riophae/vue-treeselect": "0.4.0", + "axios": "0.24.0", + "clipboard": "2.0.8", + "core-js": "3.25.3", + "crypto-js": "^4.1.1", + "echarts": "5.4.0", + "element-ui": "2.15.13", + "file-saver": "2.0.5", + "fuse.js": "6.4.3", + "highlight.js": "9.18.5", + "js-beautify": "1.13.0", + "js-cookie": "3.0.1", + "jsencrypt": "3.0.0-rc.1", + "nprogress": "0.2.0", + "quill": "1.3.7", + "screenfull": "5.0.2", + "sortablejs": "1.10.2", + "vue": "2.6.12", + "vue-count-to": "1.0.13", + "vue-cropper": "0.5.5", + "vue-meta": "2.4.0", + "vue-router": "3.4.9", + "vuedraggable": "2.24.3", + "vuex": "3.6.0" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "4.4.6", + "@vue/cli-plugin-eslint": "4.4.6", + "@vue/cli-service": "4.4.6", + "babel-eslint": "10.1.0", + "babel-plugin-dynamic-import-node": "2.3.3", + "chalk": "4.1.0", + "compression-webpack-plugin": "5.0.2", + "connect": "3.6.6", + "eslint": "7.15.0", + "eslint-plugin-vue": "7.2.0", + "lint-staged": "10.5.3", + "runjs": "4.4.2", + "sass": "1.32.13", + "sass-loader": "10.1.1", + "script-ext-html-webpack-plugin": "2.1.5", + "svg-sprite-loader": "5.1.1", + "vue-template-compiler": "2.6.12" + }, + "engines": { + "node": ">=8.9", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions" + ] +} diff --git a/ruoyi-ui/public/favicon.ico b/ruoyi-ui/public/favicon.ico new file mode 100644 index 0000000..a724084 Binary files /dev/null and b/ruoyi-ui/public/favicon.ico differ diff --git a/ruoyi-ui/public/html/ie.html b/ruoyi-ui/public/html/ie.html new file mode 100644 index 0000000..052ffcd --- /dev/null +++ b/ruoyi-ui/public/html/ie.html @@ -0,0 +1,46 @@ + + + + + + 请升级您的浏览器 + + + + + + +

请升级您的浏览器,以便我们更好的为您提供服务!

+

您正在使用 Internet Explorer 的早期版本(IE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。

+
+

请注意:微软公司对Windows XP 及 Internet Explorer 早期版本的支持已经结束

+

自 2016 年 1 月 12 日起,Microsoft 不再为 IE 11 以下版本提供相应支持和更新。没有关键的浏览器安全更新,您的电脑可能易受有害病毒、间谍软件和其他恶意软件的攻击,它们可以窃取或损害您的业务数据和信息。请参阅 微软对 Internet Explorer 早期版本的支持将于 2016 年 1 月 12 日结束的说明 。

+
+

您可以选择更先进的浏览器

+

推荐使用以下浏览器的最新版本。如果您的电脑已有以下浏览器的最新版本则直接使用该浏览器访问即可。

+ +
+ + \ No newline at end of file diff --git a/ruoyi-ui/public/index.html b/ruoyi-ui/public/index.html new file mode 100644 index 0000000..a2249c1 --- /dev/null +++ b/ruoyi-ui/public/index.html @@ -0,0 +1,30 @@ + + + + + + + + + <%= webpackConfig.name %> + + + + +
+ + diff --git a/ruoyi-ui/public/robots.txt b/ruoyi-ui/public/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/ruoyi-ui/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/ruoyi-ui/recover.py b/ruoyi-ui/recover.py new file mode 100644 index 0000000..055e55a --- /dev/null +++ b/ruoyi-ui/recover.py @@ -0,0 +1,19 @@ +tables = [ + "tt_box", + "tt_box_open_chance", + "tt_box_ornaments", + "tt_box_records", + "tt_ornament", + "tt_upgrade_fail_ornaments", + "tt_upgrade_ornaments", + "tt_upgrade_record", + "tt_roll", + "tt_roll_jackpot", + "tt_roll_jackpot_ornaments", + "tt_roll_user_prize" +] + +for table in tables: + print(f"""RENAME TABLE {table} TO {table}_new, {table}_old TO {table};""") + + diff --git a/ruoyi-ui/restapi/admin_index.http b/ruoyi-ui/restapi/admin_index.http new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-ui/restapi/admin_request.http b/ruoyi-ui/restapi/admin_request.http new file mode 100644 index 0000000..ec5acf7 --- /dev/null +++ b/ruoyi-ui/restapi/admin_request.http @@ -0,0 +1,89 @@ +POST http://localhost:8081/login HTTP/1.1 +Accept: */* +Accept-Encoding: gzip, deflate +Accept-Language: zh-CN,zh;q=0.9 +Cache-Control: no-cache +Connection: keep-alive +Content-Type: application/json; charset=UTF-8 +Host: 154.12.94.98:8081 +Origin: http://154.12.94.98:10002 +Pragma: no-cache +Referer: http://154.12.94.98:10002/ +User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 + +{"username":"admin","password":"123456"} + + +### 生成账号 +GET http://localhost:8081/admin/user/generateAccountByPhoneNum?phoneNum=13214201158&password=123456789 HTTP/1.1 +Accept: */* +Accept-Language: zh-CN,zh;q=0.9 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJjYzhiYjc3LWU4OGQtNDM0Ny1iODAyLTNjMWUwNmQ0NDBmZSJ9.5TDTp1_hED9A6Gg177FZ-CY655LeyTzPjAjOYo5cqg79odNmH5GAs0JNkaF8iT7o8BnDgS4vpt8Ikchp8YfZGA +Cache-Control: no-cache +Connection: keep-alive +Origin: http://154.12.94.98:10002 +Pragma: no-cache' +Referer: http://154.12.94.98:10002/ +User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 + + +### + +GET http://localhost:8081/admin/sugar/user/list?pageNum=1&pageSize=10&userId=2 HTTP/1.1 +Accept: */* +Accept-Language: zh-CN,zh;q=0.9 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-042BGqOPWBCwKFGukNCQIjqYB6aDcYtIhO0e8pzUZJp9bA +Cache-Control: no-cache +Connection: keep-alive +Origin: http://154.12.94.98:10002 +Pragma: no-cache' +Referer: http://154.12.94.98:10002/ +User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 + +### + +GET http://localhost:8081/admin/sugar/gameconfig HTTP/1.1 +Accept: */* +Accept-Language: zh-CN,zh;q=0.9 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-042BGqOPWBCwKFGukNCQIjqYB6aDcYtIhO0e8pzUZJp9bA +Cache-Control: no-cache +Connection: keep-alive +Origin: http://154.12.94.98:10002 +Pragma: no-cache' +Referer: http://154.12.94.98:10002/ +User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 + +### +POST http://localhost:8081/admin/sugar/gameconfig HTTP/1.1 +Content-Type: application/json; charset=UTF-8 +Accept: */* +Accept-Language: zh-CN,zh;q=0.9 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-042BGqOPWBCwKFGukNCQIjqYB6aDcYtIhO0e8pzUZJp9bA +Cache-Control: no-cache +Connection: keep-alive +Origin: http://154.12.94.98:10002 +Pragma: no-cache' +Referer: http://154.12.94.98:10002/ +User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 + +{ +"defaultAwardPool": 10000.0, +"jpPercent": [ + 5, + 100 +], +"rtp": 0.80, +"minProfit": 0.3 +} + +### +GET http://localhost:8081/admin/sugar/dailywin/list?endDate=2026-03-30T00:00:00 HTTP/1.1 +Accept: */* +Accept-Language: zh-CN,zh;q=0.9 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJjYzhiYjc3LWU4OGQtNDM0Ny1iODAyLTNjMWUwNmQ0NDBmZSJ9.5TDTp1_hED9A6Gg177FZ-CY655LeyTzPjAjOYo5cqg79odNmH5GAs0JNkaF8iT7o8BnDgS4vpt8Ikchp8YfZGA +Cache-Control: no-cache +Connection: keep-alive +Origin: http://154.12.94.98:10002 +Pragma: no-cache' +Referer: http://154.12.94.98:10002/ +User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 diff --git a/ruoyi-ui/restapi/admin_sugar_api.http b/ruoyi-ui/restapi/admin_sugar_api.http new file mode 100644 index 0000000..8420762 --- /dev/null +++ b/ruoyi-ui/restapi/admin_sugar_api.http @@ -0,0 +1,36 @@ +POST http://localhost:8081/login HTTP/1.1 +Accept: */* +Accept-Encoding: gzip, deflate +Accept-Language: zh-CN,zh;q=0.9 +Cache-Control: no-cache +Connection: keep-alive +Content-Type: application/json; charset=UTF-8 +Host: 154.12.94.98:8081 +Origin: http://154.12.94.98:10002 +Pragma: no-cache +Referer: http://154.12.94.98:10002/ +User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 + +{"username":"admin","password":"123456"} + + +### 获取临时结果列表 +GET http://localhost:8081/admin/sugar/mockresult/list?uid=11 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJjYzhiYjc3LWU4OGQtNDM0Ny1iODAyLTNjMWUwNmQ0NDBmZSJ9.5TDTp1_hED9A6Gg177FZ-CY655LeyTzPjAjOYo5cqg79odNmH5GAs0JNkaF8iT7o8BnDgS4vpt8Ikchp8YfZGA + +### 生成临时结果列表 +POST http://localhost:8081/admin/sugar/mockresult/list HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJjYzhiYjc3LWU4OGQtNDM0Ny1iODAyLTNjMWUwNmQ0NDBmZSJ9.5TDTp1_hED9A6Gg177FZ-CY655LeyTzPjAjOYo5cqg79odNmH5GAs0JNkaF8iT7o8BnDgS4vpt8Ikchp8YfZGA +Content-Type: application/json; charset=UTF-8 + +{ + "uid": 11, + "normalScores": [100,90,80], + "freeScores": [100,90], + "superScores": [100,90,800] +} + +### 获取玩家记录 + +GET http://localhost:8081/admin/sugar/user/record/list?uid=2&startTime=2026-04-02%2009:40:00 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJjYzhiYjc3LWU4OGQtNDM0Ny1iODAyLTNjMWUwNmQ0NDBmZSJ9.5TDTp1_hED9A6Gg177FZ-CY655LeyTzPjAjOYo5cqg79odNmH5GAs0JNkaF8iT7o8BnDgS4vpt8Ikchp8YfZGA diff --git a/ruoyi-ui/restapi/admin_wheel_api.http b/ruoyi-ui/restapi/admin_wheel_api.http new file mode 100644 index 0000000..ed3b85c --- /dev/null +++ b/ruoyi-ui/restapi/admin_wheel_api.http @@ -0,0 +1,26 @@ +POST http://localhost:8081/login HTTP/1.1 +Accept: */* +Accept-Encoding: gzip, deflate +Accept-Language: zh-CN,zh;q=0.9 +Cache-Control: no-cache +Connection: keep-alive +Content-Type: application/json; charset=UTF-8 +Host: 154.12.94.98:8081 +Origin: http://154.12.94.98:10002 +Pragma: no-cache +Referer: http://154.12.94.98:10002/ +User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 + +{"username":"admin","password":"123456"} + + +### +GET http://localhost:8081/admin/wheel/gameconfig HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJjYzhiYjc3LWU4OGQtNDM0Ny1iODAyLTNjMWUwNmQ0NDBmZSJ9.5TDTp1_hED9A6Gg177FZ-CY655LeyTzPjAjOYo5cqg79odNmH5GAs0JNkaF8iT7o8BnDgS4vpt8Ikchp8YfZGA + +### +POST http://localhost:8081/admin/wheel/gameconfig HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjliMTY3MDdmLTAzZmYtNGQ4Zi05MjkzLTlkZWM0ZjgwNTVmYSJ9.CXeQ-IJKY3GK7aL2gQBOSr2NfUuLSUPsqV0mW7F-eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJjYzhiYjc3LWU4OGQtNDM0Ny1iODAyLTNjMWUwNmQ0NDBmZSJ9.5TDTp1_hED9A6Gg177FZ-CY655LeyTzPjAjOYo5cqg79odNmH5GAs0JNkaF8iT7o8BnDgS4vpt8Ikchp8YfZGA +Content-Type: application/json; charset=UTF-8 + +{} \ No newline at end of file diff --git a/ruoyi-ui/restapi/bullet.http b/ruoyi-ui/restapi/bullet.http new file mode 100644 index 0000000..b00fd53 --- /dev/null +++ b/ruoyi-ui/restapi/bullet.http @@ -0,0 +1,144 @@ +### ============================================================ +### 子弹与赏金 (Bullets & Bounty) API 测试 +### ============================================================ + +### 1. 登录 +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + +{"username":"19156010758","password":"123456"} + +### ============================================================ +### 2. 获取可用箱子列表 +POST http://localhost:8081/api/bullet/boxes HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{} + +### ============================================================ +### 3. 进行一次旋转 +### boxId: 箱子ID(传值=新游戏, 不传=继续游戏) +### autoDecompose: true=自动分解(静默加积分,不弹窗), false/不传=手动选择 +### +### 响应关键字段: +### hasGamble: true → 触发Gamble,需要调用 /gamble 决策 +### hasGamble: false + prize != null → 获取到饰品,调用 /prize_decide 决策 +### hasGamble: false + prize == null → 直接结算或自动分解,游戏结束 +### +### --- 普通模式 (手动选择) --- +POST http://localhost:8081/api/bullet/dospin HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{"boxId": 27} + +### --- 自动分解模式 --- +### autoDecompose=true 时,游戏全程静默结算: +### 1. 普通旋转 → 自动加积分,返回 prize=null,游戏结束 +### 2. 触发Gamble → 仍需手动调用 /gamble 决策 +### 3. Gamble后 → 自动加积分,无需调用 /prize_decide +POST http://localhost:8081/api/bullet/dospin HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{"boxId": 27, "autoDecompose": true} + +### ============================================================ +### 4. Gamble决策 (仅当hasGamble=true时调用) +### action: CONTINUE = 继续玩免费旋转 +### action: GAMBLE = 冒险(加权随机出CASH或UPGRADE) +### +### GAMBLE响应: +### gambleResult.type = "CASH" → 获得现金奖金 cashMul × bet +### gambleResult.type = "UPGRADE" → 升级到更高Bonus (如 FOUR_SHOTS) +### 如果升级后还有Gamble → hasGamble=true,继续调用本接口 +### 如果升级后无Gamble → 返回prize或直接结算 +### +### 注意: 如果 dospin 时 autoDecompose=true,Gamble结果会自动结算 +### CASH → 自动加积分,prize=null +### UPGRADE → 自动加积分,prize=null +### CONTINUE结束 → 自动加积分,prize=null +### 全程无需再调用 /prize_decide + +### 4a. 冒险 +POST http://localhost:8081/api/bullet/gamble HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{"action": "GAMBLE"} + +### 4b. 继续玩免费旋转 +POST http://localhost:8081/api/bullet/gamble HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{"action": "CONTINUE"} + +### ============================================================ +### 5. 饰品处理决策 (仅当prize != null时调用) +### 注意: autoDecompose=true 时无需调用此接口(已自动结算) +### action: DECOMPOSE = 分解(加余额) +### action: KEEP = 收藏(写背包,不加余额) +### +### 响应包含: prize (饰品信息), accountAmount (最新余额) + +### 5a. 分解饰品 +POST http://localhost:8081/api/bullet/prize_decide HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{"action": "DECOMPOSE"} + +### 5b. 收藏饰品 +POST http://localhost:8081/api/bullet/prize_decide HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{"action": "KEEP"} + +### ============================================================ +### 6. 检查是否有进行中的游戏 +### 响应: hasActive, boxType, bet, bonusType, hasGamble, gambleOptions, +### hasPrize, prizeId, prizeName, prizeGrade, prizeImageUrl, +### totalWin, totalMul, cost +POST http://localhost:8081/api/bullet/has_bonus HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{} + +### ============================================================ +### 7. 查询个人记录 +### boxType: normal / buy4_true_grit / buy5_four_shots 等 +POST http://localhost:8081/api/bullet/records HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{"pageNum": 1, "pageSize": 10, "boxType": ""} + +### ============================================================ +### 8. 查询全服排行 +### rankType: win / multiplier +POST http://localhost:8081/api/bullet/rank HTTP/1.1 +Authorization: Bearer {{token}} +Content-Type: application/json; charset=UTF-8 + +{"pageNum": 1, "pageSize": 10, "rankType": "win", "boxType": ""} + +### ============================================================ +### 9. 自动分解完整流程示例 +### 从旋转到结算全程只需调用 dospin 和 gamble: +### +### Step 1: 旋转 (autoDecompose=true) +### POST /api/bullet/dospin {"boxId": 27, "autoDecompose": true} +### → hasGamble=false, prize=null → 游戏结束,积分已自动到账 +### → hasGamble=true → 进入 Step 2 +### +### Step 2: Gamble决策 +### POST /api/bullet/gamble {"action": "GAMBLE"} 或 {"action": "CONTINUE"} +### → CONTINUE: 继续免费旋转,回到Step 1检查 +### → GAMBLE CASH: 自动加积分,prize=null,游戏结束 +### → GAMBLE UPGRADE: 自动加积分,prize=null,游戏结束 +### +### 对比手动模式: 还需要额外调用 /prize_decide {"action": "DECOMPOSE"} diff --git a/ruoyi-ui/restapi/cs340item.http b/ruoyi-ui/restapi/cs340item.http new file mode 100644 index 0000000..ac87c7e --- /dev/null +++ b/ruoyi-ui/restapi/cs340item.http @@ -0,0 +1,10 @@ +### 获取账户余额 +GET http://95.40.65.4:8081/admin/cs340item/checkMerchantBalance HTTP/1.1 + +### 查询库存 +POST http://95.40.65.4:8081/admin/cs340item/queryOnSaleInfo HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + +{ + "marketHashName": "10 Year Birthday Sticker Capsule" +} \ No newline at end of file diff --git a/ruoyi-ui/restapi/fight.http b/ruoyi-ui/restapi/fight.http new file mode 100644 index 0000000..64f0cdf --- /dev/null +++ b/ruoyi-ui/restapi/fight.http @@ -0,0 +1,39 @@ +POST http://95.40.65.4:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"17688554522","password":"123456"} + +### 获取个人战绩列表。除了page,size外,其他参数都是用来查询想要的战绩数据的,参数不传或者传null都表示不过滤这个条件 + +POST http://95.40.65.4:8081/api/fight/fightOnMyOwn HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM5ZmQ5MTE5LTk5YzgtNDlhMy04YjU4LWYzYzVlOWI3MTUyNSJ9.VQHwPNaDliBaDvlVeC3SxB_AxJ7Io4kWssqXzh3saW10kteT9sEaXNtVn4CN7xIA-N49geqTzPO4zCEJNRg9oQ +Content-Type: application/json; charset=UTF-8 + +{ + "fightId": 1, + "model": 0, + "statusList": [2], + "page": 1, + "size": 10 +} + + +### 获取全服战绩列表。除了page,size外,其他参数都是用来查询想要的战绩数据的,参数不传或者传null都表示不过滤这个条件 + +POST http://95.40.65.4:8081/api/fight/fightOnAll HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM5ZmQ5MTE5LTk5YzgtNDlhMy04YjU4LWYzYzVlOWI3MTUyNSJ9.VQHwPNaDliBaDvlVeC3SxB_AxJ7Io4kWssqXzh3saW10kteT9sEaXNtVn4CN7xIA-N49geqTzPO4zCEJNRg9oQ +Content-Type: application/json; charset=UTF-8 + +{ + + "model": 0, + "statusList": [2], + "page": 1, + "size": 10 +} + +### 十佳排行榜 + +GET http://95.40.65.4:8081/api/fight/tenTopFight?maxType=1 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjMxZTBmNTc5LWViNDQtNDU1Yy04Y2ZiLTM1ZWM3MGU0NTU0OSJ9.CcD5eXGaMB2DjtJkdZS9UuNQEAvz2WB1jf0VpSjZ7-PTNUZLvznuAJXqVZnNTqlGg3aDN6QP_Pl3WZAMD2MVBA diff --git a/ruoyi-ui/restapi/roll.http b/ruoyi-ui/restapi/roll.http new file mode 100644 index 0000000..60617d6 --- /dev/null +++ b/ruoyi-ui/restapi/roll.http @@ -0,0 +1,68 @@ +POST https://wildcs2.com/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"19156010758","password":"123456"} + +### 获取房间列表 + +GET http://localhost:8081/api/roll/getRollList?rollType=0&rollStatus=0&pageNum=1&pageSize=10&rollName=&userId=11 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM5ZmQ5MTE5LTk5YzgtNDlhMy04YjU4LWYzYzVlOWI3MTUyNSJ9.VQHwPNaDliBaDvlVeC3SxB_AxJ7Io4kWssqXzh3saW10kteT9sEaXNtVn4CN7xIA-N49geqTzPO4zCEJNRg9oQ + + + +### 获取房间详情 + +GET http://localhost:8081/api/roll/getRollDetails/1 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM5ZmQ5MTE5LTk5YzgtNDlhMy04YjU4LWYzYzVlOWI3MTUyNSJ9.VQHwPNaDliBaDvlVeC3SxB_AxJ7Io4kWssqXzh3saW10kteT9sEaXNtVn4CN7xIA-N49geqTzPO4zCEJNRg9oQ + + +### 获取房间玩家列表 + +POST http://localhost:8081/api/roll/getRollPlayers HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM5ZmQ5MTE5LTk5YzgtNDlhMy04YjU4LWYzYzVlOWI3MTUyNSJ9.VQHwPNaDliBaDvlVeC3SxB_AxJ7Io4kWssqXzh3saW10kteT9sEaXNtVn4CN7xIA-N49geqTzPO4zCEJNRg9oQ +Content-Type: application/json; charset=UTF-8 + +{ + "rollId": 1, + "page": 1, + "size": 10 +} + +### 加入房间 +POST http://localhost:8081/api/roll/joinRoll HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM5ZmQ5MTE5LTk5YzgtNDlhMy04YjU4LWYzYzVlOWI3MTUyNSJ9.VQHwPNaDliBaDvlVeC3SxB_AxJ7Io4kWssqXzh3saW10kteT9sEaXNtVn4CN7xIA-N49geqTzPO4zCEJNRg9oQ +Content-Type: application/json; charset=UTF-8 + +{ + "rollId": 1, + "rollPassword": "123456" +} + +### 获取ROLL房奖池详情 暂时用不到,房间详情接口有 +POST http://localhost:8081/api/roll/getRollPrizePool HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM5ZmQ5MTE5LTk5YzgtNDlhMy04YjU4LWYzYzVlOWI3MTUyNSJ9.VQHwPNaDliBaDvlVeC3SxB_AxJ7Io4kWssqXzh3saW10kteT9sEaXNtVn4CN7xIA-N49geqTzPO4zCEJNRg9oQ +Content-Type: application/json; charset=UTF-8 + +{ + "rollId": 1, + "page": 1, + "size": 10 +} + + +#### 获取ROLL房开奖数据 +POST http://localhost:8081/api/roll/getRollOpenPrize HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM5ZmQ5MTE5LTk5YzgtNDlhMy04YjU4LWYzYzVlOWI3MTUyNSJ9.VQHwPNaDliBaDvlVeC3SxB_AxJ7Io4kWssqXzh3saW10kteT9sEaXNtVn4CN7xIA-N49geqTzPO4zCEJNRg9oQ +Content-Type: application/json; charset=UTF-8 + +{ + "rollId": 1, + "page": 1, + "size": 10 +} + + + + + diff --git a/ruoyi-ui/restapi/sugar_api.http b/ruoyi-ui/restapi/sugar_api.http new file mode 100644 index 0000000..87c7ae2 --- /dev/null +++ b/ruoyi-ui/restapi/sugar_api.http @@ -0,0 +1,34 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"17774038557","password":"123456"} + +### + +GET http://localhost:8081/api/sugar/dospin?bet=10 HTTP/1.1 +Accept: */* +Accept-Language: zh-CN,zh;q=0.9 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImI0Mjk5ZmRjLTY4YmMtNDVhZi04ZDY1LWU3YjM1NDNhYTM5NCJ9.zsx5cfrqC_BICELsK0Kn0TsQQDYPENLQEDHPIAw76u8OpfsxUU9_T-vMWQI7UEU5NvFD7WoDuFu8AH0aFZRTVQ + +### + +GET http://localhost:8081/api/sugar/buy_free_spins?kind=standard&bet=100 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImI0Mjk5ZmRjLTY4YmMtNDVhZi04ZDY1LWU3YjM1NDNhYTM5NCJ9.zsx5cfrqC_BICELsK0Kn0TsQQDYPENLQEDHPIAw76u8OpfsxUU9_T-vMWQI7UEU5NvFD7WoDuFu8AH0aFZRTVQ + + +### + +GET http://localhost:8081/api/sugar/has_free_spin HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImI0Mjk5ZmRjLTY4YmMtNDVhZi04ZDY1LWU3YjM1NDNhYTM5NCJ9.zsx5cfrqC_BICELsK0Kn0TsQQDYPENLQEDHPIAw76u8OpfsxUU9_T-vMWQI7UEU5NvFD7WoDuFu8AH0aFZRTVQ + +### 个人记录 +GET http://localhost:8081/api/sugar/records?pageNum=1&pageSize=20&kind= +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImI0Mjk5ZmRjLTY4YmMtNDVhZi04ZDY1LWU3YjM1NDNhYTM5NCJ9.zsx5cfrqC_BICELsK0Kn0TsQQDYPENLQEDHPIAw76u8OpfsxUU9_T-vMWQI7UEU5NvFD7WoDuFu8AH0aFZRTVQ + +### 全局记录 +GET http://localhost:8081/api/sugar/rank_records?pageNum=1&pageSize=20&kind=normal&rankType=win +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImI0Mjk5ZmRjLTY4YmMtNDVhZi04ZDY1LWU3YjM1NDNhYTM5NCJ9.zsx5cfrqC_BICELsK0Kn0TsQQDYPENLQEDHPIAw76u8OpfsxUU9_T-vMWQI7UEU5NvFD7WoDuFu8AH0aFZRTVQ + + + diff --git a/ruoyi-ui/restapi/user.http b/ruoyi-ui/restapi/user.http new file mode 100644 index 0000000..847b502 --- /dev/null +++ b/ruoyi-ui/restapi/user.http @@ -0,0 +1,32 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"17688554522","password":"123456"} + + +### 更新提货链接 + +POST http://localhost:8081/api/user/updateDeliveryAddress HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImMxOTQ2YzIyLTUzZGMtNDdkMS1hMGUzLTg5ZWY2NjgxYzY2YyJ9.06j6rdNdZ23Ws52Kf_7Uo5FWRKNxGCrJf-xPDi5PCi7N6YTAiD1O3hgBtQ1pTWYE_f5dupbjzs4s0-FN0RWPaw +Content-Type: application/json; charset=UTF-8 + +{ + "transactionLink": "123" +} + +### 认证系统 + +POST http://localhost:8081/api/user/realNameAuthentication HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImQ5YTk0MzU0LWEyYzEtNDI5ZC04MTg1LTNiMDRmMzllMzAxMSJ9.uaFFxKiXNfM-bNeWAdSUNv85yfiNK8vAi8xeraTDULjxjUXCKimr7X_AprrKiFCaIt1FMsGbN_0AaOY9KBjajQ +Content-Type: application/json; charset=UTF-8 + +{ + "realName": "123", + "idNum": "411425199502074510" +} + +### 获取个人账户信息 +GET http://localhost:8081/api/getAccountAmount HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjZmMGU5ZTdjLTc4OTktNDAxNS1hYTJkLTU3N2ViOTY3MDQxOSJ9.a8w6jH-psbmJsxWr9S_lLX05C8Yz3K8D8Ajtkm7tItt8uWErE4kJMFSxs9QZpE4UgeXkwRo8lXlc8dSdxYuXPw +Content-Type: application/json; charset=UTF-8 \ No newline at end of file diff --git a/ruoyi-ui/restapi/wheel_api.http b/ruoyi-ui/restapi/wheel_api.http new file mode 100644 index 0000000..ddc63c3 --- /dev/null +++ b/ruoyi-ui/restapi/wheel_api.http @@ -0,0 +1,26 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"19156010758","password":"123456"} + +### + +GET http://localhost:8081/api/wheel/game_config HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjRjNDY5MGJhLTdkYTgtNGYyYi1iZGQwLWYyMDE3MTcwMTM5ZSJ9.7CvtJhZDbdgfMx5emTEWg-lwBIRKkPDOQITEtyQz5vqzG5p8X6lwJCdALJz69bFIog-DpvlnsFL9Yn7xo28DWA + +### 获取最后一轮转盘信息 +GET http://localhost:8081/api/wheel/last_round_info HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjRjNDY5MGJhLTdkYTgtNGYyYi1iZGQwLWYyMDE3MTcwMTM5ZSJ9.7CvtJhZDbdgfMx5emTEWg-lwBIRKkPDOQITEtyQz5vqzG5p8X6lwJCdALJz69bFIog-DpvlnsFL9Yn7xo28DWA + +### 下注 +GET http://localhost:8081/api/wheel/dobet?bet=5&symbol=A HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjRjNDY5MGJhLTdkYTgtNGYyYi1iZGQwLWYyMDE3MTcwMTM5ZSJ9.7CvtJhZDbdgfMx5emTEWg-lwBIRKkPDOQITEtyQz5vqzG5p8X6lwJCdALJz69bFIog-DpvlnsFL9Yn7xo28DWA + +### 个人游戏历史记录 +GET http://localhost:8081/api/wheel/history_bet_info?pageNum=1&pageSize=10 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjRjNDY5MGJhLTdkYTgtNGYyYi1iZGQwLWYyMDE3MTcwMTM5ZSJ9.7CvtJhZDbdgfMx5emTEWg-lwBIRKkPDOQITEtyQz5vqzG5p8X6lwJCdALJz69bFIog-DpvlnsFL9Yn7xo28DWA + +### 游戏历史记录 +GET http://localhost:8081/api/wheel/round_history_bet_info?pageNum=10&pageSize=10 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjRjNDY5MGJhLTdkYTgtNGYyYi1iZGQwLWYyMDE3MTcwMTM5ZSJ9.7CvtJhZDbdgfMx5emTEWg-lwBIRKkPDOQITEtyQz5vqzG5p8X6lwJCdALJz69bFIog-DpvlnsFL9Yn7xo28DWA \ No newline at end of file diff --git a/ruoyi-ui/restapi/个人记录.http b/ruoyi-ui/restapi/个人记录.http new file mode 100644 index 0000000..06b3d2b --- /dev/null +++ b/ruoyi-ui/restapi/个人记录.http @@ -0,0 +1,36 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"19156010758","password":"123456"} + +### 个人记录 充值、消费、红包记录 + +POST http://localhost:8081/api/userAmountRecords/personalRecord +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImQ2NTBkOWVlLTljNmYtNDIxNy04NTQzLTcwMWQ1M2NjZjc5ZiJ9.5PN9bmzzAliJrbeSbPXQsZ4jr1HpX-crusfUswU0YqIv4kwCxgoG-m4S5lZbL6nOTIYBPfB0V-1aP3Zpcs_X_A +Content-Type: application/json; charset=UTF-8 + +{ + "page": 1, + "pageSize": 2, + "type": 3, + "source": 2 +} + +### 个人记录 提货记录 + +POST http://localhost:8081/api/userAmountRecords/deliveryRecords +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImQ2NTBkOWVlLTljNmYtNDIxNy04NTQzLTcwMWQ1M2NjZjc5ZiJ9.5PN9bmzzAliJrbeSbPXQsZ4jr1HpX-crusfUswU0YqIv4kwCxgoG-m4S5lZbL6nOTIYBPfB0V-1aP3Zpcs_X_A +Content-Type: application/json; charset=UTF-8 + +{ + "page": 1, + "size": 2, + "statusList": [1] +} + +### 个人记录 转增记录 + +GET http://localhost:8081/api/userPackSack/transfer/records?pageNum=1&pageSize=2&kind=src +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImZhZTJkZTY3LTQ0OTMtNGUwZS04NzQ1LTBlNTdjNmFjZGY5ZiJ9.l9DAJTA7UkOhRXKbIMIDQBDefeiMWvoYPRXtJCSaamnuczaBULKF-mtDuYd2FXW3OEE4YHmnpmj0ZPZ10Oq2Zw + diff --git a/ruoyi-ui/restapi/商城.http b/ruoyi-ui/restapi/商城.http new file mode 100644 index 0000000..66f8815 --- /dev/null +++ b/ruoyi-ui/restapi/商城.http @@ -0,0 +1,12 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"19156010758","password":"123456"} + +### 商城商品列表 + +GET http://localhost:8081/api/shopping/list?pageNum=2&pageSize=2 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFmMDM4M2RhLTdiZGYtNDBlYi1iNDhkLTFiMjVlMmIxOGYwZCJ9.Y76_V_Zr7B26yyr6DHiH64Mt-JkgmZFFoaXIo0-cWwdUsArSatLK_P4sGG4gJWS9N1nbaw4IfmGn4Hd-4aKbqQ +Content-Type: application/json; charset=UTF-8 + diff --git a/ruoyi-ui/restapi/定时任务.http b/ruoyi-ui/restapi/定时任务.http new file mode 100644 index 0000000..4fe7148 --- /dev/null +++ b/ruoyi-ui/restapi/定时任务.http @@ -0,0 +1 @@ +GET http://localhost:8081/api/ttTask/test?kind=zhubo HTTP/1.1 diff --git a/ruoyi-ui/restapi/排行榜.http b/ruoyi-ui/restapi/排行榜.http new file mode 100644 index 0000000..ac7200a --- /dev/null +++ b/ruoyi-ui/restapi/排行榜.http @@ -0,0 +1,14 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + +{"username":"19156010758","password":"123456"} + +### 获取综合流水排行榜 +POST http://localhost:8081/api/userAmountRecords/blendErcashRank HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"page":1,"size":10,"type":1} + +### 获取综合流水排行榜奖励 +GET http://localhost:8081/api/userAmountRecords/rechargeRankingRewardsIntroduction HTTP/1.1 diff --git a/ruoyi-ui/restapi/提货.http b/ruoyi-ui/restapi/提货.http new file mode 100644 index 0000000..dd24934 --- /dev/null +++ b/ruoyi-ui/restapi/提货.http @@ -0,0 +1,45 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"19156010758","password":"123456"} + + +### 背包 +POST http://localhost:8081/api/userPackSack/getPackSack? HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImZhZTJkZTY3LTQ0OTMtNGUwZS04NzQ1LTBlNTdjNmFjZGY5ZiJ9.l9DAJTA7UkOhRXKbIMIDQBDefeiMWvoYPRXtJCSaamnuczaBULKF-mtDuYd2FXW3OEE4YHmnpmj0ZPZ10Oq2Zw +Content-Type: application/json; charset=UTF-8 + +{ + "page":1, + "size":10 +} + +### 转增 + +POST http://localhost:8081/api/userPackSack/transfer HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImZhZTJkZTY3LTQ0OTMtNGUwZS04NzQ1LTBlNTdjNmFjZGY5ZiJ9.l9DAJTA7UkOhRXKbIMIDQBDefeiMWvoYPRXtJCSaamnuczaBULKF-mtDuYd2FXW3OEE4YHmnpmj0ZPZ10Oq2Zw +Content-Type: application/json; charset=UTF-8 + +{"targetUid":1,"packSackIds":[2160]} + +### 提货 +POST http://localhost:8081/api/userPackSack/delivery HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImZhZTJkZTY3LTQ0OTMtNGUwZS04NzQ1LTBlNTdjNmFjZGY5ZiJ9.l9DAJTA7UkOhRXKbIMIDQBDefeiMWvoYPRXtJCSaamnuczaBULKF-mtDuYd2FXW3OEE4YHmnpmj0ZPZ10Oq2Zw +Content-Type: application/json; charset=UTF-8 + +{"packSackIds":[2159], "isAll": false} + + +### 购买 + +POST http://localhost:8081/api/shopping/exchange?ornamentsId=1878515908 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImZhZTJkZTY3LTQ0OTMtNGUwZS04NzQ1LTBlNTdjNmFjZGY5ZiJ9.l9DAJTA7UkOhRXKbIMIDQBDefeiMWvoYPRXtJCSaamnuczaBULKF-mtDuYd2FXW3OEE4YHmnpmj0ZPZ10Oq2Zw +Content-Type: application/json; charset=UTF-8 + +### 查询库存 +POST http://localhost:8081/admin/cs340item/queryOnSaleInfo HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImZhZTJkZTY3LTQ0OTMtNGUwZS04NzQ1LTBlNTdjNmFjZGY5ZiJ9.l9DAJTA7UkOhRXKbIMIDQBDefeiMWvoYPRXtJCSaamnuczaBULKF-mtDuYd2FXW3OEE4YHmnpmj0ZPZ10Oq2Zw +Content-Type: application/json; charset=UTF-8 + +{"marketHashName":"P90 | Desert Warfare (Well-Worn)"} \ No newline at end of file diff --git a/ruoyi-ui/restapi/支付.http b/ruoyi-ui/restapi/支付.http new file mode 100644 index 0000000..8b417c7 --- /dev/null +++ b/ruoyi-ui/restapi/支付.http @@ -0,0 +1,41 @@ +POST http://154.12.94.98:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"17688554522","password":"123456"} + +### 卡密支付 + +POST http://localhost:8081/api/recharge/cardPay?password=ABB3CBC8E36A49AB8DFC26D64D1E2F92 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFjNGJlZWI3LWFkZDMtNDY3Yy05YTRhLTRkZmFiMWU0N2RiMyJ9.Wjt2kcwyFkzTF-qOzh5Yi_GEhIpX6F1CnilhcZj0WUhv7Q-RAJLgq2px16Bo8dg8HJiQrj0c7l2x0P5kY5w-mw + + +### 在线支付 + +POST http://154.12.94.98:8081/api/hipay/pay HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjI3MmU4M2JhLTgxMzYtNDc3Yi05MDIyLTYyMDc0MTQxNDI1MiJ9.GfB7Bgt5zmK4wvJPN7Qv8Csuk5_RhI4qewS-YpCAHD0vyvXI50VtixsHW3IIC3e0ekn44nLw4_THt28opVyGBw +Content-Type: application/json; charset=UTF-8 + +{ + "goodsId": 6, + "goodsNum": 1 +} + +### 回调 +POST http://localhost:8081/api/hipay/notify HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjI3MmU4M2JhLTgxMzYtNDc3Yi05MDIyLTYyMDc0MTQxNDI1MiJ9.GfB7Bgt5zmK4wvJPN7Qv8Csuk5_RhI4qewS-YpCAHD0vyvXI50VtixsHW3IIC3e0ekn44nLw4_THt28opVyGBw +Content-Type: application/json; charset=UTF-8 + +{"code":"1","data":"{\"amount\":100.00,\"createtime\":1774019940000,\"id\":\"89878e6d7ada40e18bb76da3276fc9e5\",\"note\":\"充值100.00\",\"notifyurl\":\"http://154.12.94.98:8081/api/hipay/notify\",\"orderid\":\"2035013237443006464\",\"ordertype\":1,\"recvcharge\":12.000000,\"recvid\":\"e42835c685f0712b19f27d693ee11f42\",\"recvnickname\":\"星耀科技\",\"resign\":\"edced5cb250a97f5775cdd4f72814129\",\"returnurl\":\"\",\"sign\":\"c0ca2c25fbcb2a8cdfaf72fda7cb4b98\",\"state\":5,\"transtime\":1774020221000}","msg":"success"} + +### 制作红包 +POST http://localhost:8081/admin/redPacket HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjI3MmU4M2JhLTgxMzYtNDc3Yi05MDIyLTYyMDc0MTQxNDI1MiJ9.GfB7Bgt5zmK4wvJPN7Qv8Csuk5_RhI4qewS-YpCAHD0vyvXI50VtixsHW3IIC3e0ekn44nLw4_THt28opVyGBw +Content-Type: application/json; charset=UTF-8 + +{"num":2,"userId":1,"createNum":1,"title":"红包","openingTime":"2026-03-24 20:54:05","amount":"[1,1]"} + +### 红包兑换 + +GET http://154.12.94.98:8081/api/bonus/receiveRedPacket/219D1DDB124440EB875FDCCF03310D2F HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjEzNTFhNGUzLTk5ZDItNGQzMy1iOGFkLTA3ODhjNDhmNDcwNiJ9.THdSGhpP8F1VOBldWdI3kqhro1wSUvRKYEHUw5bcK4wJJFynUkJSjoT9Acnzb4EuJ-Su2pcvp9Jc4BoUX-6deQ diff --git a/ruoyi-ui/restapi/消息.http b/ruoyi-ui/restapi/消息.http new file mode 100644 index 0000000..4a67ee4 --- /dev/null +++ b/ruoyi-ui/restapi/消息.http @@ -0,0 +1,9 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"19156010758","password":"123456"} + +### 获取消息列表 +GET http://localhost:8081/api/message/getMessageList HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6Ijg2YzcxOGQ4LTc0MzItNDAyNS04MmVhLWQxYjgyNTg4OTk2MyJ9.6nybsO4P4cUxQP_r_CUpYJKJfFtvx9qTpRF4RkjpZ_JgCKvajoYP4NibJgU9mfOHl32cuYV0yOemkFSEs_Ilgg \ No newline at end of file diff --git a/ruoyi-ui/restapi/福利.http b/ruoyi-ui/restapi/福利.http new file mode 100644 index 0000000..b6b04b6 --- /dev/null +++ b/ruoyi-ui/restapi/福利.http @@ -0,0 +1,10 @@ +POST http://localhost:8081/api/login HTTP/1.1 +Content-Type: application/json; charset=UTF-8 + + +{"username":"17774038557","password":"123456"} + +### 查询返利福利 +GET http://localhost:8081/api/bonus/vipreward/list HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImUyYTE1ODU5LTQ0MzktNGM2Zi1hNjBjLTRhOGNmMmE1NTdiNyJ9.l32RqHK4IV8DTH6tR7oq-rQyJ8YvOTqlRWCzgdUR2Iea-_-Svu6R-eCz4UOz4-QNGs9yoBbldrAfvaaMfqYK7w + diff --git a/ruoyi-ui/src/App.vue b/ruoyi-ui/src/App.vue new file mode 100644 index 0000000..f392ff0 --- /dev/null +++ b/ruoyi-ui/src/App.vue @@ -0,0 +1,32 @@ + + + + diff --git a/ruoyi-ui/src/api/game/box/mock/api.js b/ruoyi-ui/src/api/game/box/mock/api.js new file mode 100644 index 0000000..167cd84 --- /dev/null +++ b/ruoyi-ui/src/api/game/box/mock/api.js @@ -0,0 +1,19 @@ +import request from '@/utils/request' + +// 查询:GET /admin/sugar/mockresult/list +export function list(query) { + return request({ + url: '/admin/box/mockresult/list', + method: 'get', + params: query + }) +} + +// 更新:POST /admin/sugar/mockresult/list +export function update(data) { + return request({ + url: '/admin/box/mockresult/list', + method: 'post', + data: data + }) +} diff --git a/ruoyi-ui/src/api/game/bullet/box/api.js b/ruoyi-ui/src/api/game/bullet/box/api.js new file mode 100644 index 0000000..468bf9e --- /dev/null +++ b/ruoyi-ui/src/api/game/bullet/box/api.js @@ -0,0 +1,63 @@ +import request from '@/utils/request' + +// 箱子列表 (支持分页+筛选) +export function boxList(params) { + return request({ + url: '/admin/bullet/box/list', + method: 'get', + params + }) +} + +// 新增/编辑箱子 +export function boxSave(data) { + return request({ + url: '/admin/bullet/box/save', + method: 'post', + data: data + }) +} + +// 删除箱子 +export function boxDelete(boxId) { + return request({ + url: '/admin/bullet/box/delete', + method: 'post', + params: { boxId } + }) +} + +// 查看箱子饰品配置 +export function boxOrnaments(boxId) { + return request({ + url: '/admin/bullet/box/ornaments', + method: 'get', + params: { boxId } + }) +} + +// 一键重算全部箱子饰品分布 +export function recalcAll() { + return request({ url: '/admin/bullet/box/recalc_all', method: 'post' }) +} + +// 新增/编辑饰品数量 (upsert) +export function saveOrnaments(boxId, data) { + return request({ + url: '/admin/bullet/box/ornaments/save', + method: 'post', + params: { boxId }, + data: data + }) +} + +// 版本管理 +export function getBoxVersions(boxId) { + return request({ url: '/admin/bullet/box/versions', method: 'get', params: { boxId } }) +} +export function publishVersion(boxId, version) { + return request({ url: '/admin/bullet/box/publish', method: 'post', params: { boxId, version } }) +} +export function rollbackVersion(boxId, version) { + return request({ url: '/admin/bullet/box/rollback', method: 'post', params: { boxId, version } }) +} diff --git a/ruoyi-ui/src/api/game/bullet/dailywin/api.js b/ruoyi-ui/src/api/game/bullet/dailywin/api.js new file mode 100644 index 0000000..12c6625 --- /dev/null +++ b/ruoyi-ui/src/api/game/bullet/dailywin/api.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +export function winStats(params) { + return request({ url: '/admin/bullet/winstats', method: 'get', params }) +} + +export function rewardPoolReset() { + return request({ url: '/admin/bullet/rewardpool/reset', method: 'post' }) +} diff --git a/ruoyi-ui/src/api/game/bullet/ornament/api.js b/ruoyi-ui/src/api/game/bullet/ornament/api.js new file mode 100644 index 0000000..678eab4 --- /dev/null +++ b/ruoyi-ui/src/api/game/bullet/ornament/api.js @@ -0,0 +1,5 @@ +import request from '@/utils/request' + +export function ornamentList() { + return request({ url: '/admin/bullet/ornament/list', method: 'get' }) +} diff --git a/ruoyi-ui/src/api/game/bullet/prize/api.js b/ruoyi-ui/src/api/game/bullet/prize/api.js new file mode 100644 index 0000000..00b1e41 --- /dev/null +++ b/ruoyi-ui/src/api/game/bullet/prize/api.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +export function prizeList() { + return request({ url: '/admin/bullet/prize/list', method: 'get' }) +} + +export function prizeSave(data) { + return request({ url: '/admin/bullet/prize/save', method: 'post', data }) +} diff --git a/ruoyi-ui/src/api/game/bullet/record/api.js b/ruoyi-ui/src/api/game/bullet/record/api.js new file mode 100644 index 0000000..6b90630 --- /dev/null +++ b/ruoyi-ui/src/api/game/bullet/record/api.js @@ -0,0 +1,13 @@ +import request from '@/utils/request' + +export function recordList(params) { + return request({ url: '/admin/bullet/records', method: 'get', params }) +} + +export function deleteRecord(recordId) { + return request({ url: '/admin/bullet/records/delete', method: 'post', params: { recordId } }) +} + +export function clearRecords() { + return request({ url: '/admin/bullet/records/clear', method: 'post' }) +} diff --git a/ruoyi-ui/src/api/game/bullet/round/api.js b/ruoyi-ui/src/api/game/bullet/round/api.js new file mode 100644 index 0000000..beb32da --- /dev/null +++ b/ruoyi-ui/src/api/game/bullet/round/api.js @@ -0,0 +1,13 @@ +import request from '@/utils/request' + +export function roundList(params) { + return request({ url: '/admin/bullet/round/list', method: 'get', params }) +} + +export function roundByOrnament(params) { + return request({ url: '/admin/bullet/round/byOrnament', method: 'get', params }) +} + +export function roundDelete(roundId) { + return request({ url: '/admin/bullet/round/delete', method: 'post', params: { roundId } }) +} diff --git a/ruoyi-ui/src/api/game/olympus/box/api.js b/ruoyi-ui/src/api/game/olympus/box/api.js new file mode 100644 index 0000000..430349d --- /dev/null +++ b/ruoyi-ui/src/api/game/olympus/box/api.js @@ -0,0 +1,56 @@ +import request from '@/utils/request' + +// 箱子列表(分页 + 按模式筛选) +export function getBoxList(params) { + return request({ url: '/admin/olympus/box/list', method: 'get', params }) +} + +// 新增/编辑箱子 +export function saveBox(data) { + return request({ url: '/admin/olympus/box/save', method: 'post', data }) +} + +// 某箱某身份的饰品桶分布(含 convergeCount / fixedRounds / convergeRounds) +export function getBoxOrnaments(boxId, audience) { + return request({ url: `/admin/olympus/box/ornaments/${boxId}`, method: 'get', params: { audience }}) +} + +// 保存某箱某身份饰品回合分布 +export function saveBoxOrnaments(data) { + return request({ url: '/admin/olympus/box/ornaments/save', method: 'post', data }) +} + +// 按 RTP 重算某箱某身份分布 +export function calcDistribution(data) { + return request({ url: '/admin/olympus/box/ornaments/calc', method: 'post', data }) +} + +// 模拟 payoutCap 约束下的预估实际 RTP +export function simulateRtp(data) { + return request({ url: '/admin/olympus/box/ornaments/simulate-rtp', method: 'post', data }) +} + +// ==================== 分布模板(按身份) ==================== + +export function getBoxTemplates(boxId, audience) { + return request({ url: `/admin/olympus/box/templates/${boxId}`, method: 'get', params: { audience }}) +} +export function saveBoxTemplate(data) { + return request({ url: '/admin/olympus/box/template/save', method: 'post', data }) +} +export function deleteBoxTemplate(id) { + return request({ url: `/admin/olympus/box/template/${id}`, method: 'delete' }) +} +export function applyBoxTemplate(data) { + return request({ url: '/admin/olympus/box/template/apply', method: 'post', data }) +} + +// 一键重算所有箱子 × 所有身份(按各箱现有配置) +export function recalcAll() { + return request({ url: '/admin/olympus/box/recalc_all', method: 'post' }) +} + +// 重算分布:按参数批量重算(boxType 可选=全部模式;audience 可选=全部身份;rtp/totalRounds/convergeRatio 必传统一设置) +export function recalcBatch(data) { + return request({ url: '/admin/olympus/box/recalcBatch', method: 'post', data }) +} diff --git a/ruoyi-ui/src/api/game/olympus/dailywin/api.js b/ruoyi-ui/src/api/game/olympus/dailywin/api.js new file mode 100644 index 0000000..e3ba04f --- /dev/null +++ b/ruoyi-ui/src/api/game/olympus/dailywin/api.js @@ -0,0 +1,11 @@ +import request from '@/utils/request' + +// 每日统计列表(分页) +export function getWinList(params) { + return request({ url: '/admin/olympus/win/list', method: 'get', params }) +} + +// 汇总(按日期区间) +export function getWinSummary(params) { + return request({ url: '/admin/olympus/win/summary', method: 'get', params }) +} diff --git a/ruoyi-ui/src/api/game/olympus/ornament/api.js b/ruoyi-ui/src/api/game/olympus/ornament/api.js new file mode 100644 index 0000000..cfd2379 --- /dev/null +++ b/ruoyi-ui/src/api/game/olympus/ornament/api.js @@ -0,0 +1,6 @@ +import request from '@/utils/request' + +// 饰品桶定义列表(17 桶) +export function getOrnamentList() { + return request({ url: '/admin/olympus/ornament/list', method: 'get' }) +} diff --git a/ruoyi-ui/src/api/game/olympus/prize/api.js b/ruoyi-ui/src/api/game/olympus/prize/api.js new file mode 100644 index 0000000..d38b1cf --- /dev/null +++ b/ruoyi-ui/src/api/game/olympus/prize/api.js @@ -0,0 +1,11 @@ +import request from '@/utils/request' + +// 返奖饰品列表(含返奖区间) +export function prizeList() { + return request({ url: '/admin/olympus/prize/list', method: 'get' }) +} + +// 新增/编辑返奖饰品(含区间) +export function prizeSave(data) { + return request({ url: '/admin/olympus/prize/save', method: 'post', data }) +} diff --git a/ruoyi-ui/src/api/game/olympus/record/api.js b/ruoyi-ui/src/api/game/olympus/record/api.js new file mode 100644 index 0000000..49268e9 --- /dev/null +++ b/ruoyi-ui/src/api/game/olympus/record/api.js @@ -0,0 +1,11 @@ +import request from '@/utils/request' + +// 游戏记录列表(分页 + 按玩家ID/身份筛选;identity=common/anchor/robot/white,空=全部身份) +export function getRecordList(params) { + return request({ url: '/admin/olympus/record/list', method: 'get', params }) +} + +// 返奖汇总(按日期区间) +export function getWinSummary(params) { + return request({ url: '/admin/olympus/win/summary', method: 'get', params }) +} diff --git a/ruoyi-ui/src/api/game/olympus/rewardPool/api.js b/ruoyi-ui/src/api/game/olympus/rewardPool/api.js new file mode 100644 index 0000000..15e4143 --- /dev/null +++ b/ruoyi-ui/src/api/game/olympus/rewardPool/api.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 奖池配置快照(总池 + 各箱子池),按身份 audience=common/anchor/robot/white +export function getPoolConfig(params) { + return request({ url: '/admin/olympus/rewardPool/config', method: 'get', params }) +} + +// 逐箱注水/抽水(type=1 注水 / 2 抽水,含 audience、remark) +export function adjustPool(data) { + return request({ url: '/admin/olympus/rewardPool/adjust', method: 'post', data }) +} + +// 批量按一轮花费比例注水/抽水:{ audience, type, ratio, remark },每箱金额=一轮总花费(底注×模式倍数×总回合数)×ratio% +export function adjustPoolBatch(data) { + return request({ url: '/admin/olympus/rewardPool/adjustBatch', method: 'post', data }) +} + +// 重置人工调整:该身份所有箱子注水/抽水累计归零,只保留真实水位(下注−返奖):{ audience, remark } +export function adjustPoolReset(data) { + return request({ url: '/admin/olympus/rewardPool/adjustReset', method: 'post', data }) +} + +// 奖池调整历史(分页,params 含 audience) +export function getPoolLogs(params) { + return request({ url: '/admin/olympus/rewardPool/adjustLog', method: 'get', params }) +} diff --git a/ruoyi-ui/src/api/game/olympus/round/api.js b/ruoyi-ui/src/api/game/olympus/round/api.js new file mode 100644 index 0000000..2337472 --- /dev/null +++ b/ruoyi-ui/src/api/game/olympus/round/api.js @@ -0,0 +1,6 @@ +import request from '@/utils/request' + +// 预生成回合列表(分页 + 按回合ID/模式筛选) +export function getRoundList(params) { + return request({ url: '/admin/olympus/round/list', method: 'get', params }) +} diff --git a/ruoyi-ui/src/api/game/sugar/config/api.js b/ruoyi-ui/src/api/game/sugar/config/api.js new file mode 100644 index 0000000..50b9cf1 --- /dev/null +++ b/ruoyi-ui/src/api/game/sugar/config/api.js @@ -0,0 +1,17 @@ +import request from '@/utils/request' + + +export function getConfig() { + return request({ + url: '/admin/sugar/gameconfig', + method: 'get' + }) +} + +export function updateConfig(data) { + return request({ + url: '/admin/sugar/gameconfig', + method: 'post', + data: data + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/game/sugar/dailywin/api.js b/ruoyi-ui/src/api/game/sugar/dailywin/api.js new file mode 100644 index 0000000..96c1d32 --- /dev/null +++ b/ruoyi-ui/src/api/game/sugar/dailywin/api.js @@ -0,0 +1,17 @@ +import request from '@/utils/request' + + +export function list(query) { + return request({ + url: '/admin/sugar/dailywin/list', + method: 'get', + params: query + }) +} + +export function resetRewardPool() { + return request({ + url: '/api/sugar/rewardpool/reset', + method: 'post' + }) +} diff --git a/ruoyi-ui/src/api/game/sugar/mock/api.js b/ruoyi-ui/src/api/game/sugar/mock/api.js new file mode 100644 index 0000000..cce9960 --- /dev/null +++ b/ruoyi-ui/src/api/game/sugar/mock/api.js @@ -0,0 +1,18 @@ +import request from '@/utils/request' + + +export function list(query) { + return request({ + url: '/admin/sugar/mockresult/list', + method: 'get', + params: query + }) +} + +export function update(data) { + return request({ + url: '/admin/sugar/mockresult/list', + method: 'POST', + data: data + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/game/sugar/record/api.js b/ruoyi-ui/src/api/game/sugar/record/api.js new file mode 100644 index 0000000..c6ace5d --- /dev/null +++ b/ruoyi-ui/src/api/game/sugar/record/api.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + + +export function listRecord(query) { + return request({ + url: '/admin/sugar/user/record/list', + method: 'get', + params: query + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/game/sugar/user/api.js b/ruoyi-ui/src/api/game/sugar/user/api.js new file mode 100644 index 0000000..1c388d1 --- /dev/null +++ b/ruoyi-ui/src/api/game/sugar/user/api.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询注册用户信息列表 +export function listUser(query) { + return request({ + url: '/admin/sugar/user/list', + method: 'get', + params: query + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/game/sweet/box/api.js b/ruoyi-ui/src/api/game/sweet/box/api.js new file mode 100644 index 0000000..dc71336 --- /dev/null +++ b/ruoyi-ui/src/api/game/sweet/box/api.js @@ -0,0 +1,97 @@ +import request from '@/utils/request' + +export function getBoxList(params) { + return request({ + url: '/admin/sweet/box/list', + method: 'get', + params + }) +} + +export function saveBox(data) { + return request({ + url: '/admin/sweet/box/save', + method: 'post', + data + }) +} + +export function getBoxOrnaments(boxId) { + return request({ + url: `/admin/sweet/box/ornaments/${boxId}`, + method: 'get' + }) +} + +export function saveBoxOrnaments(data) { + return request({ + url: '/admin/sweet/box/ornaments/save', + method: 'post', + data + }) +} + +export function calcDistribution(data) { + return request({ + url: '/admin/sweet/box/ornaments/calc', + method: 'post', + data + }) +} + +// 模拟 payoutCap 约束下的预估实际 RTP +export function simulateRtp(data) { + return request({ + url: '/admin/sweet/box/ornaments/simulate-rtp', + method: 'post', + data + }) +} + +// ==================== 箱子饰品分布模板(后端持久化) ==================== + +// 查询某箱子的模板列表 +export function getBoxTemplates(boxId) { + return request({ + url: `/admin/sweet/box/templates/${boxId}`, + method: 'get' + }) +} + +// 保存当前分布为模板 +export function saveBoxTemplate(data) { + return request({ + url: '/admin/sweet/box/template/save', + method: 'post', + data + }) +} + +// 删除模板 +export function deleteBoxTemplate(id) { + return request({ + url: `/admin/sweet/box/template/${id}`, + method: 'delete' + }) +} + +// 应用模板(写入分布 + 标记为生效模板) +export function applyBoxTemplate(data) { + return request({ + url: '/admin/sweet/box/template/apply', + method: 'post', + data + }) +} + +// ==================== 奖池水位 ==================== + +export function getPoolStatus() { + return request({ url: '/admin/sweet/pool/status', method: 'get' }) +} +export function addPoolBet(data) { + return request({ url: '/admin/sweet/pool/addBet', method: 'post', data }) +} +export function addPoolWin(data) { + return request({ url: '/admin/sweet/pool/addWin', method: 'post', data }) +} diff --git a/ruoyi-ui/src/api/game/sweet/dailywin/api.js b/ruoyi-ui/src/api/game/sweet/dailywin/api.js new file mode 100644 index 0000000..733b711 --- /dev/null +++ b/ruoyi-ui/src/api/game/sweet/dailywin/api.js @@ -0,0 +1,17 @@ +import request from '@/utils/request' + +export function getWinList(params) { + return request({ + url: '/admin/sweet/win/list', + method: 'get', + params + }) +} + +export function getWinSummary(params) { + return request({ + url: '/admin/sweet/win/summary', + method: 'get', + params + }) +} diff --git a/ruoyi-ui/src/api/game/sweet/ornament/api.js b/ruoyi-ui/src/api/game/sweet/ornament/api.js new file mode 100644 index 0000000..2188984 --- /dev/null +++ b/ruoyi-ui/src/api/game/sweet/ornament/api.js @@ -0,0 +1,8 @@ +import request from '@/utils/request' + +export function getOrnamentList() { + return request({ + url: '/admin/sweet/ornament/list', + method: 'get' + }) +} diff --git a/ruoyi-ui/src/api/game/sweet/prize/api.js b/ruoyi-ui/src/api/game/sweet/prize/api.js new file mode 100644 index 0000000..f28b8e2 --- /dev/null +++ b/ruoyi-ui/src/api/game/sweet/prize/api.js @@ -0,0 +1,16 @@ +import request from '@/utils/request' + +export function prizeList() { + return request({ + url: '/admin/sweet/prize/list', + method: 'get' + }) +} + +export function prizeSave(data) { + return request({ + url: '/admin/sweet/prize/save', + method: 'post', + data + }) +} diff --git a/ruoyi-ui/src/api/game/sweet/record/api.js b/ruoyi-ui/src/api/game/sweet/record/api.js new file mode 100644 index 0000000..188a22f --- /dev/null +++ b/ruoyi-ui/src/api/game/sweet/record/api.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +export function getRecordList(params) { + return request({ + url: '/admin/sweet/record/list', + method: 'get', + params + }) +} diff --git a/ruoyi-ui/src/api/game/sweet/round/api.js b/ruoyi-ui/src/api/game/sweet/round/api.js new file mode 100644 index 0000000..059261b --- /dev/null +++ b/ruoyi-ui/src/api/game/sweet/round/api.js @@ -0,0 +1,17 @@ +import request from '@/utils/request' + +export function getRoundList(params) { + return request({ + url: '/admin/sweet/round/list', + method: 'get', + params + }) +} + +export function roundDelete(ids) { + return request({ + url: '/admin/sweet/round/delete', + method: 'delete', + data: ids + }) +} diff --git a/ruoyi-ui/src/api/game/wheel/config/api.js b/ruoyi-ui/src/api/game/wheel/config/api.js new file mode 100644 index 0000000..3477607 --- /dev/null +++ b/ruoyi-ui/src/api/game/wheel/config/api.js @@ -0,0 +1,86 @@ +import request from '@/utils/request' + +// ======================== 游戏配置 ======================== + +export function getConfig() { + return request({ + url: '/admin/wheel/gameconfig', + method: 'get' + }) +} + +export function updateConfig(data) { + return request({ + url: '/admin/wheel/gameconfig', + method: 'post', + data: data + }) +} + +// ======================== 4池统计 ======================== + +export function getPoolStats() { + return request({ + url: '/admin/wheel/pool/stats', + method: 'get' + }) +} + +// ======================== 奖励矩阵 CRUD ======================== + +export function getMatrixList(params) { + return request({ + url: '/admin/wheel/matrix/list', + method: 'get', + params + }) +} + +export function saveMatrix(data) { + return request({ + url: '/admin/wheel/matrix/save', + method: 'post', + data + }) +} + +export function deleteMatrix(id) { + return request({ + url: '/admin/wheel/matrix/delete/' + id, + method: 'delete' + }) +} + +// ======================== 奖池调节 ======================== + +export function adjustPool(data) { + return request({ + url: '/admin/wheel/pool/adjust', + method: 'post', + data: data + }) +} + +export function resetPool(data) { + return request({ + url: '/admin/wheel/pool/reset', + method: 'post', + data: data + }) +} + +export function getAdjustLog(params) { + return request({ + url: '/admin/wheel/pool/adjustLog', + method: 'get', + params: params + }) +} + +export function getPoolBalance(params) { + return request({ + url: '/admin/wheel/pool/balance', + method: 'get', + params: params + }) +} diff --git a/ruoyi-ui/src/api/index.js b/ruoyi-ui/src/api/index.js new file mode 100644 index 0000000..ad0fb2e --- /dev/null +++ b/ruoyi-ui/src/api/index.js @@ -0,0 +1,43 @@ +import request from '@/utils/request' + +export function getBalance() { + return request({ + url: '/admin/cs340item/checkMerchantBalance', + method: 'get', + }) +} +export function developmentInfo() { + return request({ + url: '/admin/zbt/developmentInfo', + method: 'get', + }) +} + +export function getV5itemBalance() { + return request({ + url: '/admin/v5item/checkMerchantBalance', + method: 'get', + }) +} + +export function getHomeStats() { + return request({ + url: '/admin/rechargeRecord/homeStats', + method: 'get', + }) +} + +export function getChartData(type) { + return request({ + url: '/admin/rechargeRecord/chartData', + method: 'get', + params: { type } + }) +} + +export function getRtpData() { + return request({ + url: '/admin/rechargeRecord/realTimeRtp', + method: 'get', + }) +} diff --git a/ruoyi-ui/src/api/login.js b/ruoyi-ui/src/api/login.js new file mode 100644 index 0000000..9b71d78 --- /dev/null +++ b/ruoyi-ui/src/api/login.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 登录方法 +export function login(username, password, code, uuid) { + const data = { + username, + password, + code, + uuid + } + return request({ + url: '/login', + headers: { + isToken: false, + repeatSubmit: false + }, + method: 'post', + data: data + }) +} + +// 注册方法 +export function register(data) { + return request({ + url: '/register', + headers: { + isToken: false + }, + method: 'post', + data: data + }) +} + +// 获取用户详细信息 +export function getInfo() { + return request({ + url: '/getInfo', + method: 'get' + }) +} + +// 退出方法 +export function logout() { + return request({ + url: '/logout', + method: 'post' + }) +} + +// 获取验证码 +export function getCodeImg() { + return request({ + url: '/captchaImage', + headers: { + isToken: false + }, + method: 'get', + timeout: 20000 + }) +} diff --git a/ruoyi-ui/src/api/menu.js b/ruoyi-ui/src/api/menu.js new file mode 100644 index 0000000..faef101 --- /dev/null +++ b/ruoyi-ui/src/api/menu.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 获取路由 +export const getRouters = () => { + return request({ + url: '/getRouters', + method: 'get' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/monitor/cache.js b/ruoyi-ui/src/api/monitor/cache.js new file mode 100644 index 0000000..72c5f6a --- /dev/null +++ b/ruoyi-ui/src/api/monitor/cache.js @@ -0,0 +1,57 @@ +import request from '@/utils/request' + +// 查询缓存详细 +export function getCache() { + return request({ + url: '/monitor/cache', + method: 'get' + }) +} + +// 查询缓存名称列表 +export function listCacheName() { + return request({ + url: '/monitor/cache/getNames', + method: 'get' + }) +} + +// 查询缓存键名列表 +export function listCacheKey(cacheName) { + return request({ + url: '/monitor/cache/getKeys/' + cacheName, + method: 'get' + }) +} + +// 查询缓存内容 +export function getCacheValue(cacheName, cacheKey) { + return request({ + url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey, + method: 'get' + }) +} + +// 清理指定名称缓存 +export function clearCacheName(cacheName) { + return request({ + url: '/monitor/cache/clearCacheName/' + cacheName, + method: 'delete' + }) +} + +// 清理指定键名缓存 +export function clearCacheKey(cacheKey) { + return request({ + url: '/monitor/cache/clearCacheKey/' + cacheKey, + method: 'delete' + }) +} + +// 清理全部缓存 +export function clearCacheAll() { + return request({ + url: '/monitor/cache/clearCacheAll', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/monitor/job.js b/ruoyi-ui/src/api/monitor/job.js new file mode 100644 index 0000000..3815569 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/job.js @@ -0,0 +1,71 @@ +import request from '@/utils/request' + +// 查询定时任务调度列表 +export function listJob(query) { + return request({ + url: '/monitor/job/list', + method: 'get', + params: query + }) +} + +// 查询定时任务调度详细 +export function getJob(jobId) { + return request({ + url: '/monitor/job/' + jobId, + method: 'get' + }) +} + +// 新增定时任务调度 +export function addJob(data) { + return request({ + url: '/monitor/job', + method: 'post', + data: data + }) +} + +// 修改定时任务调度 +export function updateJob(data) { + return request({ + url: '/monitor/job', + method: 'put', + data: data + }) +} + +// 删除定时任务调度 +export function delJob(jobId) { + return request({ + url: '/monitor/job/' + jobId, + method: 'delete' + }) +} + +// 任务状态修改 +export function changeJobStatus(jobId, status) { + const data = { + jobId, + status + } + return request({ + url: '/monitor/job/changeStatus', + method: 'put', + data: data + }) +} + + +// 定时任务立即执行一次 +export function runJob(jobId, jobGroup) { + const data = { + jobId, + jobGroup + } + return request({ + url: '/monitor/job/run', + method: 'put', + data: data + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/monitor/jobLog.js b/ruoyi-ui/src/api/monitor/jobLog.js new file mode 100644 index 0000000..6e0be61 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/jobLog.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 查询调度日志列表 +export function listJobLog(query) { + return request({ + url: '/monitor/jobLog/list', + method: 'get', + params: query + }) +} + +// 删除调度日志 +export function delJobLog(jobLogId) { + return request({ + url: '/monitor/jobLog/' + jobLogId, + method: 'delete' + }) +} + +// 清空调度日志 +export function cleanJobLog() { + return request({ + url: '/monitor/jobLog/clean', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/monitor/logininfor.js b/ruoyi-ui/src/api/monitor/logininfor.js new file mode 100644 index 0000000..4d112b7 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/logininfor.js @@ -0,0 +1,34 @@ +import request from '@/utils/request' + +// 查询登录日志列表 +export function list(query) { + return request({ + url: '/monitor/logininfor/list', + method: 'get', + params: query + }) +} + +// 删除登录日志 +export function delLogininfor(infoId) { + return request({ + url: '/monitor/logininfor/' + infoId, + method: 'delete' + }) +} + +// 解锁用户登录状态 +export function unlockLogininfor(userName) { + return request({ + url: '/monitor/logininfor/unlock/' + userName, + method: 'get' + }) +} + +// 清空登录日志 +export function cleanLogininfor() { + return request({ + url: '/monitor/logininfor/clean', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/monitor/online.js b/ruoyi-ui/src/api/monitor/online.js new file mode 100644 index 0000000..b5c2303 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/online.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 查询在线用户列表 +export function list(query) { + return request({ + url: '/monitor/online/list', + method: 'get', + params: query + }) +} + +// 强退用户 +export function forceLogout(tokenId) { + return request({ + url: '/monitor/online/' + tokenId, + method: 'delete' + }) +} + +// 心跳 +export function heartbeat() { + return request({ + url: '/monitor/online/heartbeat', + method: 'post' + }) +} diff --git a/ruoyi-ui/src/api/monitor/operlog.js b/ruoyi-ui/src/api/monitor/operlog.js new file mode 100644 index 0000000..a04bca8 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/operlog.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 查询操作日志列表 +export function list(query) { + return request({ + url: '/monitor/operlog/list', + method: 'get', + params: query + }) +} + +// 删除操作日志 +export function delOperlog(operId) { + return request({ + url: '/monitor/operlog/' + operId, + method: 'delete' + }) +} + +// 清空操作日志 +export function cleanOperlog() { + return request({ + url: '/monitor/operlog/clean', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/monitor/server.js b/ruoyi-ui/src/api/monitor/server.js new file mode 100644 index 0000000..e1f9ca2 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/server.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 获取服务信息 +export function getServer() { + return request({ + url: '/monitor/server', + method: 'get' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/skins/blankCardKey/api.js b/ruoyi-ui/src/api/skins/blankCardKey/api.js new file mode 100644 index 0000000..b940a34 --- /dev/null +++ b/ruoyi-ui/src/api/skins/blankCardKey/api.js @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +// 获取充值产品列表(金额选项) +export function getRechargeList() { + return request({ + url: '/admin/blankCardKey/rechargeList', + method: 'get' + }) +} + +// 生成空白卡密 +export function generateBlankCardKeys(data) { + return request({ + url: '/admin/blankCardKey/generate', + method: 'post', + data: data + }) +} + +// 获取空白卡密列表 +export function getBlankCardKeyList(params) { + return request({ + url: '/admin/blankCardKey/list', + method: 'get', + params: params + }) +} diff --git a/ruoyi-ui/src/api/skins/promoCdkeyRedeem/api.js b/ruoyi-ui/src/api/skins/promoCdkeyRedeem/api.js new file mode 100644 index 0000000..f2d9002 --- /dev/null +++ b/ruoyi-ui/src/api/skins/promoCdkeyRedeem/api.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询推广CDKEY兑换记录列表 +export function listPromoCdkeyRedeem(query) { + return request({ + url: '/admin/promoCdkeyRedeem/list', + method: 'get', + params: query + }) +} diff --git a/ruoyi-ui/src/api/skins/transfer/api.js b/ruoyi-ui/src/api/skins/transfer/api.js new file mode 100644 index 0000000..2e944cf --- /dev/null +++ b/ruoyi-ui/src/api/skins/transfer/api.js @@ -0,0 +1,55 @@ +import request from '@/utils/request' + +// 转赠申请列表 +export function transferApplyList(query) { + return request({ + url: '/admin/transferApply/list', + method: 'get', + params: query + }) +} + +// 审核通过 +export function approveTransfer(id) { + return request({ + url: '/admin/transferApply/approve', + method: 'post', + params: { id } + }) +} + +// 审核驳回 +export function rejectTransfer(id, rejectReason) { + return request({ + url: '/admin/transferApply/reject', + method: 'post', + params: { id, rejectReason } + }) +} + +// 转赠申请批次列表(按批次聚合,含饰品明细) +export function transferApplyBatchList(query) { + return request({ + url: '/admin/transferApply/batchList', + method: 'get', + params: query + }) +} + +// 批次审核通过(整个批次一起通过) +export function approveTransferBatch(batchId) { + return request({ + url: '/admin/transferApply/approveBatch', + method: 'post', + params: { batchId } + }) +} + +// 批次审核驳回(整个批次一起驳回) +export function rejectTransferBatch(batchId, rejectReason) { + return request({ + url: '/admin/transferApply/rejectBatch', + method: 'post', + params: { batchId, rejectReason } + }) +} diff --git a/ruoyi-ui/src/api/skins/ttBonus/api.js b/ruoyi-ui/src/api/skins/ttBonus/api.js new file mode 100644 index 0000000..5deeea9 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttBonus/api.js @@ -0,0 +1,39 @@ +import request from '@/utils/request' +//福利-查询福利列表 +export function getBonus(query) { + return request({ + url: '/admin/bonus/list', + method: 'get', + params: query + }) +} +//福利-获取福利详细信息 +export function getBonusid(id) { + return request({ + url: `/admin/bonus/${id}`, + method: 'get', + }) +} +//福利-获取福利详细信息 +export function delBonus(ids) { + return request({ + url: `/admin/bonus/${ids}`, + method: 'delete', + }) +} +//福利-新增福利 +export function addBonus(data) { + return request({ + url: '/admin/bonus', + method: 'post', + data: data + }) +} +//福利-修改福利 +export function updateBonus (data) { + return request({ + url: '/admin/bonus', + method: 'put', + data: data + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/skins/ttBox/api.js b/ruoyi-ui/src/api/skins/ttBox/api.js new file mode 100644 index 0000000..58450cc --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttBox/api.js @@ -0,0 +1,191 @@ +import request from '@/utils/request' + +// 查询宝箱数据列表 +export function listBox(query) { + return request({ + url: '/admin/box/list', + method: 'get', + params: query + }) +} + +// 查询宝箱数据详细 +export function getBox(boxId) { + return request({ + url: '/admin/box/' + boxId, + method: 'get' + }) +} + +// 新增宝箱数据 +export function addBox(data) { + return request({ + url: '/admin/box', + method: 'post', + data: data + }) +} + +// 修改宝箱数据 +export function updateBox(data) { + return request({ + url: '/admin/box', + method: 'put', + data: data + }) +} + +// 删除宝箱数据 +export function delBox(boxId) { + return request({ + url: '/admin/box/' + boxId, + method: 'delete' + }) +} + +//宝箱类型 +export function boxTypelist(data) { + return request({ + url: '/admin/boxType/list', + method: 'get', + params:data + }) +} +//新增宝箱 + +export function boxType(data) { + return request({ + url: '/admin/boxType', + method: 'post', + data:data + }) +} +// +export function boxTypeChange(data) { + return request({ + url: '/admin/boxType', + method: 'put', + data:data + }) +} +// +export function boxTypeId(id) { + return request({ + url: `/admin/boxType/${id}`, + method: 'get', + }) +} +// +export function boxChange(data) { + return request({ + url: `/admin/box`, + method: 'put', + data:data + }) +} +//宝箱类型-删除宝箱类型数据 +export function delBoxType(ids) { + return request({ + url: `/admin/boxType/${ids}`, + method: 'delete', + }) +} +//宝箱-查看奖项 +export function getBoxOrnaments(boxId,page,size, includeChildren = false) { + let url = `/admin/boxOrnaments/list/${boxId}?pageNum=${page}&pageSize=${size}`; + if (includeChildren) url += '&includeChildren=true'; + return request({ url, method: 'get' }) +} +//宝箱-修改奖项 +export function saveBoxOrnaments(data) { + return request({ + url: `/admin/boxOrnaments`, + method: 'put', + data:data + }) +} +//宝箱-新增奖项 +export function addBoxOrnaments(data) { + return request({ + url: `/admin/boxOrnaments`, + method: 'post', + data:data + }) +} +//宝箱-获取奖项详细信息 +export function getidBoxOrnaments(id) { + return request({ + url: `/admin/boxOrnaments/${id}`, + method: 'get', + }) +} +//宝箱-获取奖项详细信息 +export function delBoxOrnaments(boxId,ids) { + return request({ + url: `/admin/boxOrnaments/${boxId}/${ids}`, + method: 'delete', + }) +} +//宝箱记录-获取宝箱记录数据列表 +export function getBoxRecords(data) { + return request({ + url: `/admin/boxRecords/list`, + method: 'get', + params:data + }) +} +//获取宝箱利润率 +export function getProfitMargin(boxId) { + return request({ + url: `/admin/boxOrnaments/getProfitMargin/${boxId}`, + method: 'get', + }) +} + +//校验饰品是否可添加到宝箱(饰品独占校验,按 ornamentId) +export function checkOrnamentAvailability(ornamentId) { + return request({ + url: `/admin/boxOrnaments/checkOrnament/${ornamentId}`, + method: 'get', + }) +} + +//饰品搜索 (支持价格范围) +export function searchOrnaments(query) { + return request({ + url: '/admin/ornament/list', + method: 'get', + params: query + }) +} + +//宝箱饰品-批量填货 + export function batchAdd(data) { + return request({ + url: `/admin/boxOrnaments/batchAdd`, + method: 'post', + data: data + }) +} +//宝箱-重置宝箱 + export function resetBox(boxId) { + return request({ + url: `/admin/box/resetBox/${boxId}`, + method: 'get', + }) +} +//宝箱-配置金色箱子壳子(金中金模式) + export function setupGoldenBox(boxId) { + return request({ + url: `/admin/boxOrnaments/setupGoldenBox/${boxId}`, + method: 'post', + }) +} +// +//宝箱-统计宝箱数据 + export function countBox(boxId,data) { + return request({ + url: `/admin/box/statisticsBoxData/${boxId}?date=${data}`, + method: 'get', + }) +} diff --git a/ruoyi-ui/src/api/skins/ttCardMerchantRecharge/api.js b/ruoyi-ui/src/api/skins/ttCardMerchantRecharge/api.js new file mode 100644 index 0000000..7714bd7 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttCardMerchantRecharge/api.js @@ -0,0 +1,37 @@ +import request from '@/utils/request' + +// 获取卡商列表 +export function getCardMerchantList(query) { + return request({ + url: '/admin/cardMerchant/list', + method: 'get', + params: query + }) +} + +// 卡商充值(管理员给卡商增加余额) +export function cardMerchantRecharge(data) { + return request({ + url: '/admin/cardMerchant/recharge', + method: 'post', + data: data + }) +} + +// 修改卡商余额(管理员直接把卡商余额改成指定值) +export function updateCardMerchantBalance(data) { + return request({ + url: '/admin/cardMerchant/updateBalance', + method: 'post', + data: data + }) +} + +// 卡商充值记录 +export function getCardMerchantRechargeLog(query) { + return request({ + url: '/admin/cardMerchant/rechargeLog', + method: 'get', + params: query + }) +} diff --git a/ruoyi-ui/src/api/skins/ttFight/api.js b/ruoyi-ui/src/api/skins/ttFight/api.js new file mode 100644 index 0000000..f13c1c4 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttFight/api.js @@ -0,0 +1,24 @@ +import request from '@/utils/request' + +//获取对战列表 +export function getFightlist(data){ + return request({ + url: '/admin/fight/list', + method: 'get', + params: data + }) +} +//获取对战宝箱列表 +export function getFightBoxList(fightId){ + return request({ + url: `/admin/fight/getFightBoxList/${fightId}`, + method: 'get', + }) +} +//查询对战结果 +export function getFightResult (fightId){ + return request({ + url: `/admin/fightResult/getFightResult/${fightId}`, + method: 'get', + }) +} diff --git a/ruoyi-ui/src/api/skins/ttFight/fightRankingReward.js b/ruoyi-ui/src/api/skins/ttFight/fightRankingReward.js new file mode 100644 index 0000000..176a7d5 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttFight/fightRankingReward.js @@ -0,0 +1,58 @@ +import request from '@/utils/request' + +// 查询对战奖励金额列表 +export function listReward(query) { + return request({ + url: '/admin/reward/list', + method: 'get', + params: query + }) +} + +// 查询对战奖励金额详细 +export function getReward(id) { + return request({ + url: '/admin/reward/' + id, + method: 'get' + }) +} + +// 新增对战奖励金额 +export function addReward(data) { + return request({ + url: '/admin/reward', + method: 'post', + data: data + }) +} + +export function generateRankingReward() { + return request({ + url: '/admin/reward/generateRankingReward/', + method: 'post' + }) +} + +// 修改对战奖励金额 +export function updateReward(data) { + return request({ + url: '/admin/reward', + method: 'put', + data: data + }) +} + +// 删除对战奖励金额 +export function delReward(id) { + return request({ + url: '/admin/reward/' + id, + method: 'delete' + }) +} + +export function truncateRankingReward() { + return request({ + url: '/admin/reward/truncateRankingReward', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttFight/robotFightConfig.js b/ruoyi-ui/src/api/skins/ttFight/robotFightConfig.js new file mode 100644 index 0000000..8d7788a --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttFight/robotFightConfig.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询机器人对战进房时间配置列表 +export function listRobotFightConfig(query) { + return request({ + url: '/admin/robotFightConfig/list', + method: 'get', + params: query + }) +} + +// 查询机器人对战进房时间配置详细 +export function getRobotFightConfig(id) { + return request({ + url: '/admin/robotFightConfig/' + id, + method: 'get' + }) +} + +// 新增机器人对战进房时间配置 +export function addRobotFightConfig(data) { + return request({ + url: '/admin/robotFightConfig', + method: 'post', + data: data + }) +} + +// 修改机器人对战进房时间配置 +export function updateRobotFightConfig(data) { + return request({ + url: '/admin/robotFightConfig', + method: 'put', + data: data + }) +} + +// 删除机器人对战进房时间配置 +export function delRobotFightConfig(id) { + return request({ + url: '/admin/robotFightConfig/' + id, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttFight/robotFightGroup.js b/ruoyi-ui/src/api/skins/ttFight/robotFightGroup.js new file mode 100644 index 0000000..49a7bf7 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttFight/robotFightGroup.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询机器人对战分组列表 +export function listGroup(query) { + return request({ + url: '/admin/group/list', + method: 'get', + params: query + }) +} + +// 查询机器人对战分组详细 +export function getGroup(groupId) { + return request({ + url: '/admin/group/' + groupId, + method: 'get' + }) +} + +// 新增机器人对战分组 +export function addGroup(data) { + return request({ + url: '/admin/group', + method: 'post', + data: data + }) +} + +// 修改机器人对战分组 +export function updateGroup(data) { + return request({ + url: '/admin/group', + method: 'put', + data: data + }) +} + +// 删除机器人对战分组 +export function delGroup(groupId) { + return request({ + url: '/admin/group/' + groupId, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttFight/robotFightGroupBox.js b/ruoyi-ui/src/api/skins/ttFight/robotFightGroupBox.js new file mode 100644 index 0000000..fae361c --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttFight/robotFightGroupBox.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询机器人对战分组宝箱列表 +export function listGroupBox(groupId, query) { + return request({ + url: '/admin/group/box/list/' + groupId, + method: 'get', + params: query + }) +} + +// 查询机器人对战分组宝箱详细 +export function getGroupBox(groupId, boxId) { + return request({ + url: '/admin/group/box/' + groupId + '/' + boxId, + method: 'get' + }) +} + +// 新增机器人对战分组宝箱 +export function addGroupBox(data) { + return request({ + url: '/admin/group/box', + method: 'post', + data: data + }) +} + +// 修改机器人对战分组宝箱 +export function updateGroupBox(data) { + return request({ + url: '/admin/group/box', + method: 'put', + data: data + }) +} + +// 删除机器人对战分组宝箱 +export function delGroupBox(groupId, boxId) { + return request({ + url: '/admin/group/box/' + groupId + "/" + boxId, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttFight/robotFightPriceConfig.js b/ruoyi-ui/src/api/skins/ttFight/robotFightPriceConfig.js new file mode 100644 index 0000000..37417d1 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttFight/robotFightPriceConfig.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询机器人对战价格档位配置列表 +export function listRobotFightPriceConfig(query) { + return request({ + url: '/admin/robotFightPriceConfig/list', + method: 'get', + params: query + }) +} + +// 查询机器人对战价格档位配置详细 +export function getRobotFightPriceConfig(id) { + return request({ + url: '/admin/robotFightPriceConfig/' + id, + method: 'get' + }) +} + +// 新增机器人对战价格档位配置 +export function addRobotFightPriceConfig(data) { + return request({ + url: '/admin/robotFightPriceConfig', + method: 'post', + data: data + }) +} + +// 修改机器人对战价格档位配置 +export function updateRobotFightPriceConfig(data) { + return request({ + url: '/admin/robotFightPriceConfig', + method: 'put', + data: data + }) +} + +// 删除机器人对战价格档位配置 +export function delRobotFightPriceConfig(id) { + return request({ + url: '/admin/robotFightPriceConfig/' + id, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttFirstRecharge/api.js b/ruoyi-ui/src/api/skins/ttFirstRecharge/api.js new file mode 100644 index 0000000..91e4c3f --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttFirstRecharge/api.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询首充赠送列表 +export function listRecharge(query) { + return request({ + url: '/admin/recharge/list', + method: 'get', + params: query + }) +} + +// 查询首充赠送详细 +export function getRecharge(id) { + return request({ + url: '/admin/recharge/' + id, + method: 'get' + }) +} + +// 新增首充赠送 +export function addRecharge(data) { + return request({ + url: '/admin/recharge', + method: 'post', + data: data + }) +} + +// 修改首充赠送 +export function updateRecharge(data) { + return request({ + url: '/admin/recharge', + method: 'put', + data: data + }) +} + +// 删除首充赠送 +export function delRecharge(id) { + return request({ + url: '/admin/recharge/' + id, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttGenerateCard/api.js b/ruoyi-ui/src/api/skins/ttGenerateCard/api.js new file mode 100644 index 0000000..9dc3c3a --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttGenerateCard/api.js @@ -0,0 +1,30 @@ +import request from '@/utils/request' + + +export function getRechargeCard(query) { + return request({ + url: '/admin/rechargeCard/list', + method: 'get', + params: query + }) +} +export function addRechargeCard(rechargeListId,num) { + return request({ + url: `/admin/rechargeCard/generateCard/${rechargeListId}/${num}`, + method:'post' + }) +} + +export function addBlankRechargeCard(rechargeListId,num) { + return request({ + url: `/admin/rechargeCard/generateBlankCard/${rechargeListId}/${num}`, + method:'post' + }) +} + +export function downloadRechargeCard() { + return request({ + url: `/admin/rechargeCard/export`, + method:'post' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttOrder/api.js b/ruoyi-ui/src/api/skins/ttOrder/api.js new file mode 100644 index 0000000..a899ddb --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttOrder/api.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 网站支付订单查询 +export function orderList(data) { + return request({ + url: '/admin/order/list', + method: 'post', + data: data + }) +} diff --git a/ruoyi-ui/src/api/skins/ttOrnaments/api.js b/ruoyi-ui/src/api/skins/ttOrnaments/api.js new file mode 100644 index 0000000..0c677fd --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttOrnaments/api.js @@ -0,0 +1,96 @@ +import request from '@/utils/request' + +// 查询饰品数据列表 +export function listOrnaments(query) { + return request({ + url: '/admin/ornaments/list', + method: 'get', + params: query + }) +} + +// 查询饰品数据详细 +export function getOrnaments(id) { + return request({ + url: '/admin/ornaments/' + id, + method: 'get' + }) +} + +// 新增饰品数据 +export function addOrnaments(data) { + return request({ + url: '/admin/ornaments/addTtOrnament', + method: 'post', + data: data + }) +} +// 发放饰品 +export function grantOrnaments(data,userId,ornamentsId,num) { + return request({ + url: `/admin/ornaments/grantOrnaments/${userId}/${ornamentsId}/${num}`, + method: 'post', + params:data + }) +} + +// 修改饰品数据 +export function updateOrnaments(data) { + return request({ + url: '/admin/ornaments/editTtOrnament', + method: 'post', + data: data + }) +} + +// 删除饰品数据 +export function delOrnaments(id) { + return request({ + url: '/admin/ornaments/' + id, + method: 'delete' + }) +} + +// 网站道具-查询网站道具列表 +export function getWebsiteProperty(params) { + return request({ + url: '/admin/websiteProperty/list', + method: 'get', + params: params + }) +} + +// 网站道具-查询网站道具详细信息 +export function getWebsitePropertyid(id) { + return request({ + url: `/admin/websiteProperty/${id}`, + method: 'get', + }) +} + +// 网站道具-删除网站道具 +export function delWebsiteProperty(ids) { + return request({ + url: `/admin/websiteProperty/${ids}`, + method: 'delete' + }) +} + +// 网站道具-修改网站道具 +export function changeWebsiteProperty(data) { + return request({ + url: '/admin/websiteProperty', + method: 'put', + data: data + }) +} + +// 网站道具-新增网站道具 +export function addWebsiteProperty(data) { + return request({ + url: '/admin/websiteProperty', + method: 'post', + data: data + }) +} + diff --git a/ruoyi-ui/src/api/skins/ttOrnaments/boxOpenChance.js b/ruoyi-ui/src/api/skins/ttOrnaments/boxOpenChance.js new file mode 100644 index 0000000..e201e08 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttOrnaments/boxOpenChance.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询开箱机会列表 +export function listChance(query) { + return request({ + url: '/admin/chance/list', + method: 'get', + params: query + }) +} + +// 查询开箱机会详细 +export function getChance(ornamentId) { + return request({ + url: '/admin/chance/' + ornamentId, + method: 'get' + }) +} + +// 新增开箱机会 +export function addChance(data) { + return request({ + url: '/admin/chance', + method: 'post', + data: data + }) +} + +// 修改开箱机会 +export function updateChance(data) { + return request({ + url: '/admin/chance', + method: 'put', + data: data + }) +} + +// 删除开箱机会 +export function delChance(ornamentId) { + return request({ + url: '/admin/chance/' + ornamentId, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttOrnamentsLevel/api.js b/ruoyi-ui/src/api/skins/ttOrnamentsLevel/api.js new file mode 100644 index 0000000..048089e --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttOrnamentsLevel/api.js @@ -0,0 +1,50 @@ +import request from '@/utils/request' + +// 查询饰品级别数据列表 +export function listLevel(query) { + return request({ + url: '/admin/ornamentsLevel/list', + method: 'get', + params: query + }) +} + +// 查询饰品级别数据详细 +export function getLevel(id) { + return request({ + url: '/admin/ornamentsLevel/' + id, + method: 'get' + }) +} + +// 新增饰品级别数据 +export function addLevel(num) { + return request({ + url: `/admin/ornamentsLevel/generateOrnamentsLevel/${num}`, + method: 'post', + }) +} + +// 修改饰品级别数据 +export function updateLevel(data) { + return request({ + url: '/admin/ornamentsLevel', + method: 'put', + data: data + }) +} + +// 删除饰品级别数据 +// export function delLevel(id) { +// return request({ +// url: '/admin/ornamentsLevel/' + id, +// method: 'delete' +// }) +// } +//重置列表 +export function resetLevel() { + return request({ + url: '/admin/ornamentsLevel', + method: 'post' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttRecharge/api.js b/ruoyi-ui/src/api/skins/ttRecharge/api.js new file mode 100644 index 0000000..3ec840b --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttRecharge/api.js @@ -0,0 +1,48 @@ +import request from '@/utils/request' + +//充值记录 +export function rechargeRecord(data) { + return request({ + url: '/admin/rechargeRecord/list', + method: 'get', + params:data + }) +} +//充值 +export function getRechargeList(data) { + return request({ + url: '/admin/rechargeList/list', + method: 'get', + params:data + }) +} +//修改 +export function changeRechargeList(data) { + return request({ + url: '/admin/rechargeList', + method: 'put', + data:data + }) +} +//新增 +export function addRechargeList(data) { + return request({ + url: '/admin/rechargeList', + method: 'post', + data:data + }) +} +//id查询 +export function getRechargeListid(id) { + return request({ + url: `/admin/rechargeList/${id}`, + method: 'get', + }) +} +//删除 +export function delRechargeList(id) { + return request({ + url: `/admin/rechargeList/${id}`, + method: 'delete', + }) +} diff --git a/ruoyi-ui/src/api/skins/ttRedPack/api.js b/ruoyi-ui/src/api/skins/ttRedPack/api.js new file mode 100644 index 0000000..34e3eb3 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttRedPack/api.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询【请填写功能名称】列表 +export function listTtRedPack(query) { + return request({ + url: '/admin/redPack/list', + method: 'get', + params: query + }) +} + +// 查询【请填写功能名称】详细 +export function getTtRedPack(id) { + return request({ + url: '/admin/redPack/' + id, + method: 'get' + }) +} + +// 新增【请填写功能名称】 +export function addTtRedPack(data) { + return request({ + url: '/admin/redPack', + method: 'post', + data: data + }) +} + +// 修改【请填写功能名称】 +export function updateTtRedPack(data) { + return request({ + url: '/admin/redPack', + method: 'put', + data: data + }) +} + +// 删除【请填写功能名称】 +export function delTtRedPack(id) { + return request({ + url: '/admin/redPack/' + id, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttReplacementRecord/api.js b/ruoyi-ui/src/api/skins/ttReplacementRecord/api.js new file mode 100644 index 0000000..071c69a --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttReplacementRecord/api.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询汰换记录列表 +export function listReplacementRecord(query) { + return request({ + url: '/admin/replacementRecord/list', + method: 'get', + params: query + }) +} + +// 查询汰换记录详细 +export function getReplacementRecord(id) { + return request({ + url: '/admin/replacementRecord/' + id, + method: 'get' + }) +} + +// 新增汰换记录 +export function addReplacementRecord(data) { + return request({ + url: '/admin/replacementRecord', + method: 'post', + data: data + }) +} + +// 修改汰换记录 +export function updateReplacementRecord(data) { + return request({ + url: '/admin/replacementRecord', + method: 'put', + data: data + }) +} + +// 删除汰换记录 +export function delReplacementRecord(id) { + return request({ + url: '/admin/replacementRecord/' + id, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttRoll/api.js b/ruoyi-ui/src/api/skins/ttRoll/api.js new file mode 100644 index 0000000..1a1d0e9 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttRoll/api.js @@ -0,0 +1,297 @@ +import request from "@/utils/request"; + +export function list(query) { + return request({ + url: "/admin/roll/list", + method: "get", + params: query, + }); +} +export function rollJackpotList(query) { + return request({ + url: "/admin/rollJackpot/list", + method: "get", + params: query + }); +} +//Roll房-查看奖项列表 +export function getRollPrizeList(rollId) { + return request({ + url: `/admin/roll/getRollPrizeList/${rollId}`, + method: "get", + }); +} +// 查看roll奖池的饰品 +export function rollJackpotOrnamentsList(query) { + return request({ + url: `/admin/rollJackpotOrnaments/list`, + method: "get", + params: query, + }); +} +//指定获奖者 +export function namedWinner(query) { + return request({ + url: "/admin/roll/namedWinner", + method: "post", + data: query, + }); +} +//取消指定roll房奖品 +export function cancelNamedWinner(data) { + return request({ + url: "/admin/roll/cancelNamedWinner", + method: "delete", + data: data, + }); +} +//获取指定将品用户 /admin/roll/getRollUsers/{rollId} +export function getzhidinguser(rollId) { + return request({ + url: `/admin/roll/getRollUsers/${rollId}`, + method: "get", + }); +} + + +export function roll(query) { + return request({ + url: "/admin/roll", + method: "post", + data: query, + }); +} +export function rollJackpotCreat(query) { + return request({ + url: "/admin/rollJackpot", + method: "post", + data: query, + }); +} +export function rollJackpotAdd(data) { + return request({ + url: "/admin/rollJackpotOrnaments", + method: "post", + data: data, + }); +} +export function rollJackpotChange(query) { + return request({ + url: "/admin/rollJackpot", + method: "put", + data: query, + }); +} +export function rollChange(query) { + return request({ + url: "/admin/roll", + method: "put", + data: query, + }); +} +export function delRoll(rollId) { + return request({ + url: `/admin/roll/remove/${rollId}`, + method: "delete", + }); +} +export function rollJackpotOrnamentDel(jackpotId, rollJackpotOrnamentsIds) { + return request({ + url: `/admin/rollJackpotOrnaments/${jackpotId}/${rollJackpotOrnamentsIds}`, + method: "delete", + }); +} +export function delRollJackpot(jackpotId) { + return request({ + url: `/admin/rollJackpot/remove/${jackpotId}`, + method: "delete", + }); +} +// 根据id差奖池 +export function rollJackpotdetial(jackpotId) { + return request({ + url: `/admin/rollJackpot/${jackpotId}`, + method: "get", + }); +} +//修改奖池饰品 +export function rollJackpotOrnamentsChange(query) { + return request({ + url: `/admin/rollJackpotOrnaments`, + method: "put", + data: query, + }); +} +//Roll房奖池饰品-批量填货 +export function batchAdd(rollId, OrnamentsIds) { + return request({ + url: `/admin/rollJackpotOrnaments/batchAdd/${rollId}/${OrnamentsIds}`, + method: "post", + }); +} + +//时间Roll房-新增时间Roll房 +export function addtimeRoll(data) { + return request({ + url: `/admin/timeRoll`, + method: "post", + data: data, + }); +} + +//时间Roll房-修改时间Roll房数据 +export function changetimeRoll(data) { + return request({ + url: `/admin/timeRoll`, + method: "put", + data: data, + }); +} + +//时间Roll房-状态修改 +export function changeStatus() { + return request({ + url: `/admin/timeRoll/changeStatus`, + method: "put", + }); +} + +//时间Roll房-查看奖项列表 + +export function getTimeRollPrizeList() { + return request({ + url: `/admin/timeRoll/getTimeRollPrizeList/${id}`, + method: "get", + }); +} + +//时间Roll房-查询时间Roll房列表 +export function getTimeRoll() { + return request({ + url: `/admin/timeRoll/list`, + method: "get", + }); +} + +//时间Roll房-指定获奖者 +export function timeNamedWinner(query) { + return request({ + url: `/admin/timeRoll/namedWinner`, + method: "post", + data: query, + }); +} + +//时间Roll房-删除时间Roll房 +export function delTimeroll(id) { + return request({ + url: `/admin/timeRoll/remove/${id}`, + method: "delete", + }); +} + +//时间Roll房-获取时间Roll房详细信息 + +export function getTimeRollDetial(timeRollId) { + return request({ + url: `/admin/timeRoll/${timeRollId}`, + method: "get", + }); +} + +export function getRollUser(rollId) { + return request({ + url: `/admin/roll/getRollUser/${rollId}`, + method: "get" + }); +} + +// Roll房实际中奖记录(开奖后参与用户中了什么饰品) +export function getRollWinningRecords(rollId) { + return request({ + url: `/admin/roll/winningRecords/${rollId}`, + method: "get" + }); +} + +export function addRobot(data) { + return request({ + url: `/admin/roll/addRobot`, + method: "post", + data: data + }); +} +// 搜索用户(按昵称) +export function searchUserByNick(nickName) { + return request({ + url: '/admin/user/list', + method: 'get', + params: { nickName, pageNum: 1, pageSize: 20 } + }); +} + +// 搜索用户(按用户ID) +export function searchUserById(userId) { + return request({ + url: '/admin/user/list', + method: 'get', + params: { userId, pageNum: 1, pageSize: 20 } + }); +} + +// 强制加入普通用户 +export function addUser(data) { + return request({ + url: '/admin/roll/addUser', + method: 'post', + data: data + }); +} + +// ============== 官方周期Roll配置 ============== +// 官方周期Roll配置-列表(分页) +export function officialRollConfigList(query) { + return request({ + url: '/admin/officialRollConfig/list', + method: 'get', + params: query + }); +} +// 官方周期Roll配置-详情 +export function officialRollConfigDetail(id) { + return request({ + url: `/admin/officialRollConfig/${id}`, + method: 'get' + }); +} +// 官方周期Roll配置-新增 +export function officialRollConfigAdd(data) { + return request({ + url: '/admin/officialRollConfig', + method: 'post', + data: data + }); +} +// 官方周期Roll配置-编辑 +export function officialRollConfigChange(data) { + return request({ + url: '/admin/officialRollConfig', + method: 'put', + data: data + }); +} +// 官方周期Roll配置-删除 +export function officialRollConfigDel(id) { + return request({ + url: `/admin/officialRollConfig/${id}`, + method: 'delete' + }); +} +// 官方周期Roll配置-启用/停用 +export function officialRollConfigChangeStatus(data) { + return request({ + url: '/admin/officialRollConfig/changeStatus', + method: 'put', + data: data + }); +} diff --git a/ruoyi-ui/src/api/skins/ttSetting/announcement.js b/ruoyi-ui/src/api/skins/ttSetting/announcement.js new file mode 100644 index 0000000..4600b90 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttSetting/announcement.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询公告列表 +export function listAnnouncement(query) { + return request({ + url: '/admin/announcement/list', + method: 'get', + params: query + }) +} + +// 查询公告详细 +export function getAnnouncement(announcementId) { + return request({ + url: '/admin/announcement/' + announcementId, + method: 'get' + }) +} + +// 新增公告 +export function addAnnouncement(data) { + return request({ + url: '/admin/announcement', + method: 'post', + data: data + }) +} + +// 修改公告 +export function updateAnnouncement(data) { + return request({ + url: '/admin/announcement', + method: 'put', + data: data + }) +} + +// 删除公告 +export function delAnnouncement(announcementId) { + return request({ + url: '/admin/announcement/' + announcementId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/skins/ttSetting/api.js b/ruoyi-ui/src/api/skins/ttSetting/api.js new file mode 100644 index 0000000..71e39fe --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttSetting/api.js @@ -0,0 +1,263 @@ +import request from '@/utils/request' + +//banner +//banner列表 +export function getBanner(query) { + return request({ + url: '/admin/banner/list', + method: 'get', + params: query + }) +} +export function getBannerid(id) { + return request({ + url: `/admin/banner/${id}`, + method: 'get', + }) +} +//添加banner +export function addBanner(query) { + return request({ + url: '/admin/banner', + method: 'post', + data: query + }) +} +//删除 +export function delBanner(ids) { + return request({ + url: `/admin/banner/${ids}`, + method: 'delete', + }) +} +//修改banner +export function changeBanner(query) { + return request({ + url: '/admin/banner', + method: 'put', + data: query + }) +} + + +//avatar +//avatar列表 + +export function getuserAvatar(query) { + return request({ + url: '/admin/userAvatar/list', + method: 'get', + params: query + }) +} +//新增 +export function adduserAvatar(query) { + return request({ + url: '/admin/userAvatar', + method: 'post', + data: query + }) +} +//修改 +export function changheuserAvatar(query) { + return request({ + url: '/admin/userAvatar', + method: 'put', + data: query + }) +} +//删除 +export function deluserAvatar(ids) { + return request({ + url: `/admin/userAvatar/${ids}`, + method: 'delete', + }) +} +//id +export function getuserAvatarid(id) { + return request({ + url: `/admin/userAvatar/${id}`, + method: 'get', + }) +} + + +//综合设置 +export function getParameterSetting() { + return request({ + url: `/admin/websiteSetup/getParameterSetting`, + method: 'post', + }) +} + +// +export function updateParameterSetting(data) { + return request({ + url: `/admin/websiteSetup/updateParameterSetting`, + method: 'post', + data:data + }) +} +//vip等级-生成vip等级 +export function addVipLevel(num) { + return request({ + url: `/admin/vipLevel/generateVipLevel/${num}`, + method: 'post', + }) +} +//vip等级-获取vip等级详细信息 +export function getVipLevelid(id) { + return request({ + url: `/admin/vipLevel/${id}`, + method: 'get', + }) +} +//vip等级-重置vip等级 +export function resetVipLevel(id) { + return request({ + url: `/admin/vipLevel`, + method: 'post', + }) +} + +//vip等级-获取vip等级列表 +export function getVipLevel() { + return request({ + url: `/admin/vipLevel/list`, + method: 'get', + }) +} +//vip等级-修改vip等级 +export function changeVipLevel(data) { + return request({ + url: `/admin/vipLevel`, + method: 'put', + data:data + }) +} + +//推广等级-生成推广等级 +export function generatePromotionLevel(num) { + return request({ + url: `/admin/promotionLevel/generatePromotionLevel/${num}`, + method: 'post', + }) +} +//推广等级-获取推广等级详细信息 +export function getPromotionLevelid(id) { + return request({ + url: `/admin/promotionLevel/${id}`, + method: 'get', + }) +} +//推广等级-重置推广等级 +export function resetPromotionLevel() { + return request({ + url: `/admin/promotionLevel`, + method: 'post', + }) +} + +//推广等级-获取推广等级列表 +export function getPromotionLevel() { + return request({ + url: `/admin/promotionLevel/list`, + method: 'get', + }) +} + +//推广等级-修改推广等级 +export function changePromotionLevel(data) { + return request({ + url: `/admin/promotionLevel`, + method: 'put', + data:data + }) +} + + +//文章管理-查询文章列表 +export function getContent(query) { + return request({ + url: `admin/content/list`, + method: 'get', + params: query + }) +} +//文章管理-查询文章详细信息显示 +export function getContentid(id) { + return request({ + url: `/admin/content/${id}`, + method: 'get', + }) +} +//删除 +export function delContent(ids) { + return request({ + url: `/admin/content/${ids}`, + method: 'delete', + }) +} +//文章管理-新增文章列表 +export function addContent(data) { + return request({ + url: `/admin/content`, + method: 'post', + data:data + }) +} +//文章管理-修改文章 +export function changeContent(data) { + return request({ + url: `/admin/content`, + method: 'put', + data:data + }) +} + + +//文章类别管理-文章类别列表 +export function getContentType(data) { + return request({ + url: `admin/contentType/list`, + method: 'get', + params:data + }) +} +//文章类别管理-获取文章类别详细信息 +export function getContentTypeid(id) { + return request({ + url: `/admin/contentType/${id}`, + method: 'get', + }) +} +//文章类别管理-删除文章类别 +export function delContentType(ids) { + return request({ + url: `/admin/contentType/${ids}`, + method: 'delete', + }) +} +//文章类别管理-新增文章类别 +export function addContentType(data) { + return request({ + url: `/admin/contentType`, + method: 'post', + data:data + }) +} +//文章类别管理-修改文章类别 +export function changeContentType(data) { + return request({ + url: `/admin/contentType`, + method: 'put', + data:data + }) +} +// +export function getOperationalStatistics() { + return request({ + url: `/admin/websiteSetup/getOperationalStatistics`, + method: 'get', + }) +} diff --git a/ruoyi-ui/src/api/skins/ttShopping/api.js b/ruoyi-ui/src/api/skins/ttShopping/api.js new file mode 100644 index 0000000..9f105c3 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttShopping/api.js @@ -0,0 +1,28 @@ +import request from '@/utils/request' + +export function shoppingList(query) { + return request({ + url: '/admin/shopping/list', + method: 'get', + params: query + }) +} +export function batchPutAwayOrSoldOut(query, status) { + return request({ + url: `/admin/shopping/batchPutAwayOrSoldOut/${status}`, + method: 'post', + data: query + }) +} +export function soldOutAll() { + return request({ + url: '/admin/shopping/soldOutAll', + method: 'post' + }) +} +export function putAwayAll() { + return request({ + url: '/admin/shopping/putAwayAll', + method: 'post' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttStatistics/api.js b/ruoyi-ui/src/api/skins/ttStatistics/api.js new file mode 100644 index 0000000..e29acb3 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttStatistics/api.js @@ -0,0 +1,168 @@ +import request from '@/utils/request' + +// 查询大盘统计数据 +export function getStatistics(query) { + return request({ + url: '/admin/statistics/info', + method: 'get', + params: query + }) +} + +// 查询宝箱数据详细 +export function getBox(boxId) { + return request({ + url: '/admin/box/' + boxId, + method: 'get' + }) +} + +// 新增宝箱数据 +export function addBox(data) { + return request({ + url: '/admin/box', + method: 'post', + data: data + }) +} + +// 修改宝箱数据 +export function updateBox(data) { + return request({ + url: '/admin/box', + method: 'put', + data: data + }) +} + +// 删除宝箱数据 +export function delBox(boxId) { + return request({ + url: '/admin/box/' + boxId, + method: 'delete' + }) +} + +//宝箱类型 +export function boxTypelist(data) { + return request({ + url: '/admin/boxType/list', + method: 'get', + params:data + }) +} +//新增宝箱 + +export function boxType(data) { + return request({ + url: '/admin/boxType', + method: 'post', + data:data + }) +} +// +export function boxTypeChange(data) { + return request({ + url: '/admin/boxType', + method: 'put', + data:data + }) +} +// +export function boxTypeId(id) { + return request({ + url: `/admin/boxType/${id}`, + method: 'get', + }) +} +// +export function boxChange(data) { + return request({ + url: `/admin/box`, + method: 'put', + data:data + }) +} +//宝箱类型-删除宝箱类型数据 +export function delBoxType(ids) { + return request({ + url: `/admin/boxType/${ids}`, + method: 'delete', + }) +} +//宝箱-查看奖项 +export function getBoxOrnaments(boxId,page,size) { + return request({ + url: `/admin/boxOrnaments/list/${boxId}?pageNum=${page}&pageSize=${size}`, + method: 'get', + }) +} +//宝箱-修改奖项 +export function saveBoxOrnaments(data) { + return request({ + url: `/admin/boxOrnaments`, + method: 'put', + data:data + }) +} +//宝箱-新增奖项 +export function addBoxOrnaments(data) { + return request({ + url: `/admin/boxOrnaments`, + method: 'post', + data:data + }) +} +//宝箱-获取奖项详细信息 +export function getidBoxOrnaments(id) { + return request({ + url: `/admin/boxOrnaments/${id}`, + method: 'get', + }) +} +//宝箱-获取奖项详细信息 +export function delBoxOrnaments(boxId,ids) { + return request({ + url: `/admin/boxOrnaments/${boxId}/${ids}`, + method: 'delete', + }) +} +//宝箱记录-获取宝箱记录数据列表 +export function getBoxRecords(data) { + return request({ + url: `/admin/boxRecords/list`, + method: 'get', + params:data + }) +} +//获取宝箱利润率 +export function getProfitMargin(boxId) { + return request({ + url: `/admin/boxOrnaments/getProfitMargin/${boxId}`, + method: 'get', + }) +} + +//宝箱饰品-批量填货 + export function batchAdd(data) { + return request({ + url: `/admin/boxOrnaments/batchAdd`, + method: 'post', + data: data + }) +} +//宝箱-重置宝箱 + export function resetBox(boxId) { + return request({ + url: `/admin/box/resetBox/${boxId}`, + method: 'get', + }) +} +// +//宝箱-统计宝箱数据 + export function countBox(boxId,data) { + return request({ + url: `/admin/box/statisticsBoxData/${boxId}?date=${data}`, + method: 'get', + }) +} diff --git a/ruoyi-ui/src/api/skins/ttTaskCenter/api.js b/ruoyi-ui/src/api/skins/ttTaskCenter/api.js new file mode 100644 index 0000000..dfcf086 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttTaskCenter/api.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询任务中心列表 +export function listTaskCenter(query) { + return request({ + url: '/admin/taskCenter/list', + method: 'get', + params: query + }) +} + +// 查询任务中心详细 +export function getTaskCenter(id) { + return request({ + url: '/admin/taskCenter/' + id, + method: 'get' + }) +} + +// 新增任务中心 +export function addTaskCenter(data) { + return request({ + url: '/admin/taskCenter', + method: 'post', + data: data + }) +} + +// 修改任务中心 +export function updateTaskCenter(data) { + return request({ + url: '/admin/taskCenter', + method: 'put', + data: data + }) +} + +// 删除任务中心 +export function delTaskCenter(id) { + return request({ + url: '/admin/taskCenter/' + id, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttUpgrade/api.js b/ruoyi-ui/src/api/skins/ttUpgrade/api.js new file mode 100644 index 0000000..28cb4dd --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttUpgrade/api.js @@ -0,0 +1,93 @@ +import request from '@/utils/request' + +// 查询幸运升级数据列表 +export function getupgradeOrnaments(query) { + return request({ + url: '/admin/upgradeOrnaments/list', + method: 'get', + params: query + }) +} + +// 查询幸运升级数据详细 +export function getUpgradeOrnamentsid(id) { + return request({ + url: '/admin/upgradeOrnaments/'+ id, + method: 'get' + }) +} + +// 新增幸运升级数据 +export function addUpgradeOrnaments(data) { + return request({ + url: `/admin/upgradeOrnaments/batchAdd/${data}`, + method: 'post', + // data: data + }) +} + +// 修改幸运升级数据 +export function updateUpgradeOrnaments(data) { + return request({ + url: '/admin/upgradeOrnaments', + method: 'put', + data: data + }) +} + +// 删除幸运升级数据 +export function delUpgradeOrnaments(ids) { + return request({ + url: `/admin/upgradeOrnaments/${ids}`, + method: 'delete' + }) +} +//失败奖励-删除奖励 +export function delFail(ids) { + return request({ + url: `/admin/upgradeFailOrnaments/${ids}`, + method: 'delete' + }) +} +//失败奖励-获取奖励列表 +export function getFail(data) { + return request({ + url: `/admin/upgradeFailOrnaments/list`, + method: 'post', + data: data + }) +} +//失败 添加 +export function addFail(data) { + return request({ + url: `/admin/upgradeFailOrnaments/batchAdd`, + method: 'post', + data: data + }) +} +//失败 更新 +export function upFail(data) { + return request({ + url: `/admin/upgradeFailOrnaments`, + method: 'put', + data:data + }) +} + +export function getupgradeRecord(data) { + return request({ + url: '/admin/upgradeRecord/list', + method: 'post', + data: data + }) +} + +//统计-获取升级饰品利润统计 +export function getUserProfitStatistics(id) { + return request({ + url:`/admin/upgradeOrnaments/getUpgradeProfitStatistics/${id}`, + method: 'get', + }) +} + + diff --git a/ruoyi-ui/src/api/skins/ttUserBlendErcash/api.js b/ruoyi-ui/src/api/skins/ttUserBlendErcash/api.js new file mode 100644 index 0000000..0177b2d --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttUserBlendErcash/api.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询收支明细 +export function getUserBlendErcash(data) { + return request({ + url: '/admin/blendErcash/list', + method: 'post', + data: data + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/skins/ttUserBlendErcash/archiveApi.js b/ruoyi-ui/src/api/skins/ttUserBlendErcash/archiveApi.js new file mode 100644 index 0000000..d17b784 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttUserBlendErcash/archiveApi.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询收支明细(归档) +export function getUserBlendErcashArchive(data) { + return request({ + url: '/admin/blendErcash/archive/list', + method: 'post', + data: data + }) +} diff --git a/ruoyi-ui/src/api/skins/ttWelfare/api.js b/ruoyi-ui/src/api/skins/ttWelfare/api.js new file mode 100644 index 0000000..2764923 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttWelfare/api.js @@ -0,0 +1,69 @@ +import request from '@/utils/request' + +// 查询福利列表列表 +export function listWelfare(query) { + return request({ + url: '/admin/welfare/list', + method: 'get', + params: query + }) +} + +// 查询福利列表详细 +export function getWelfare(welfareId) { + return request({ + url: '/admin/welfare/' + welfareId, + method: 'get' + }) +} + +// 新增福利列表 +export function addWelfare(data) { + return request({ + url: '/admin/welfare', + method: 'post', + data: data + }) +} + +// 修改福利列表 +export function updateWelfare(data) { + return request({ + url: '/admin/welfare', + method: 'put', + data: data + }) +} + +// 删除福利列表 +export function delWelfare(welfareId) { + return request({ + url: '/admin/welfare/' + welfareId, + method: 'delete' + }) +} + +// 查询福利箱子开箱记录 +export function getWelfareBoxRecords(query) { + return request({ + url: '/admin/welfare/boxRecords', + method: 'get', + params: query + }) +} + +// 删除福利箱子开箱记录(批量) +export function delWelfareBoxRecords(ids) { + return request({ + url: '/admin/welfare/boxRecords/' + ids, + method: 'delete' + }) +} + +// 清空全部福利开箱记录 +export function clearWelfareBoxRecords() { + return request({ + url: '/admin/welfare/boxRecords/clear', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttWelfare/monthlyRecharges.js b/ruoyi-ui/src/api/skins/ttWelfare/monthlyRecharges.js new file mode 100644 index 0000000..ca04b23 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttWelfare/monthlyRecharges.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询每月充值福利列表 +export function listRecharges(query) { + return request({ + url: '/admin/recharges/list', + method: 'get', + params: query + }) +} + +// 查询每月充值福利详细 +export function getRecharges(id) { + return request({ + url: '/admin/recharges/' + id, + method: 'get' + }) +} + +// 新增每月充值福利 +export function addRecharges(data) { + return request({ + url: '/admin/recharges', + method: 'post', + data: data + }) +} + +// 修改每月充值福利 +export function updateRecharges(data) { + return request({ + url: '/admin/recharges', + method: 'put', + data: data + }) +} + +// 删除每月充值福利 +export function delRecharges(id) { + return request({ + url: '/admin/recharges/' + id, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttWelfare/rechargeRankingReward.js b/ruoyi-ui/src/api/skins/ttWelfare/rechargeRankingReward.js new file mode 100644 index 0000000..bf1ec51 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttWelfare/rechargeRankingReward.js @@ -0,0 +1,58 @@ +import request from '@/utils/request' + +// 查询充值奖励金额列表 +export function listReward(query) { + return request({ + url: '/admin/rechargeRankingReward/list', + method: 'get', + params: query + }) +} + +// 查询充值奖励金额详细 +export function getReward(id) { + return request({ + url: '/admin/rechargeRankingReward/' + id, + method: 'get' + }) +} + +// 新增充值奖励金额 +export function addReward(data) { + return request({ + url: '/admin/rechargeRankingReward', + method: 'post', + data: data + }) +} + +export function generateRankingReward() { + return request({ + url: '/admin/rechargeRankingReward/generateRankingReward', + method: 'post' + }) +} + +// 修改充值奖励金额 +export function updateReward(data) { + return request({ + url: '/admin/rechargeRankingReward', + method: 'put', + data: data + }) +} + +// 删除充值奖励金额 +export function delReward(id) { + return request({ + url: '/admin/rechargeRankingReward/' + id, + method: 'delete' + }) +} + +export function truncateRankingReward() { + return request({ + url: '/admin/rechargeRankingReward/truncateRankingReward', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/ttdelivery/api.js b/ruoyi-ui/src/api/skins/ttdelivery/api.js new file mode 100644 index 0000000..a180f1c --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttdelivery/api.js @@ -0,0 +1,76 @@ +import request from '@/utils/request' + +//申请记录 +export function deliverGoodslist(query) { + return request({ + url: '/admin/deliverGoods/getDeliveryApplyList', + method: 'get', + params: query + }) +} +//获取饰品所有在售列表 +export function getAvailableMarketList(data) { + return request({ + url: '/admin/deliverGoods/getAvailableMarketList', + method: 'post', + data: data + }) +} +//同步状态 +export function synchronousStatus(query) { + return request({ + url: `/admin/deliverGoods/synchronousStatus`, + method: 'get', + params:query + }) +} +// 一键同步状态 +export function syncAllStatus() { + return request({ + url: '/admin/deliverGoods/syncAllStatus', + method: 'get', + timeout: 30000 + }) +} +//提货记录 +export function getDeliveryRecordList(query) { + return request({ + url: '/admin/deliverGoods/getDeliveryRecordList', + method: 'get', + params: query + }) +} +//发货 +export function tradeBuy(data) { + return request({ + url: '/admin/deliverGoods/tradeBuy', + method: 'post', + data: data + }) +} +//退回 +export function deliveryFail(query) { + return request({ + url: '/admin/deliverGoods/deliveryFail', + method: 'post', + params: query + }) +} + +// 查询cs2pifa在售信息 +export function queryOnSaleInfo(data) { + return request({ + url: '/admin/cs340item/queryOnSaleInfo', + method: 'post', + data: data + }) +} + +// 批量实时查询三方最低在售价格 +export function batchQueryPrices(hashNames) { + return request({ + url: '/admin/cs340item/batchQueryPrices', + method: 'post', + data: { hashNames: hashNames } + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/skins/ttredPacket/api.js b/ruoyi-ui/src/api/skins/ttredPacket/api.js new file mode 100644 index 0000000..8676ff7 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttredPacket/api.js @@ -0,0 +1,79 @@ + +import request from '@/utils/request' +export function getRedPacket(query) { + return request({ + url: '/admin/redPacket/list', + method: 'get', + params: query + }) +} +export function addRedPacket(query) { + return request({ + url: '/admin/redPacket', + method: 'post', + data: query + }) +} +//删除 +export function delRedPacket(ids) { + return request({ + url: `/admin/redPacket/${ids}`, + method: 'delete', + }) +} +//根据id查询 +export function getRedPacketid(ids) { + return request({ + url: `/admin/redPacket/${ids}`, + method: 'get', + }) +} +//修改 +export function changeRedPacket(query) { + return request({ + url: `/admin/redPacket`, + method: 'put', + data: query + + }) +} +//开启记录 +export function getRedPacketRecordList(query) { + return request({ + url: `/admin/redPacketRecord/getRedPacketRecordList`, + method: 'get', + params: query + + }) +} +//亏损返水结算明细(定时任务每半月写入,后台只读 + 人工发放) +export function getLossRebateList(query) { + return request({ + url: `/admin/lossRebate/list`, + method: 'get', + params: query + }) +} +//人工发放亏损返利CDK(超过单期上限 / 自动发放失败的记录) +export function grantLossRebate(data) { + return request({ + url: `/admin/lossRebate/grant`, + method: 'post', + data: data + }) +} +//读取单期返水上限 +export function getLossRebateConfig() { + return request({ + url: `/admin/lossRebate/config`, + method: 'get' + }) +} +//保存单期返水上限(0=不设上限) +export function saveLossRebateConfig(maxAmount) { + return request({ + url: `/admin/lossRebate/config`, + method: 'post', + params: { maxAmount: maxAmount } + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/skins/ttuser/api.js b/ruoyi-ui/src/api/skins/ttuser/api.js new file mode 100644 index 0000000..85fdad4 --- /dev/null +++ b/ruoyi-ui/src/api/skins/ttuser/api.js @@ -0,0 +1,143 @@ +import request from '@/utils/request' + +// 查询注册用户信息列表 +export function listUser(query) { + return request({ + url: '/admin/user/list', + method: 'get', + params: query + }) +} + +// 查询注册用户信息详细 +export function getUser(userId) { + return request({ + url: '/admin/user/' + userId, + method: 'get' + }) +} + +// 修改注册用户信息 +export function updateUser(data) { + return request({ + url: '/admin/user', + method: 'put', + data: data + }) +} + +// 删除注册用户信息 +export function delUser(userId) { + return request({ + url: '/admin/user/' + userId, + method: 'delete' + }) +} + +export function listUser_Online(query) { + return request({ + url: '/admin/user/online/list', + method: 'get', + params: query + }) +} + +export function generateAccount(data) { + return request({ + url: `/admin/user/generateAccount`, + method: 'post', + data: data + }) +} + +export function generateRobot(query) { + return request({ + url:`/admin/user/generateRobot/${query}`, + method:'post' + }) +} + +export function generateBlankAccount(data) { + return request({ + url: `/admin/user/generateBlankAccount`, + method: 'post', + data: data + }) +} + +export function generateCardMerchant(data) { + return request({ + url: `/admin/user/generateCardMerchant`, + method: 'post', + data: data + }) +} + +//用户账户-余额变动记录 +export function userAmountRecords(data) { + return request({ + url:`/admin/userAmountRecords/list`, + method: 'post', + data: data + }) +} +//用户账户-积分变动记录 +export function userCreditsRecords(data) { + return request({ + url:`/admin/userCreditsRecords/list`, + method: 'get', + params:data + }) +} +//获取用户背包 +export function getPackSack(data) { + return request({ + url:`/admin/user/getPackSack`, + method: 'get', + params:data + }) +} + +// 推广记录-获取推广记录 +export function getPromotionRecord(data) { + return request({ + url:`/admin/promotionRecord/getPromotionRecord`, + method: 'get', + params:data + }) +} +export function statisticsPromotionData(userId) { + return request({ + url:`/admin/promotionRecord/statisticsPromotionData/${userId}`, + method: 'get', + }) +} + +//用户背包-删除用户背包饰品 +export function removeUserPackSackData(ids) { + return request({ + url:`/admin/user/removeUserPackSackData/${ids}`, + method: 'post', + }) +} +//统计-获取用户利润统计 +export function getUserProfitStatistics(userId) { + return request({ + url:`/admin/user/getUserProfitStatistics/${userId}`, + method: 'get', + }) +} +// 重置用户余额和积分 +export function resetUserBalance(userId) { + return request({ + url: `/admin/user/resetBalance/${userId}`, + method: 'post' + }) +} +// 真删除用户(物理删除) +export function forceDeleteUser(userId) { + return request({ + url: `/admin/user/forceDelete/${userId}`, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/skins/userTurnover.js b/ruoyi-ui/src/api/skins/userTurnover.js new file mode 100644 index 0000000..313cc27 --- /dev/null +++ b/ruoyi-ui/src/api/skins/userTurnover.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +export function getUserTurnover(params) { + return request({ + url: '/admin/userAmountRecords/userTurnover', + method: 'get', + params + }) +} diff --git a/ruoyi-ui/src/api/system/config.js b/ruoyi-ui/src/api/system/config.js new file mode 100644 index 0000000..a404d82 --- /dev/null +++ b/ruoyi-ui/src/api/system/config.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询参数列表 +export function listConfig(query) { + return request({ + url: '/system/config/list', + method: 'get', + params: query + }) +} + +// 查询参数详细 +export function getConfig(configId) { + return request({ + url: '/system/config/' + configId, + method: 'get' + }) +} + +// 根据参数键名查询参数值 +export function getConfigKey(configKey) { + return request({ + url: '/system/config/configKey/' + configKey, + method: 'get' + }) +} + +// 新增参数配置 +export function addConfig(data) { + return request({ + url: '/system/config', + method: 'post', + data: data + }) +} + +// 修改参数配置 +export function updateConfig(data) { + return request({ + url: '/system/config', + method: 'put', + data: data + }) +} + +// 删除参数配置 +export function delConfig(configId) { + return request({ + url: '/system/config/' + configId, + method: 'delete' + }) +} + +// 刷新参数缓存 +export function refreshCache() { + return request({ + url: '/system/config/refreshCache', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/system/dept.js b/ruoyi-ui/src/api/system/dept.js new file mode 100644 index 0000000..fc943cd --- /dev/null +++ b/ruoyi-ui/src/api/system/dept.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +// 查询部门列表 +export function listDept(query) { + return request({ + url: '/system/dept/list', + method: 'get', + params: query + }) +} + +// 查询部门列表(排除节点) +export function listDeptExcludeChild(deptId) { + return request({ + url: '/system/dept/list/exclude/' + deptId, + method: 'get' + }) +} + +// 查询部门详细 +export function getDept(deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'get' + }) +} + +// 新增部门 +export function addDept(data) { + return request({ + url: '/system/dept', + method: 'post', + data: data + }) +} + +// 修改部门 +export function updateDept(data) { + return request({ + url: '/system/dept', + method: 'put', + data: data + }) +} + +// 删除部门 +export function delDept(deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/system/dict/data.js b/ruoyi-ui/src/api/system/dict/data.js new file mode 100644 index 0000000..6c9eb79 --- /dev/null +++ b/ruoyi-ui/src/api/system/dict/data.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +// 查询字典数据列表 +export function listData(query) { + return request({ + url: '/system/dict/data/list', + method: 'get', + params: query + }) +} + +// 查询字典数据详细 +export function getData(dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'get' + }) +} + +// 根据字典类型查询字典数据信息 +export function getDicts(dictType) { + return request({ + url: '/system/dict/data/type/' + dictType, + method: 'get' + }) +} + +// 新增字典数据 +export function addData(data) { + return request({ + url: '/system/dict/data', + method: 'post', + data: data + }) +} + +// 修改字典数据 +export function updateData(data) { + return request({ + url: '/system/dict/data', + method: 'put', + data: data + }) +} + +// 删除字典数据 +export function delData(dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/system/dict/type.js b/ruoyi-ui/src/api/system/dict/type.js new file mode 100644 index 0000000..a7a6e01 --- /dev/null +++ b/ruoyi-ui/src/api/system/dict/type.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询字典类型列表 +export function listType(query) { + return request({ + url: '/system/dict/type/list', + method: 'get', + params: query + }) +} + +// 查询字典类型详细 +export function getType(dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'get' + }) +} + +// 新增字典类型 +export function addType(data) { + return request({ + url: '/system/dict/type', + method: 'post', + data: data + }) +} + +// 修改字典类型 +export function updateType(data) { + return request({ + url: '/system/dict/type', + method: 'put', + data: data + }) +} + +// 删除字典类型 +export function delType(dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'delete' + }) +} + +// 刷新字典缓存 +export function refreshCache() { + return request({ + url: '/system/dict/type/refreshCache', + method: 'delete' + }) +} + +// 获取字典选择框列表 +export function optionselect() { + return request({ + url: '/system/dict/type/optionselect', + method: 'get' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/system/menu.js b/ruoyi-ui/src/api/system/menu.js new file mode 100644 index 0000000..f6415c6 --- /dev/null +++ b/ruoyi-ui/src/api/system/menu.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询菜单列表 +export function listMenu(query) { + return request({ + url: '/system/menu/list', + method: 'get', + params: query + }) +} + +// 查询菜单详细 +export function getMenu(menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'get' + }) +} + +// 查询菜单下拉树结构 +export function treeselect() { + return request({ + url: '/system/menu/treeselect', + method: 'get' + }) +} + +// 根据角色ID查询菜单下拉树结构 +export function roleMenuTreeselect(roleId) { + return request({ + url: '/system/menu/roleMenuTreeselect/' + roleId, + method: 'get' + }) +} + +// 新增菜单 +export function addMenu(data) { + return request({ + url: '/system/menu', + method: 'post', + data: data + }) +} + +// 修改菜单 +export function updateMenu(data) { + return request({ + url: '/system/menu', + method: 'put', + data: data + }) +} + +// 删除菜单 +export function delMenu(menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/system/notice.js b/ruoyi-ui/src/api/system/notice.js new file mode 100644 index 0000000..c274ea5 --- /dev/null +++ b/ruoyi-ui/src/api/system/notice.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询公告列表 +export function listNotice(query) { + return request({ + url: '/system/notice/list', + method: 'get', + params: query + }) +} + +// 查询公告详细 +export function getNotice(noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'get' + }) +} + +// 新增公告 +export function addNotice(data) { + return request({ + url: '/system/notice', + method: 'post', + data: data + }) +} + +// 修改公告 +export function updateNotice(data) { + return request({ + url: '/system/notice', + method: 'put', + data: data + }) +} + +// 删除公告 +export function delNotice(noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/system/post.js b/ruoyi-ui/src/api/system/post.js new file mode 100644 index 0000000..1a8e9ca --- /dev/null +++ b/ruoyi-ui/src/api/system/post.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询岗位列表 +export function listPost(query) { + return request({ + url: '/system/post/list', + method: 'get', + params: query + }) +} + +// 查询岗位详细 +export function getPost(postId) { + return request({ + url: '/system/post/' + postId, + method: 'get' + }) +} + +// 新增岗位 +export function addPost(data) { + return request({ + url: '/system/post', + method: 'post', + data: data + }) +} + +// 修改岗位 +export function updatePost(data) { + return request({ + url: '/system/post', + method: 'put', + data: data + }) +} + +// 删除岗位 +export function delPost(postId) { + return request({ + url: '/system/post/' + postId, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/system/role.js b/ruoyi-ui/src/api/system/role.js new file mode 100644 index 0000000..f13e6f4 --- /dev/null +++ b/ruoyi-ui/src/api/system/role.js @@ -0,0 +1,119 @@ +import request from '@/utils/request' + +// 查询角色列表 +export function listRole(query) { + return request({ + url: '/system/role/list', + method: 'get', + params: query + }) +} + +// 查询角色详细 +export function getRole(roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'get' + }) +} + +// 新增角色 +export function addRole(data) { + return request({ + url: '/system/role', + method: 'post', + data: data + }) +} + +// 修改角色 +export function updateRole(data) { + return request({ + url: '/system/role', + method: 'put', + data: data + }) +} + +// 角色数据权限 +export function dataScope(data) { + return request({ + url: '/system/role/dataScope', + method: 'put', + data: data + }) +} + +// 角色状态修改 +export function changeRoleStatus(roleId, status) { + const data = { + roleId, + status + } + return request({ + url: '/system/role/changeStatus', + method: 'put', + data: data + }) +} + +// 删除角色 +export function delRole(roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'delete' + }) +} + +// 查询角色已授权用户列表 +export function allocatedUserList(query) { + return request({ + url: '/system/role/authUser/allocatedList', + method: 'get', + params: query + }) +} + +// 查询角色未授权用户列表 +export function unallocatedUserList(query) { + return request({ + url: '/system/role/authUser/unallocatedList', + method: 'get', + params: query + }) +} + +// 取消用户授权角色 +export function authUserCancel(data) { + return request({ + url: '/system/role/authUser/cancel', + method: 'put', + data: data + }) +} + +// 批量取消用户授权角色 +export function authUserCancelAll(data) { + return request({ + url: '/system/role/authUser/cancelAll', + method: 'put', + params: data + }) +} + +// 授权用户选择 +export function authUserSelectAll(data) { + return request({ + url: '/system/role/authUser/selectAll', + method: 'put', + params: data + }) +} + +// 根据角色ID查询部门树结构 +export function deptTreeSelect(roleId) { + return request({ + url: '/system/role/deptTree/' + roleId, + method: 'get' + }) +} diff --git a/ruoyi-ui/src/api/system/turnoverLottery.js b/ruoyi-ui/src/api/system/turnoverLottery.js new file mode 100644 index 0000000..549346e --- /dev/null +++ b/ruoyi-ui/src/api/system/turnoverLottery.js @@ -0,0 +1,120 @@ +import request from '@/utils/request' + +// 查询抽奖活动列表 +export function listLottery(query) { + return request({ + url: '/admin/turnoverLottery/list', + method: 'get', + params: query + }) +} + +// 保存抽奖奖品配置 +export function savePrizes(lotteryId, data) { + return request({ + url: '/admin/turnoverLottery/save/' + lotteryId, + method: 'post', + data + }) +} + +// 更新抽奖状态 +export function toggleStatus(lotteryId, status) { + return request({ + url: '/admin/turnoverLottery/toggleStatus', + method: 'post', + data: { lotteryId, status } + }) +} + +// 创建抽奖活动 +export function createLottery(data) { + return request({ + url: '/admin/turnoverLottery/create', + method: 'post', + data + }) +} + +// 修改抽奖活动 +export function updateLottery(data) { + return request({ + url: '/admin/turnoverLottery/update', + method: 'post', + data + }) +} + +// 删除抽奖活动 +export function deleteLottery(lotteryId) { + return request({ + url: '/admin/turnoverLottery/delete/' + lotteryId, + method: 'delete' + }) +} + + +// 搜索饰品 +export function searchOrnaments(name, limit = 50) { + return request({ + url: '/admin/turnoverLottery/searchOrnaments', + method: 'get', + params: { name, limit } + }) +} + +// ==================== 奖品管理相关 (仿宝箱奖项) ==================== + +// 获取奖品列表 (分页,含爆率计算) +export function getLotteryPrizes(lotteryId, pageNum, pageSize) { + return request({ + url: '/admin/turnoverLotteryPrize/list/' + lotteryId, + method: 'get', + params: { pageNum, pageSize } + }) +} + +// 获取单个奖品详情 +export function getLotteryPrize(prizeId) { + return request({ + url: '/admin/turnoverLotteryPrize/' + prizeId, + method: 'get' + }) +} + +// 新增奖品 +export function addLotteryPrize(data) { + return request({ + url: '/admin/turnoverLotteryPrize', + method: 'post', + data + }) +} + +// 修改奖品 +export function saveLotteryPrize(data) { + return request({ + url: '/admin/turnoverLotteryPrize', + method: 'put', + data + }) +} + +// 删除奖品 +export function deleteLotteryPrize(lotteryId, ids) { + return request({ + url: '/admin/turnoverLotteryPrize/' + lotteryId + '/' + ids, + method: 'delete' + }) +} + +// ==================== 抽奖记录相关 ==================== + +// 获取抽奖记录列表 +export function listRecords(query) { + return request({ + url: '/admin/turnoverLotteryRecord/list', + method: 'get', + params: query + }) +} diff --git a/ruoyi-ui/src/api/system/user.js b/ruoyi-ui/src/api/system/user.js new file mode 100644 index 0000000..f2f76ef --- /dev/null +++ b/ruoyi-ui/src/api/system/user.js @@ -0,0 +1,135 @@ +import request from '@/utils/request' +import { parseStrEmpty } from "@/utils/ruoyi"; + +// 查询用户列表 +export function listUser(query) { + return request({ + url: '/system/user/list', + method: 'get', + params: query + }) +} + +// 查询用户详细 +export function getUser(userId) { + return request({ + url: '/system/user/' + parseStrEmpty(userId), + method: 'get' + }) +} + +// 新增用户 +export function addUser(data) { + return request({ + url: '/system/user', + method: 'post', + data: data + }) +} + +// 修改用户 +export function updateUser(data) { + return request({ + url: '/system/user', + method: 'put', + data: data + }) +} + +// 删除用户 +export function delUser(userId) { + return request({ + url: '/system/user/' + userId, + method: 'delete' + }) +} + +// 用户密码重置 +export function resetUserPwd(userId, password) { + const data = { + userId, + password + } + return request({ + url: '/system/user/resetPwd', + method: 'put', + data: data + }) +} + +// 用户状态修改 +export function changeUserStatus(userId, status) { + const data = { + userId, + status + } + return request({ + url: '/system/user/changeStatus', + method: 'put', + data: data + }) +} + +// 查询用户个人信息 +export function getUserProfile() { + return request({ + url: '/system/user/profile', + method: 'get' + }) +} + +// 修改用户个人信息 +export function updateUserProfile(data) { + return request({ + url: '/system/user/profile', + method: 'put', + data: data + }) +} + +// 用户密码重置 +export function updateUserPwd(oldPassword, newPassword) { + const data = { + oldPassword, + newPassword + } + return request({ + url: '/system/user/profile/updatePwd', + method: 'put', + params: data + }) +} + +// 用户头像上传 +export function uploadAvatar(data) { + return request({ + url: '/system/user/profile/avatar', + method: 'post', + data: data + }) +} + +// 查询授权角色 +export function getAuthRole(userId) { + return request({ + url: '/system/user/authRole/' + userId, + method: 'get' + }) +} + +// 保存授权角色 +export function updateAuthRole(data) { + return request({ + url: '/system/user/authRole', + method: 'put', + params: data + }) +} + +// 查询部门下拉树结构 +export function deptTreeSelect() { + return request({ + url: '/system/user/deptTree', + method: 'get' + }) +} diff --git a/ruoyi-ui/src/api/tool/gen.js b/ruoyi-ui/src/api/tool/gen.js new file mode 100644 index 0000000..4506927 --- /dev/null +++ b/ruoyi-ui/src/api/tool/gen.js @@ -0,0 +1,76 @@ +import request from '@/utils/request' + +// 查询生成表数据 +export function listTable(query) { + return request({ + url: '/tool/gen/list', + method: 'get', + params: query + }) +} +// 查询db数据库列表 +export function listDbTable(query) { + return request({ + url: '/tool/gen/db/list', + method: 'get', + params: query + }) +} + +// 查询表详细信息 +export function getGenTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'get' + }) +} + +// 修改代码生成信息 +export function updateGenTable(data) { + return request({ + url: '/tool/gen', + method: 'put', + data: data + }) +} + +// 导入表 +export function importTable(data) { + return request({ + url: '/tool/gen/importTable', + method: 'post', + params: data + }) +} + +// 预览生成代码 +export function previewTable(tableId) { + return request({ + url: '/tool/gen/preview/' + tableId, + method: 'get' + }) +} + +// 删除表数据 +export function delTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'delete' + }) +} + +// 生成代码(自定义路径) +export function genCode(tableName) { + return request({ + url: '/tool/gen/genCode/' + tableName, + method: 'get' + }) +} + +// 同步数据库 +export function synchDb(tableName) { + return request({ + url: '/tool/gen/synchDb/' + tableName, + method: 'get' + }) +} diff --git a/ruoyi-ui/src/assets/401_images/401.gif b/ruoyi-ui/src/assets/401_images/401.gif new file mode 100644 index 0000000..cd6e0d9 Binary files /dev/null and b/ruoyi-ui/src/assets/401_images/401.gif differ diff --git a/ruoyi-ui/src/assets/404_images/404.png b/ruoyi-ui/src/assets/404_images/404.png new file mode 100644 index 0000000..3d8e230 Binary files /dev/null and b/ruoyi-ui/src/assets/404_images/404.png differ diff --git a/ruoyi-ui/src/assets/404_images/404_cloud.png b/ruoyi-ui/src/assets/404_images/404_cloud.png new file mode 100644 index 0000000..c6281d0 Binary files /dev/null and b/ruoyi-ui/src/assets/404_images/404_cloud.png differ diff --git a/ruoyi-ui/src/assets/icons/index.js b/ruoyi-ui/src/assets/icons/index.js new file mode 100644 index 0000000..2c6b309 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/index.js @@ -0,0 +1,9 @@ +import Vue from 'vue' +import SvgIcon from '@/components/SvgIcon'// svg component + +// register globally +Vue.component('svg-icon', SvgIcon) + +const req = require.context('./svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys().map(requireContext) +requireAll(req) diff --git a/ruoyi-ui/src/assets/icons/svg/404.svg b/ruoyi-ui/src/assets/icons/svg/404.svg new file mode 100644 index 0000000..6df5019 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/bug.svg b/ruoyi-ui/src/assets/icons/svg/bug.svg new file mode 100644 index 0000000..05a150d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/bug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/build.svg b/ruoyi-ui/src/assets/icons/svg/build.svg new file mode 100644 index 0000000..97c4688 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/build.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/button.svg b/ruoyi-ui/src/assets/icons/svg/button.svg new file mode 100644 index 0000000..904fddc --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/cascader.svg b/ruoyi-ui/src/assets/icons/svg/cascader.svg new file mode 100644 index 0000000..e256024 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/cascader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/chart.svg b/ruoyi-ui/src/assets/icons/svg/chart.svg new file mode 100644 index 0000000..27728fb --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/chart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/checkbox.svg b/ruoyi-ui/src/assets/icons/svg/checkbox.svg new file mode 100644 index 0000000..013fd3a --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/checkbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/clipboard.svg b/ruoyi-ui/src/assets/icons/svg/clipboard.svg new file mode 100644 index 0000000..90923ff --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/clipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/code.svg b/ruoyi-ui/src/assets/icons/svg/code.svg new file mode 100644 index 0000000..5f9c5ab --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/color.svg b/ruoyi-ui/src/assets/icons/svg/color.svg new file mode 100644 index 0000000..44a81aa --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/component.svg b/ruoyi-ui/src/assets/icons/svg/component.svg new file mode 100644 index 0000000..29c3458 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/component.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/dashboard.svg b/ruoyi-ui/src/assets/icons/svg/dashboard.svg new file mode 100644 index 0000000..5317d37 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/date-range.svg b/ruoyi-ui/src/assets/icons/svg/date-range.svg new file mode 100644 index 0000000..fda571e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/date-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/date.svg b/ruoyi-ui/src/assets/icons/svg/date.svg new file mode 100644 index 0000000..52dc73e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/date.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/dict.svg b/ruoyi-ui/src/assets/icons/svg/dict.svg new file mode 100644 index 0000000..4849377 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/dict.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/documentation.svg b/ruoyi-ui/src/assets/icons/svg/documentation.svg new file mode 100644 index 0000000..7043122 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/documentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/download.svg b/ruoyi-ui/src/assets/icons/svg/download.svg new file mode 100644 index 0000000..c896951 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/drag.svg b/ruoyi-ui/src/assets/icons/svg/drag.svg new file mode 100644 index 0000000..4185d3c --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/druid.svg b/ruoyi-ui/src/assets/icons/svg/druid.svg new file mode 100644 index 0000000..a2b4b4e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/druid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/edit.svg b/ruoyi-ui/src/assets/icons/svg/edit.svg new file mode 100644 index 0000000..d26101f --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/education.svg b/ruoyi-ui/src/assets/icons/svg/education.svg new file mode 100644 index 0000000..7bfb01d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/email.svg b/ruoyi-ui/src/assets/icons/svg/email.svg new file mode 100644 index 0000000..74d25e2 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/example.svg b/ruoyi-ui/src/assets/icons/svg/example.svg new file mode 100644 index 0000000..46f42b5 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/excel.svg b/ruoyi-ui/src/assets/icons/svg/excel.svg new file mode 100644 index 0000000..74d97b8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/excel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/exit-fullscreen.svg b/ruoyi-ui/src/assets/icons/svg/exit-fullscreen.svg new file mode 100644 index 0000000..485c128 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/exit-fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/eye-open.svg b/ruoyi-ui/src/assets/icons/svg/eye-open.svg new file mode 100644 index 0000000..88dcc98 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/eye-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/eye.svg b/ruoyi-ui/src/assets/icons/svg/eye.svg new file mode 100644 index 0000000..16ed2d8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/form.svg b/ruoyi-ui/src/assets/icons/svg/form.svg new file mode 100644 index 0000000..dcbaa18 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/form.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/fullscreen.svg b/ruoyi-ui/src/assets/icons/svg/fullscreen.svg new file mode 100644 index 0000000..0e86b6f --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/github.svg b/ruoyi-ui/src/assets/icons/svg/github.svg new file mode 100644 index 0000000..db0a0d4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/guide.svg b/ruoyi-ui/src/assets/icons/svg/guide.svg new file mode 100644 index 0000000..b271001 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/guide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/icon.svg b/ruoyi-ui/src/assets/icons/svg/icon.svg new file mode 100644 index 0000000..82be8ee --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/input.svg b/ruoyi-ui/src/assets/icons/svg/input.svg new file mode 100644 index 0000000..ab91381 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/input.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/international.svg b/ruoyi-ui/src/assets/icons/svg/international.svg new file mode 100644 index 0000000..e9b56ee --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/international.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/job.svg b/ruoyi-ui/src/assets/icons/svg/job.svg new file mode 100644 index 0000000..2a93a25 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/job.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/language.svg b/ruoyi-ui/src/assets/icons/svg/language.svg new file mode 100644 index 0000000..0082b57 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/link.svg b/ruoyi-ui/src/assets/icons/svg/link.svg new file mode 100644 index 0000000..48197ba --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/list.svg b/ruoyi-ui/src/assets/icons/svg/list.svg new file mode 100644 index 0000000..20259ed --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/lock.svg b/ruoyi-ui/src/assets/icons/svg/lock.svg new file mode 100644 index 0000000..74fee54 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/log.svg b/ruoyi-ui/src/assets/icons/svg/log.svg new file mode 100644 index 0000000..d879d33 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/logininfor.svg b/ruoyi-ui/src/assets/icons/svg/logininfor.svg new file mode 100644 index 0000000..267f844 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/logininfor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/message.svg b/ruoyi-ui/src/assets/icons/svg/message.svg new file mode 100644 index 0000000..14ca817 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/money.svg b/ruoyi-ui/src/assets/icons/svg/money.svg new file mode 100644 index 0000000..c1580de --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/monitor.svg b/ruoyi-ui/src/assets/icons/svg/monitor.svg new file mode 100644 index 0000000..bc308cb --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/monitor.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/nested.svg b/ruoyi-ui/src/assets/icons/svg/nested.svg new file mode 100644 index 0000000..06713a8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/nested.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/number.svg b/ruoyi-ui/src/assets/icons/svg/number.svg new file mode 100644 index 0000000..ad5ce9a --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/number.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/online.svg b/ruoyi-ui/src/assets/icons/svg/online.svg new file mode 100644 index 0000000..330a202 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/online.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/password.svg b/ruoyi-ui/src/assets/icons/svg/password.svg new file mode 100644 index 0000000..6c64def --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/pdf.svg b/ruoyi-ui/src/assets/icons/svg/pdf.svg new file mode 100644 index 0000000..957aa0c --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/people.svg b/ruoyi-ui/src/assets/icons/svg/people.svg new file mode 100644 index 0000000..2bd54ae --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/people.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/peoples.svg b/ruoyi-ui/src/assets/icons/svg/peoples.svg new file mode 100644 index 0000000..aab852e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/phone.svg b/ruoyi-ui/src/assets/icons/svg/phone.svg new file mode 100644 index 0000000..ab8e8c4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/post.svg b/ruoyi-ui/src/assets/icons/svg/post.svg new file mode 100644 index 0000000..2922c61 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/post.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/qq.svg b/ruoyi-ui/src/assets/icons/svg/qq.svg new file mode 100644 index 0000000..ee13d4e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/qq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/question.svg b/ruoyi-ui/src/assets/icons/svg/question.svg new file mode 100644 index 0000000..cf75bd4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/question.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/radio.svg b/ruoyi-ui/src/assets/icons/svg/radio.svg new file mode 100644 index 0000000..0cde345 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/radio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/rate.svg b/ruoyi-ui/src/assets/icons/svg/rate.svg new file mode 100644 index 0000000..aa3b14d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/rate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/redis-list.svg b/ruoyi-ui/src/assets/icons/svg/redis-list.svg new file mode 100644 index 0000000..98a15b2 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/redis-list.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/redis.svg b/ruoyi-ui/src/assets/icons/svg/redis.svg new file mode 100644 index 0000000..2f1d62d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/row.svg b/ruoyi-ui/src/assets/icons/svg/row.svg new file mode 100644 index 0000000..0780992 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/row.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/search.svg b/ruoyi-ui/src/assets/icons/svg/search.svg new file mode 100644 index 0000000..84233dd --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/select.svg b/ruoyi-ui/src/assets/icons/svg/select.svg new file mode 100644 index 0000000..d628382 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/server.svg b/ruoyi-ui/src/assets/icons/svg/server.svg new file mode 100644 index 0000000..eb287e3 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/shopping.svg b/ruoyi-ui/src/assets/icons/svg/shopping.svg new file mode 100644 index 0000000..87513e7 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/shopping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/size.svg b/ruoyi-ui/src/assets/icons/svg/size.svg new file mode 100644 index 0000000..ddb25b8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/skill.svg b/ruoyi-ui/src/assets/icons/svg/skill.svg new file mode 100644 index 0000000..a3b7312 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/skill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/slider.svg b/ruoyi-ui/src/assets/icons/svg/slider.svg new file mode 100644 index 0000000..fbe4f39 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/slider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/star.svg b/ruoyi-ui/src/assets/icons/svg/star.svg new file mode 100644 index 0000000..6cf86e6 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/swagger.svg b/ruoyi-ui/src/assets/icons/svg/swagger.svg new file mode 100644 index 0000000..05d4e7b --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/swagger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/switch.svg b/ruoyi-ui/src/assets/icons/svg/switch.svg new file mode 100644 index 0000000..0ba61e3 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/switch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/system.svg b/ruoyi-ui/src/assets/icons/svg/system.svg new file mode 100644 index 0000000..5992593 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/system.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/tab.svg b/ruoyi-ui/src/assets/icons/svg/tab.svg new file mode 100644 index 0000000..b4b48e4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/table.svg b/ruoyi-ui/src/assets/icons/svg/table.svg new file mode 100644 index 0000000..0e3dc9d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/textarea.svg b/ruoyi-ui/src/assets/icons/svg/textarea.svg new file mode 100644 index 0000000..2709f29 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/textarea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/theme.svg b/ruoyi-ui/src/assets/icons/svg/theme.svg new file mode 100644 index 0000000..5982a2f --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/theme.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/time-range.svg b/ruoyi-ui/src/assets/icons/svg/time-range.svg new file mode 100644 index 0000000..13c1202 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/time-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/time.svg b/ruoyi-ui/src/assets/icons/svg/time.svg new file mode 100644 index 0000000..b376e32 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/tool.svg b/ruoyi-ui/src/assets/icons/svg/tool.svg new file mode 100644 index 0000000..48e0e35 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/tree-table.svg b/ruoyi-ui/src/assets/icons/svg/tree-table.svg new file mode 100644 index 0000000..8aafdb8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/tree-table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/tree.svg b/ruoyi-ui/src/assets/icons/svg/tree.svg new file mode 100644 index 0000000..dd4b7dd --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/upload.svg b/ruoyi-ui/src/assets/icons/svg/upload.svg new file mode 100644 index 0000000..bae49c0 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/user.svg b/ruoyi-ui/src/assets/icons/svg/user.svg new file mode 100644 index 0000000..0ba0716 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/validCode.svg b/ruoyi-ui/src/assets/icons/svg/validCode.svg new file mode 100644 index 0000000..cfb1021 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/validCode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/wechat.svg b/ruoyi-ui/src/assets/icons/svg/wechat.svg new file mode 100644 index 0000000..c586e55 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/wechat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/zip.svg b/ruoyi-ui/src/assets/icons/svg/zip.svg new file mode 100644 index 0000000..f806fc4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/zip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svgo.yml b/ruoyi-ui/src/assets/icons/svgo.yml new file mode 100644 index 0000000..d11906a --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svgo.yml @@ -0,0 +1,22 @@ +# replace default config + +# multipass: true +# full: true + +plugins: + + # - name + # + # or: + # - name: false + # - name: true + # + # or: + # - name: + # param1: 1 + # param2: 2 + +- removeAttrs: + attrs: + - 'fill' + - 'fill-rule' diff --git a/ruoyi-ui/src/assets/images/dark.svg b/ruoyi-ui/src/assets/images/dark.svg new file mode 100644 index 0000000..f646bd7 --- /dev/null +++ b/ruoyi-ui/src/assets/images/dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/images/light.svg b/ruoyi-ui/src/assets/images/light.svg new file mode 100644 index 0000000..ab7cc08 --- /dev/null +++ b/ruoyi-ui/src/assets/images/light.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/images/login-background.jpg b/ruoyi-ui/src/assets/images/login-background.jpg new file mode 100644 index 0000000..bcd52e3 Binary files /dev/null and b/ruoyi-ui/src/assets/images/login-background.jpg differ diff --git a/ruoyi-ui/src/assets/images/pay.png b/ruoyi-ui/src/assets/images/pay.png new file mode 100644 index 0000000..bb8b967 Binary files /dev/null and b/ruoyi-ui/src/assets/images/pay.png differ diff --git a/ruoyi-ui/src/assets/images/profile.jpg b/ruoyi-ui/src/assets/images/profile.jpg new file mode 100644 index 0000000..b3a940b Binary files /dev/null and b/ruoyi-ui/src/assets/images/profile.jpg differ diff --git a/ruoyi-ui/src/assets/logo/logo.png b/ruoyi-ui/src/assets/logo/logo.png new file mode 100644 index 0000000..3a40ab1 Binary files /dev/null and b/ruoyi-ui/src/assets/logo/logo.png differ diff --git a/ruoyi-ui/src/assets/styles/btn.scss b/ruoyi-ui/src/assets/styles/btn.scss new file mode 100644 index 0000000..e6ba1a8 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/btn.scss @@ -0,0 +1,99 @@ +@import './variables.scss'; + +@mixin colorBtn($color) { + background: $color; + + &:hover { + color: $color; + + &:before, + &:after { + background: $color; + } + } +} + +.blue-btn { + @include colorBtn($blue) +} + +.light-blue-btn { + @include colorBtn($light-blue) +} + +.red-btn { + @include colorBtn($red) +} + +.pink-btn { + @include colorBtn($pink) +} + +.green-btn { + @include colorBtn($green) +} + +.tiffany-btn { + @include colorBtn($tiffany) +} + +.yellow-btn { + @include colorBtn($yellow) +} + +.pan-btn { + font-size: 14px; + color: #fff; + padding: 14px 36px; + border-radius: 8px; + border: none; + outline: none; + transition: 600ms ease all; + position: relative; + display: inline-block; + + &:hover { + background: #fff; + + &:before, + &:after { + width: 100%; + transition: 600ms ease all; + } + } + + &:before, + &:after { + content: ''; + position: absolute; + top: 0; + right: 0; + height: 2px; + width: 0; + transition: 400ms ease all; + } + + &::after { + right: inherit; + top: inherit; + left: 0; + bottom: 0; + } +} + +.custom-button { + display: inline-block; + line-height: 1; + white-space: nowrap; + cursor: pointer; + background: #fff; + color: #fff; + -webkit-appearance: none; + text-align: center; + box-sizing: border-box; + outline: 0; + margin: 0; + padding: 10px 15px; + font-size: 14px; + border-radius: 4px; +} diff --git a/ruoyi-ui/src/assets/styles/element-ui.scss b/ruoyi-ui/src/assets/styles/element-ui.scss new file mode 100644 index 0000000..d25704d --- /dev/null +++ b/ruoyi-ui/src/assets/styles/element-ui.scss @@ -0,0 +1,132 @@ +// cover some element-ui styles + +.el-breadcrumb__inner, +.el-breadcrumb__inner a { + font-weight: 400 !important; +} + +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + +.cell { + .el-tag { + margin-right: 0px; + } +} + +.small-padding { + .cell { + padding-left: 5px; + padding-right: 5px; + } +} + +.fixed-width { + .el-button--mini { + padding: 7px 10px; + width: 60px; + } +} + +.status-col { + .cell { + padding: 0 10px; + text-align: center; + + .el-tag { + margin-right: 0px; + } + } +} + +// to fixed https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; + border-radius: 12px; + overflow: hidden; +} + +// 优化遮罩层 +.v-modal { + background: rgba(0, 0, 0, 0.4) !important; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +// 对话框包装器 +.el-dialog__wrapper { + display: flex; + align-items: center; + justify-content: center; +} + +// 对话框头部 +.el-dialog__header { + padding: 20px 24px 16px; + border-bottom: 1px solid rgba(115, 133, 166, 0.12); +} + +.el-dialog__title { + font-size: 18px; + font-weight: 600; + color: #263654; +} + +// 对话框内容 +.el-dialog__body { + padding: 24px; + color: #5a6478; +} + +// 对话框底部 +.el-dialog__footer { + padding: 16px 24px; + border-top: 1px solid rgba(115, 133, 166, 0.12); +} + +// refine element ui upload +.upload-container { + .el-upload { + width: 100%; + + .el-upload-dragger { + width: 100%; + height: 200px; + } + } +} + +// dropdown +.el-dropdown-menu { + a { + display: block + } +} + +// fix date-picker ui bug in filter-item +.el-range-editor.el-input__inner { + display: inline-flex !important; +} + +// to fix el-date-picker css style +.el-range-separator { + box-sizing: content-box; +} + +.el-menu--collapse + > div + > .el-submenu + > .el-submenu__title + .el-submenu__icon-arrow { + display: none; +} \ No newline at end of file diff --git a/ruoyi-ui/src/assets/styles/element-variables.scss b/ruoyi-ui/src/assets/styles/element-variables.scss new file mode 100644 index 0000000..1615ff2 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/element-variables.scss @@ -0,0 +1,31 @@ +/** +* I think element-ui's default theme color is too light for long-term use. +* So I modified the default color and you can modify it to your liking. +**/ + +/* theme color */ +$--color-primary: #1890ff; +$--color-success: #13ce66; +$--color-warning: #ffba00; +$--color-danger: #ff4949; +// $--color-info: #1E1E1E; + +$--button-font-weight: 400; + +// $--color-text-regular: #1f2d3d; + +$--border-color-light: #dfe4ed; +$--border-color-lighter: #e6ebf5; + +$--table-border: 1px solid #dfe6ec; + +/* icon font path, required */ +$--font-path: '~element-ui/lib/theme-chalk/fonts'; + +@import "~element-ui/packages/theme-chalk/src/index"; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + theme: $--color-primary; +} diff --git a/ruoyi-ui/src/assets/styles/index.scss b/ruoyi-ui/src/assets/styles/index.scss new file mode 100644 index 0000000..b520173 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/index.scss @@ -0,0 +1,255 @@ +@import './variables.scss'; +@import './mixin.scss'; +@import './transition.scss'; +@import './element-ui.scss'; +@import './sidebar.scss'; +@import './btn.scss'; + +body { + height: 100%; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + background: #f3f7ff; +} + +label { + font-weight: 600; +} + +html { + height: 100%; + box-sizing: border-box; + background: #f3f7ff; +} + +#app { + height: 100%; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +.no-padding { + padding: 0px !important; +} + +.padding-content { + padding: 4px 0; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +div:focus { + outline: none; +} + +.fr { + float: right; +} + +.fl { + float: left; +} + +.pr-5 { + padding-right: 5px; +} + +.pl-5 { + padding-left: 5px; +} + +.block { + display: block; +} + +.pointer { + cursor: pointer; +} + +.inlineBlock { + display: block; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +aside { + background: #eef1f6; + padding: 8px 24px; + margin-bottom: 20px; + border-radius: 2px; + display: block; + line-height: 32px; + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + color: #2c3e50; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + a { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } + } +} + +// 整页卡片由 layout 的 .page-card 统一提供,页面容器只负责内部留白 +// 注意:这两张卡片都不能加 backdrop-filter / filter / transform, +// 它们会创建层叠上下文,把 position:fixed 的 el-dialog 困在卡片里, +// 而 Element UI 的 .v-modal 遮罩挂在 body 上(z-index:2000) → 弹窗被遮罩盖住(整页变黑、点一下才恢复) +.app-container { + padding: 24px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(255, 255, 255, 0.9); + border-radius: 16px; + box-shadow: 0 8px 24px rgba(72, 105, 166, 0.09), inset 0 1px 0 rgba(255, 255, 255, 0.95); + min-height: 500px; +} + +// 顶栏 + 标签页 + 内容 在同一张卡片里(与设计图一致) +// 上边距尽量收,把高度让给下面的表格和分页 +.page-card { + position: relative; + margin: 6px 10px 6px 0; + padding: 10px 24px 24px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(255, 255, 255, 0.9); + border-radius: 16px; + box-shadow: 0 8px 24px rgba(72, 105, 166, 0.09), inset 0 1px 0 rgba(255, 255, 255, 0.95); + min-height: calc(100vh - 150px); +} + +/* 只有「固定顶栏」时顶栏才脱离文档流,卡片才需要给它让位; + fixedHeader=false(本项目默认)时顶栏自己占位置,再加边距就会多出一条空白 */ +.fixed-header + .page-card { + margin-top: 76px; +} + +.hasTagsView .fixed-header + .page-card { + margin-top: 126px; +} + +// 页面标题通用样式 +.page-header { + display: flex; + align-items: baseline; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(115, 133, 166, 0.12); +} + +.page-title { + margin: 0; + font-size: 24px; + font-weight: 700; + color: #263654; + letter-spacing: 0.3px; +} + +.page-subtitle { + margin-left: 12px; + font-size: 16px; + font-weight: 500; + color: #7385a6; + + &::before { + content: '·'; + margin-right: 8px; + color: #b0bace; + } +} + +.page-description { + margin-top: 8px; + font-size: 14px; + color: #7385a6; + line-height: 1.6; +} + +.components-container { + margin: 30px 50px; + position: relative; +} + +.pagination-container { + margin-top: 30px; +} + +.text-center { + text-align: center +} + +.sub-navbar { + height: 50px; + line-height: 50px; + position: relative; + width: 100%; + text-align: right; + padding-right: 20px; + transition: 600ms ease position; + background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); + + .subtitle { + font-size: 20px; + color: #fff; + } + + &.draft { + background: #d0d0d0; + } + + &.deleted { + background: #d0d0d0; + } +} + +.link-type, +.link-type:focus { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } +} + +.filter-container { + padding-bottom: 10px; + + .filter-item { + display: inline-block; + vertical-align: middle; + margin-bottom: 10px; + } +} + +// 皮肤层放最后,保证覆盖 Element UI 默认样式 +@import './skin.scss'; diff --git a/ruoyi-ui/src/assets/styles/mixin.scss b/ruoyi-ui/src/assets/styles/mixin.scss new file mode 100644 index 0000000..06fa061 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/mixin.scss @@ -0,0 +1,66 @@ +@mixin clearfix { + &:after { + content: ""; + display: table; + clear: both; + } +} + +@mixin scrollBar { + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } +} + +@mixin relative { + position: relative; + width: 100%; + height: 100%; +} + +@mixin pct($pct) { + width: #{$pct}; + position: relative; + margin: 0 auto; +} + +@mixin triangle($width, $height, $color, $direction) { + $width: $width/2; + $color-border-style: $height solid $color; + $transparent-border-style: $width solid transparent; + height: 0; + width: 0; + + @if $direction==up { + border-bottom: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==right { + border-left: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } + + @else if $direction==down { + border-top: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==left { + border-right: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } +} diff --git a/ruoyi-ui/src/assets/styles/ruoyi.scss b/ruoyi-ui/src/assets/styles/ruoyi.scss new file mode 100644 index 0000000..db8c29b --- /dev/null +++ b/ruoyi-ui/src/assets/styles/ruoyi.scss @@ -0,0 +1,277 @@ + /** + * 通用css样式布局处理 + * Copyright (c) 2019 ruoyi + */ + + /** 基础通用 **/ +.pt5 { + padding-top: 5px; +} +.pr5 { + padding-right: 5px; +} +.pb5 { + padding-bottom: 5px; +} +.mt5 { + margin-top: 5px; +} +.mr5 { + margin-right: 5px; +} +.mb5 { + margin-bottom: 5px; +} +.mb8 { + margin-bottom: 8px; +} +.ml5 { + margin-left: 5px; +} +.mt10 { + margin-top: 10px; +} +.mr10 { + margin-right: 10px; +} +.mb10 { + margin-bottom: 10px; +} +.ml10 { + margin-left: 10px; +} +.mt20 { + margin-top: 20px; +} +.mr20 { + margin-right: 20px; +} +.mb20 { + margin-bottom: 20px; +} +.ml20 { + margin-left: 20px; +} + +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +.el-message-box__status + .el-message-box__message{ + word-break: break-word; +} + +.el-dialog:not(.is-fullscreen) { + margin-top: 6vh !important; +} + +.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body { + overflow: auto; + overflow-x: hidden; + max-height: 70vh; + padding: 10px 20px 0; +} + +.el-table { + .el-table__header-wrapper, .el-table__fixed-header-wrapper { + th { + word-break: break-word; + background-color: #f8f8f9; + color: #515a6e; + height: 40px; + font-size: 13px; + } + } + .el-table__body-wrapper { + .el-button [class*="el-icon-"] + span { + margin-left: 1px; + } + } +} + +/** 表单布局 **/ +.form-header { + font-size:15px; + color:#6379bb; + border-bottom:1px solid #ddd; + margin:8px 10px 25px 10px; + padding-bottom:5px +} + +/** 表格布局 **/ +.pagination-container { + position: relative; + height: 25px; + margin-bottom: 10px; + margin-top: 15px; + padding: 10px 20px !important; +} + +/* tree border */ +.tree-border { + margin-top: 5px; + border: 1px solid #e5e6e7; + background: #FFFFFF none; + border-radius:4px; +} + +.pagination-container .el-pagination { + right: 0; + position: absolute; +} + +@media ( max-width : 768px) { + .pagination-container .el-pagination > .el-pagination__jump { + display: none !important; + } + .pagination-container .el-pagination > .el-pagination__sizes { + display: none !important; + } +} + +.el-table .fixed-width .el-button--mini { + padding-left: 0; + padding-right: 0; + width: inherit; +} + +/** 表格更多操作下拉样式 */ +.el-table .el-dropdown-link,.el-table .el-dropdown-selfdefine { + cursor: pointer; + margin-left: 5px; +} + +.el-table .el-dropdown, .el-icon-arrow-down { + font-size: 12px; +} + +.el-tree-node__content > .el-checkbox { + margin-right: 8px; +} + +.list-group-striped > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; +} + +.list-group { + padding-left: 0px; + list-style: none; +} + +.list-group-item { + border-bottom: 1px solid #e7eaec; + border-top: 1px solid #e7eaec; + margin-bottom: -1px; + padding: 11px 0px; + font-size: 13px; +} + +.pull-right { + float: right !important; +} + +.el-card__header { + padding: 14px 15px 7px; + min-height: 40px; +} + +.el-card__body { + padding: 15px 20px 20px 20px; +} + +.card-box { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 10px; +} + +/* button color */ +.el-button--cyan.is-active, +.el-button--cyan:active { + background: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +.el-button--cyan:focus, +.el-button--cyan:hover { + background: #48D1CC; + border-color: #48D1CC; + color: #FFFFFF; +} + +.el-button--cyan { + background-color: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +/* text color */ +.text-navy { + color: #1ab394; +} + +.text-primary { + color: inherit; +} + +.text-success { + color: #1c84c6; +} + +.text-info { + color: #23c6c8; +} + +.text-warning { + color: #f8ac59; +} + +.text-danger { + color: #ed5565; +} + +.text-muted { + color: #888888; +} + +/* image */ +.img-circle { + border-radius: 50%; +} + +.img-lg { + width: 120px; + height: 120px; +} + +.avatar-upload-preview { + position: relative; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 200px; + height: 200px; + border-radius: 50%; + box-shadow: 0 0 4px #ccc; + overflow: hidden; +} + +/* 拖拽列样式 */ +.sortable-ghost{ + opacity: .8; + color: #fff!important; + background: #42b983!important; +} + +.top-right-btn { + position: relative; + float: right; +} diff --git a/ruoyi-ui/src/assets/styles/sidebar.scss b/ruoyi-ui/src/assets/styles/sidebar.scss new file mode 100644 index 0000000..35d671b --- /dev/null +++ b/ruoyi-ui/src/assets/styles/sidebar.scss @@ -0,0 +1,539 @@ +/* ========================================================================== + 悬浮卡片侧栏(主题核心文件) + - 侧栏容器完全透明:页面上只有一张张独立卡片 + - 一级菜单:48px 毛玻璃卡片 + 28px 彩色图标块 + - 二/三级:原地向下展开,用竖线 + 圆点区分层级 + - 收起态:54px 窄栏只留图标,悬停向右弹出子菜单 + - 移动端:整栏抽屉 + ========================================================================== */ + +$sidebar-width: $base-sidebar-width; +$menu-h: 48px; +$submenu-h: 36px; +$card-radius: 13px; +$brand: #2f6bff; +$text-main: #263654; +$text-sub: #66738b; +$text-muted: #98a3b7; +$card-bg: rgba(255, 255, 255, .84); +$card-border: rgba(255, 255, 255, .88); +$card-shadow: 0 7px 20px rgba(72, 105, 166, .11), inset 0 1px 0 rgba(255, 255, 255, .92); + +/* 一级菜单图标色板:8 色循环(浅色→深色),避免整栏一个蓝 */ +$icon-palette: + (#4e9bff, #2f6bff), // 1 蓝 + (#ffb648, #f97316), // 2 橙 + (#4ade80, #16a34a), // 3 绿 + (#fb7185, #ef4444), // 4 红 + (#a78bfa, #7c3aed), // 5 紫 + (#38bdf8, #0ea5e9), // 6 青 + (#f472b6, #db2777), // 7 粉 + (#fcd34d, #f59e0b); // 8 黄 + +html, +body { + background: #f3f7ff; +} + +#app { + min-height: 100%; + background: + radial-gradient(760px 520px at 4% 100%, rgba(78, 155, 255, .18), transparent 62%), + radial-gradient(760px 520px at 96% 0%, rgba(124, 92, 252, .12), transparent 62%), + linear-gradient(155deg, #f5f8ff 0%, #fbfdff 46%, #edf4ff 100%); + background-attachment: fixed; + + .main-container { + min-height: 100%; + height: 100%; + margin-left: $sidebar-width; + position: relative; + transition: margin-left .28s; + } + + .sidebarHide { + margin-left: 0 !important; + } + + /* ----------------------------------------------------------------------- + 侧栏外壳:透明,不画任何底色 + ----------------------------------------------------------------------- */ + .sidebar-container { + width: $sidebar-width !important; + height: 100%; + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 1001; + overflow: hidden; + font-size: 0; + transition: width .28s; + + background: transparent !important; + border: 0 !important; + box-shadow: none !important; + + .horizontal-collapse-transition { + transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; + } + + .el-scrollbar { + height: 100%; + background: transparent !important; + } + + /* Logo 卡:10 + 64 + 6 = 80px */ + &.has-logo .el-scrollbar { + height: calc(100% - 80px); + } + + .scrollbar-wrapper { + overflow-x: hidden !important; + padding-bottom: 14px; + } + + .el-scrollbar__bar.is-vertical { + right: 2px; + } + + .el-scrollbar__thumb { + background: rgba(123, 144, 181, .28); + } + + .is-horizontal { + display: none; + } + + /* 只约束菜单里的链接;Logo 卡的链接要靠自己的 flex 布局,不能被这里压成 block */ + .el-menu a { + display: block; + width: 100%; + overflow: visible; + } + + .el-menu { + width: 100% !important; + height: 100%; + padding: 4px 0 16px; + border: 0 !important; + background: transparent !important; + } + + /* 文字一律不换行 */ + .el-menu-item, + .el-submenu__title { + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + /* --------------------------------------------------------------------- + 一级菜单:每张卡片完全一致 + DOM 是 ul > div > a.app-link > li.el-menu-item,中间多一层 + --------------------------------------------------------------------- */ + .el-menu:not(.el-menu--inline) > div > a > .el-menu-item, + .el-menu:not(.el-menu--inline) > div > .el-submenu > .el-submenu__title { + display: flex; + align-items: center; + width: calc(100% - 20px); + min-width: 0 !important; + height: $menu-h; + line-height: $menu-h; + margin: 6px 10px !important; + padding: 0 14px !important; + box-sizing: border-box; + + border: 1px solid $card-border; + border-radius: $card-radius; + background: $card-bg !important; + -webkit-backdrop-filter: blur(14px) saturate(155%); + backdrop-filter: blur(14px) saturate(155%); + box-shadow: $card-shadow; + + color: $text-main !important; + font-size: 14px; + font-weight: 600; + + transition: + border-color .18s ease, + background .18s ease, + box-shadow .18s ease, + transform .18s ease; + } + + .el-menu:not(.el-menu--inline) > div > a > .el-menu-item:hover, + .el-menu:not(.el-menu--inline) > div > .el-submenu > .el-submenu__title:hover { + background: rgba(255, 255, 255, .95) !important; + border-color: rgba(150, 179, 235, .38); + box-shadow: 0 9px 24px rgba(57, 94, 159, .14), inset 0 1px 0 #fff; + transform: translateY(-1px); + } + + /* 展开中的一级菜单 */ + .el-menu:not(.el-menu--inline) > div > .el-submenu.is-opened > .el-submenu__title, + .el-menu:not(.el-menu--inline) > div > .el-submenu.is-active > .el-submenu__title { + color: #173f94 !important; + border-color: rgba(67, 126, 255, .28); + background: linear-gradient(135deg, rgba(255, 255, 255, .96), rgba(235, 242, 255, .91)) !important; + box-shadow: 0 8px 22px rgba(47, 107, 255, .13), inset 3px 0 0 $brand; + } + + /* 一级叶子菜单选中态 */ + .el-menu:not(.el-menu--inline) > div > a > .el-menu-item.is-active { + color: $brand !important; + border-color: rgba(67, 126, 255, .30); + background: linear-gradient(135deg, rgba(255, 255, 255, .98), rgba(231, 239, 255, .92)) !important; + box-shadow: 0 8px 22px rgba(47, 107, 255, .14), inset 3px 0 0 $brand; + } + + /* 展开箭头 */ + .el-menu:not(.el-menu--inline) > div > .el-submenu > .el-submenu__title .el-submenu__icon-arrow { + right: 14px; + margin-top: -5px; + color: #7385a6; + font-size: 12px; + font-weight: 700; + } + + /* --------------------------------------------------------------------- + 一级彩色图标块:28px 圆角方块,图标在块内居中 + --------------------------------------------------------------------- */ + .el-menu:not(.el-menu--inline) > div > a > .el-menu-item .svg-icon, + .el-menu:not(.el-menu--inline) > div > .el-submenu > .el-submenu__title .svg-icon { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: 28px; + height: 28px; + margin: 0 11px 0 0 !important; + padding: 5px; + border-radius: 9px; + color: #fff !important; + fill: #fff; + background: linear-gradient(135deg, #4e9bff, #2f6bff); + box-shadow: 0 5px 12px rgba(47, 107, 255, .25); + } + + /* 按菜单顺序轮换图标配色:第 1~8 个一组,之后循环 */ + @for $i from 1 through 8 { + $pair: nth($icon-palette, $i); + $from: nth($pair, 1); + $to: nth($pair, 2); + + .el-menu:not(.el-menu--inline) > div:nth-child(8n + #{$i}) > a > .el-menu-item .svg-icon, + .el-menu:not(.el-menu--inline) > div:nth-child(8n + #{$i}) > .el-submenu > .el-submenu__title .svg-icon { + background: linear-gradient(135deg, $from, $to); + box-shadow: 0 5px 12px rgba($to, .28); + } + } + + /* --------------------------------------------------------------------- + 二/三级:原地向下展开,竖线 + 圆点表达层级 + --------------------------------------------------------------------- */ + .el-menu--inline { + position: relative; + width: calc(100% - 30px) !important; + margin: -1px 10px 5px 20px; + padding: 4px 0 5px 17px; + box-sizing: border-box; + background: transparent !important; + border: 0 !important; + box-shadow: none !important; + } + + /* 子菜单左侧连接线 */ + .el-menu--inline::before { + content: ''; + position: absolute; + left: 5px; + top: 3px; + bottom: 7px; + width: 2px; + border-radius: 1px; + background: linear-gradient(180deg, rgba(47, 107, 255, .40), rgba(115, 133, 166, .22)); + } + + .el-menu--inline .el-menu-item, + .el-menu--inline .el-submenu > .el-submenu__title { + position: relative; + display: flex; + align-items: center; + min-width: 0 !important; + width: 100%; + height: $submenu-h; + line-height: $submenu-h; + margin: 1px 0 !important; + padding: 0 12px 0 24px !important; + box-sizing: border-box; + + border: 0 !important; + border-radius: 9px; + background: transparent !important; + box-shadow: none !important; + + color: $text-sub !important; + font-size: 13px; + font-weight: 500; + + transition: background .16s ease, color .16s ease; + } + + /* 圆点指示器 */ + .el-menu--inline .el-menu-item::before, + .el-menu--inline .el-submenu > .el-submenu__title::before { + content: ''; + position: absolute; + left: 7px; + top: 50%; + width: 8px; + height: 8px; + margin-top: -4px; + box-sizing: border-box; + border-radius: 50%; + border: 2px solid #7385a6; + background: #fff; + transition: all .2s ease; + } + + /* 二级不再显示大色块图标 */ + .el-menu--inline .el-menu-item .svg-icon, + .el-menu--inline .el-submenu > .el-submenu__title .svg-icon { + display: none; + } + + .el-menu--inline .el-menu-item:hover, + .el-menu--inline .el-submenu > .el-submenu__title:hover { + color: #2f6bff !important; + background: rgba(234, 241, 255, .72) !important; + transform: none !important; + + &::before { + border-color: #2f6bff; + background: rgba(47, 107, 255, .15); + } + } + + .el-menu--inline .el-menu-item.is-active { + color: $brand !important; + font-weight: 650; + background: linear-gradient(90deg, rgba(222, 234, 255, .92), rgba(239, 245, 255, .72)) !important; + } + + .el-menu--inline .el-menu-item.is-active::before { + width: 10px; + height: 10px; + margin-top: -5px; + border-color: $brand; + background: $brand; + box-shadow: 0 0 0 4px rgba(47, 107, 255, .15); + } + + /* 三级:在二级基础上小步缩进,不把内容推到很右边 */ + .el-menu--inline .el-menu--inline { + width: 100% !important; + margin: 0; + padding-left: 13px; + } + + .el-menu--inline .el-menu--inline::before { + left: 4px; + opacity: .65; + } + + .el-menu--inline .el-menu--inline .el-menu-item, + .el-menu--inline .el-menu--inline .el-submenu > .el-submenu__title { + height: 34px; + line-height: 34px; + padding-left: 22px !important; + font-size: 12.5px; + color: #7c879a !important; + } + + .el-menu--inline .el-submenu__icon-arrow { + right: 10px; + color: #9aa6bb; + font-size: 11px; + } + } + + /* ------------------------------------------------------------------------- + 桌面端收起态:54px 窄栏,只留彩色图标;子菜单改为向右弹出 + 移动端是整栏抽屉,用 :not(.mobile) 排除 + ------------------------------------------------------------------------- */ + .hideSidebar:not(.mobile) { + .sidebar-container { + width: 54px !important; + } + + .main-container { + margin-left: 54px; + } + + /* 窄栏不留横向内边距 */ + .el-menu { + padding: 4px 0 16px !important; + } + + /* SidebarItem 根容器水平居中其子元素 */ + .el-menu > div { + display: flex; + justify-content: center; + } + + /* 固定 40px 卡片,上下 margin 7px;左右由父容器 flex 居中 */ + .el-menu:not(.el-menu--inline) > div > a > .el-menu-item, + .el-menu:not(.el-menu--inline) > div > .el-submenu > .el-submenu__title { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 40px !important; + min-width: 40px !important; + height: 40px !important; + line-height: 40px !important; + margin: 7px 0 !important; + padding: 0 !important; + box-sizing: border-box; + + .svg-icon { + margin: 0 !important; + width: 28px; + height: 28px; + } + + span { + display: none !important; + } + } + + .el-submenu__icon-arrow { + display: none !important; + } + } + + /* ------------------------------------------------------------------------- + 移动端:整栏抽屉移出屏幕,不做 54px 窄栏 + ------------------------------------------------------------------------- */ + .mobile { + .main-container { + margin-left: 0; + } + + .sidebar-container { + width: $sidebar-width !important; + transition: transform .28s; + } + + &.hideSidebar .sidebar-container { + transform: translate3d(-#{$sidebar-width}, 0, 0); + } + } +} + +/* ========================================================================== + 收起态悬停弹出的子菜单(popper 挂在 body 上,必须在 #app 之外单独美化) + ========================================================================== */ +.el-menu--vertical { + > .el-menu--popup { + padding: 8px; + border-radius: 12px; + background: rgba(255, 255, 255, .96); + -webkit-backdrop-filter: blur(14px) saturate(155%); + backdrop-filter: blur(14px) saturate(155%); + border: 1px solid rgba(255, 255, 255, .9); + box-shadow: 0 12px 32px rgba(72, 105, 166, .18); + + .el-menu-item { + height: 36px; + line-height: 36px; + min-width: 150px; + margin: 2px 0; + padding: 0 14px !important; + border-radius: 8px; + color: $text-sub !important; + font-size: 13px; + font-weight: 500; + background: transparent !important; + + &:hover { + background: rgba(234, 241, 255, .85) !important; + color: $brand !important; + } + + &.is-active { + color: $brand !important; + font-weight: 650; + background: linear-gradient(90deg, rgba(222, 234, 255, .95), rgba(239, 245, 255, .8)) !important; + } + + .svg-icon { + display: none; + } + } + + /* 弹出的二/三级沿用圆点 + 竖线 */ + .el-menu--inline { + position: relative; + padding: 2px 0 2px 10px; + background: transparent !important; + + &::before { + content: ''; + position: absolute; + left: 4px; + top: 3px; + bottom: 7px; + width: 2px; + border-radius: 1px; + background: linear-gradient(180deg, rgba(47, 107, 255, .35), rgba(115, 133, 166, .2)); + } + + .el-menu-item, + .el-submenu > .el-submenu__title { + position: relative; + height: 34px; + line-height: 34px; + padding-left: 20px !important; + font-size: 12.5px; + color: #7c879a !important; + background: transparent !important; + + &::before { + content: ''; + position: absolute; + left: 4px; + top: 50%; + width: 7px; + height: 7px; + margin-top: -4px; + box-sizing: border-box; + border-radius: 50%; + border: 2px solid #9aa6bb; + background: #fff; + } + + &:hover { + color: $brand !important; + background: rgba(234, 241, 255, .85) !important; + + &::before { + border-color: $brand; + } + } + + &.is-active { + color: $brand !important; + font-weight: 650; + background: rgba(234, 241, 255, .9) !important; + + &::before { + border-color: $brand; + background: $brand; + } + } + } + } + } +} diff --git a/ruoyi-ui/src/assets/styles/skin.scss b/ruoyi-ui/src/assets/styles/skin.scss new file mode 100644 index 0000000..3fbb24b --- /dev/null +++ b/ruoyi-ui/src/assets/styles/skin.scss @@ -0,0 +1,333 @@ +/* ========================================================================== + 皮肤层:按钮 / 表单 / 表格 / 分页 / 页头 / 水印 / 暗色 + 只覆盖 Element UI 默认样式,不改组件源码;需要整体回滚时删掉本文件的 import 即可 + ========================================================================== */ + +$brand: #2f6bff; +$ink: #263654; +$ink-2: #5b6880; +$ink-3: #98a3b7; +$line: #eef2f9; + +/* ------------------------------- 按钮 ------------------------------- */ +.el-button { + border-radius: 10px; + font-size: 13px; + font-weight: 600; + line-height: 1; + transition: filter .16s ease, box-shadow .16s ease; + + > i { + font-size: 14px; + } + + &:hover { + filter: brightness(1.06); + } +} + +.el-button--mini { + padding: 9px 15px; + font-size: 13px; +} + +/* 工具栏按钮:实心胶囊(新增蓝 / 修改绿 / 删除红 / 导入青 / 导出橙) */ +.el-button--primary.is-plain { + background: #2f6bff; + border-color: #2f6bff; + color: #fff; + + &:hover, &:focus { + background: #1f57e0; + border-color: #1f57e0; + color: #fff; + } + + &.is-disabled, &.is-disabled:hover, &.is-disabled:focus { + background: #a9c1ff; + border-color: #a9c1ff; + color: #fff; + } +} + +.el-button--success.is-plain { + background: #22c55e; + border-color: #22c55e; + color: #fff; + + &:hover, &:focus { + background: #16a34a; + border-color: #16a34a; + color: #fff; + } + + &.is-disabled, &.is-disabled:hover, &.is-disabled:focus { + background: #a5e5bf; + border-color: #a5e5bf; + color: #fff; + } +} + +.el-button--danger.is-plain { + background: #ef4444; + border-color: #ef4444; + color: #fff; + + &:hover, &:focus { + background: #dc2626; + border-color: #dc2626; + color: #fff; + } + + &.is-disabled, &.is-disabled:hover, &.is-disabled:focus { + background: #f7b0b0; + border-color: #f7b0b0; + color: #fff; + } +} + +.el-button--info.is-plain { + background: #06b6d4; + border-color: #06b6d4; + color: #fff; + + &:hover, &:focus { + background: #0891b2; + border-color: #0891b2; + color: #fff; + } + + &.is-disabled, &.is-disabled:hover, &.is-disabled:focus { + background: #9adcea; + border-color: #9adcea; + color: #fff; + } +} + +.el-button--warning.is-plain { + background: #f59e0b; + border-color: #f59e0b; + color: #fff; + + &:hover, &:focus { + background: #d97706; + border-color: #d97706; + color: #fff; + } + + &.is-disabled, &.is-disabled:hover, &.is-disabled:focus { + background: #fbd08a; + border-color: #fbd08a; + color: #fff; + } +} + +/* 表格行内文字按钮:编辑蓝、删除红 */ +.el-table .el-button--text { + padding: 4px 6px; + font-size: 13px; + font-weight: 600; +} + +.el-table .el-button--text.text-danger { + color: #ef4444; + + &:hover, &:focus { + color: #dc2626; + } +} + +/* 表格行内「更多」用竖点图标 */ +.el-table .more-dot { + padding: 4px 10px; + font-size: 16px; + color: $ink-3; +} + +/* ------------------------------- 表单 ------------------------------- */ +.el-input__inner, +.el-textarea__inner { + border-radius: 10px; + border-color: #e6ecf6; + color: $ink; +} + +.el-input__inner:focus, +.el-textarea__inner:focus { + border-color: rgba(47, 107, 255, .55); + box-shadow: 0 0 0 3px rgba(47, 107, 255, .1); +} + +/* 搜索条不再显示 label,用 placeholder 表达(与设计图一致) */ +.el-form--inline .el-form-item__label { + display: none; +} + +.el-form--inline .el-form-item { + margin-right: 12px; + margin-bottom: 6px; +} + +/* ------------------------------- 表格 ------------------------------- */ +.el-table { + font-size: 13px; + color: #2b3a55; + + &::before { + display: none; + } + + th.el-table__cell { + height: 42px; + background: #f7f9fe; + color: $ink-2; + font-size: 13px; + font-weight: 600; + } + + td.el-table__cell { + padding: 8px 0; + border-bottom: 1px solid $line; + } + + .el-table__row:hover > td.el-table__cell { + background: #f8fbff; + } + + .el-button--text { + color: $brand; + } +} + +/* 表格里的缩略图收紧:真正决定行高的是这张 50px 预览图, + 收到 32px 后 10 行列表大约省 180px,分页才能跟表格一起进一屏 */ +.app-container .el-table .el-image { + width: 32px !important; + height: 32px !important; +} + +/* ------------------------------- 分页 ------------------------------- + 分页保持在页面最底部(与原型图一致),不做悬浮/吸底 + ------------------------------------------------------------------- */ +.app-container .pagination-container { + margin-top: 6px; + padding: 8px 16px 6px; + border-radius: 12px; +} + +.el-pagination { + padding: 6px 0 2px; + color: $ink-2; + font-weight: 500; + + .btn-prev, + .btn-next, + .el-pager li { + min-width: 32px; + height: 32px; + line-height: 30px; + margin: 0 3px; + border: 1px solid #e9eff8; + border-radius: 8px; + background: #fff; + color: $ink-2; + font-weight: 600; + } + + .el-pager li.active { + background: $brand; + border-color: $brand; + color: #fff; + } + + .el-pagination__sizes .el-input__inner, + .el-pagination__jump .el-input__inner { + border-radius: 8px; + } +} + +/* ------------------------------- 页头 ------------------------------- + 页面标题不再统一注入,各页自行决定;这里只保留通用类名的基础定义 + ------------------------------------------------------------------- */ +.page-header { + align-items: flex-start; + margin-bottom: 14px; + padding: 4px 0 0; + border-bottom: 0; +} + +/* ------------------------------- 弹层 ------------------------------- + $confirm / $alert(el-message-box)之前完全没被皮肤层覆盖,还是 Element + 默认的 4px 方角 + 15px 内边距,和 12px 的 el-dialog、10px 的按钮/输入框 + 不是一套。这里按 el-dialog 的头/内容/脚结构对齐,图标色也用工具栏同款色板 + ------------------------------------------------------------------- */ +.el-message-box { + padding-bottom: 0; + border: 1px solid rgba(255, 255, 255, 0.9); + border-radius: 12px; + box-shadow: 0 12px 32px rgba(72, 105, 166, 0.18); +} + +.el-message-box__header { + padding: 20px 24px 16px; + border-bottom: 1px solid rgba(115, 133, 166, 0.12); +} + +.el-message-box__title { + font-size: 18px; + font-weight: 600; + color: $ink; +} + +.el-message-box__headerbtn { + top: 20px; + right: 20px; +} + +.el-message-box__content { + padding: 24px; + color: $ink-2; + font-size: 14px; +} + +.el-message-box__status { + &.el-icon-warning { + color: #f59e0b; + } + &.el-icon-success { + color: #22c55e; + } + &.el-icon-error { + color: #ef4444; + } + &.el-icon-info { + color: $brand; + } +} + +.el-message-box__btns { + padding: 16px 24px; + border-top: 1px solid rgba(115, 133, 166, 0.12); + + .el-button { + min-width: 76px; + } +} + +/* 下拉 / 气泡 / 浮层:跟着控件一起圆角,别再冒方角 */ +.el-dropdown-menu, +.el-select-dropdown, +.el-popover, +.el-autocomplete-suggestion, +.el-picker-panel, +.el-message, +.el-notification { + border-color: rgba(115, 133, 166, 0.14); + border-radius: 10px; + box-shadow: 0 8px 24px rgba(72, 105, 166, 0.12); + overflow: hidden; +} + +.el-message { + min-width: 320px; +} diff --git a/ruoyi-ui/src/assets/styles/transition.scss b/ruoyi-ui/src/assets/styles/transition.scss new file mode 100644 index 0000000..073f8c6 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/transition.scss @@ -0,0 +1,49 @@ +// global transition css + +/* fade */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.28s; +} + +.fade-enter, +.fade-leave-active { + opacity: 0; +} + +/* fade-transform */ +.fade-transform--move, +.fade-transform-leave-active, +.fade-transform-enter-active { + transition: all .5s; +} + +.fade-transform-enter { + opacity: 0; + transform: translateX(-30px); +} + +.fade-transform-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* breadcrumb transition */ +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all .5s; +} + +.breadcrumb-enter, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all .5s; +} + +.breadcrumb-leave-active { + position: absolute; +} diff --git a/ruoyi-ui/src/assets/styles/variables.scss b/ruoyi-ui/src/assets/styles/variables.scss new file mode 100644 index 0000000..72f4cb9 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/variables.scss @@ -0,0 +1,54 @@ +// base color +$blue:#324157; +$light-blue:#3A71A8; +$red:#C03639; +$pink: #E65D6E; +$green: #30B08F; +$tiffany: #4AB7BD; +$yellow:#FEC171; +$panGreen: #30B08F; + +// 默认菜单主题风格 +$base-menu-color:#bfcbd9; +$base-menu-color-active:#f4f4f5; +$base-menu-background:#304156; +$base-logo-title-color: #ffffff; + +$base-menu-light-color:rgba(0,0,0,.70); +$base-menu-light-background:#ffffff; +$base-logo-light-title-color: #001529; + +$base-sub-menu-background:#1f2d3d; +$base-sub-menu-hover:#001528; + +// 自定义暗色菜单风格 +/** +$base-menu-color:hsla(0,0%,100%,.65); +$base-menu-color-active:#fff; +$base-menu-background:#001529; +$base-logo-title-color: #ffffff; + +$base-menu-light-color:rgba(0,0,0,.70); +$base-menu-light-background:#ffffff; +$base-logo-light-title-color: #001529; + +$base-sub-menu-background:#000c17; +$base-sub-menu-hover:#001528; +*/ + +$base-sidebar-width: 240px; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + menuColor: $base-menu-color; + menuLightColor: $base-menu-light-color; + menuColorActive: $base-menu-color-active; + menuBackground: $base-menu-background; + menuLightBackground: $base-menu-light-background; + subMenuBackground: $base-sub-menu-background; + subMenuHover: $base-sub-menu-hover; + sideBarWidth: $base-sidebar-width; + logoTitleColor: $base-logo-title-color; + logoLightTitleColor: $base-logo-light-title-color +} diff --git a/ruoyi-ui/src/components/Breadcrumb/index.vue b/ruoyi-ui/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..d533384 --- /dev/null +++ b/ruoyi-ui/src/components/Breadcrumb/index.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/ruoyi-ui/src/components/Crontab/day.vue b/ruoyi-ui/src/components/Crontab/day.vue new file mode 100644 index 0000000..fe3eaf0 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/day.vue @@ -0,0 +1,161 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/hour.vue b/ruoyi-ui/src/components/Crontab/hour.vue new file mode 100644 index 0000000..4b1f1fc --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/hour.vue @@ -0,0 +1,114 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/index.vue b/ruoyi-ui/src/components/Crontab/index.vue new file mode 100644 index 0000000..3963df2 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/index.vue @@ -0,0 +1,430 @@ + + + + diff --git a/ruoyi-ui/src/components/Crontab/min.vue b/ruoyi-ui/src/components/Crontab/min.vue new file mode 100644 index 0000000..43cab90 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/min.vue @@ -0,0 +1,116 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/components/Crontab/month.vue b/ruoyi-ui/src/components/Crontab/month.vue new file mode 100644 index 0000000..fd0ac38 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/month.vue @@ -0,0 +1,114 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/result.vue b/ruoyi-ui/src/components/Crontab/result.vue new file mode 100644 index 0000000..aea6e0e --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/result.vue @@ -0,0 +1,559 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/second.vue b/ruoyi-ui/src/components/Crontab/second.vue new file mode 100644 index 0000000..e7b7761 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/second.vue @@ -0,0 +1,117 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/week.vue b/ruoyi-ui/src/components/Crontab/week.vue new file mode 100644 index 0000000..1cec700 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/week.vue @@ -0,0 +1,202 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/year.vue b/ruoyi-ui/src/components/Crontab/year.vue new file mode 100644 index 0000000..5487a6c --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/year.vue @@ -0,0 +1,131 @@ + + + diff --git a/ruoyi-ui/src/components/DictData/index.js b/ruoyi-ui/src/components/DictData/index.js new file mode 100644 index 0000000..7b85d4a --- /dev/null +++ b/ruoyi-ui/src/components/DictData/index.js @@ -0,0 +1,49 @@ +import Vue from 'vue' +import store from '@/store' +import DataDict from '@/utils/dict' +import { getDicts as getDicts } from '@/api/system/dict/data' + +function searchDictByKey(dict, key) { + if (key == null && key == "") { + return null + } + try { + for (let i = 0; i < dict.length; i++) { + if (dict[i].key == key) { + return dict[i].value + } + } + } catch (e) { + return null + } +} + +function install() { + Vue.use(DataDict, { + metas: { + '*': { + labelField: 'dictLabel', + valueField: 'dictValue', + request(dictMeta) { + const storeDict = searchDictByKey(store.getters.dict, dictMeta.type) + if (storeDict) { + return new Promise(resolve => { resolve(storeDict) }) + } else { + return new Promise((resolve, reject) => { + getDicts(dictMeta.type).then(res => { + store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data }) + resolve(res.data) + }).catch(error => { + reject(error) + }) + }) + } + }, + }, + }, + }) +} + +export default { + install, +} \ No newline at end of file diff --git a/ruoyi-ui/src/components/DictTag/index.vue b/ruoyi-ui/src/components/DictTag/index.vue new file mode 100644 index 0000000..1ef13b9 --- /dev/null +++ b/ruoyi-ui/src/components/DictTag/index.vue @@ -0,0 +1,92 @@ + + + + diff --git a/ruoyi-ui/src/components/Editor/index.vue b/ruoyi-ui/src/components/Editor/index.vue new file mode 100644 index 0000000..8981d76 --- /dev/null +++ b/ruoyi-ui/src/components/Editor/index.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/ruoyi-ui/src/components/FileUpload/index.vue b/ruoyi-ui/src/components/FileUpload/index.vue new file mode 100644 index 0000000..6c583cf --- /dev/null +++ b/ruoyi-ui/src/components/FileUpload/index.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/ruoyi-ui/src/components/Hamburger/index.vue b/ruoyi-ui/src/components/Hamburger/index.vue new file mode 100644 index 0000000..368b002 --- /dev/null +++ b/ruoyi-ui/src/components/Hamburger/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/ruoyi-ui/src/components/HeaderSearch/index.vue b/ruoyi-ui/src/components/HeaderSearch/index.vue new file mode 100644 index 0000000..7d6780b --- /dev/null +++ b/ruoyi-ui/src/components/HeaderSearch/index.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/ruoyi-ui/src/components/IconSelect/index.vue b/ruoyi-ui/src/components/IconSelect/index.vue new file mode 100644 index 0000000..8dadc02 --- /dev/null +++ b/ruoyi-ui/src/components/IconSelect/index.vue @@ -0,0 +1,104 @@ + + + + + + diff --git a/ruoyi-ui/src/components/IconSelect/requireIcons.js b/ruoyi-ui/src/components/IconSelect/requireIcons.js new file mode 100644 index 0000000..99e5c54 --- /dev/null +++ b/ruoyi-ui/src/components/IconSelect/requireIcons.js @@ -0,0 +1,11 @@ + +const req = require.context('../../assets/icons/svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys() + +const re = /\.\/(.*)\.svg/ + +const icons = requireAll(req).map(i => { + return i.match(re)[1] +}) + +export default icons diff --git a/ruoyi-ui/src/components/ImagePreview/index.vue b/ruoyi-ui/src/components/ImagePreview/index.vue new file mode 100644 index 0000000..3c770c7 --- /dev/null +++ b/ruoyi-ui/src/components/ImagePreview/index.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/ruoyi-ui/src/components/ImageUpload/index.vue b/ruoyi-ui/src/components/ImageUpload/index.vue new file mode 100644 index 0000000..19a9957 --- /dev/null +++ b/ruoyi-ui/src/components/ImageUpload/index.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/ruoyi-ui/src/components/Pagination/index.vue b/ruoyi-ui/src/components/Pagination/index.vue new file mode 100644 index 0000000..56f5a6b --- /dev/null +++ b/ruoyi-ui/src/components/Pagination/index.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/ruoyi-ui/src/components/PanThumb/index.vue b/ruoyi-ui/src/components/PanThumb/index.vue new file mode 100644 index 0000000..1bcf417 --- /dev/null +++ b/ruoyi-ui/src/components/PanThumb/index.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/ruoyi-ui/src/components/ParentView/index.vue b/ruoyi-ui/src/components/ParentView/index.vue new file mode 100644 index 0000000..7bf6148 --- /dev/null +++ b/ruoyi-ui/src/components/ParentView/index.vue @@ -0,0 +1,3 @@ + diff --git a/ruoyi-ui/src/components/RightPanel/index.vue b/ruoyi-ui/src/components/RightPanel/index.vue new file mode 100644 index 0000000..5abeecb --- /dev/null +++ b/ruoyi-ui/src/components/RightPanel/index.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/ruoyi-ui/src/components/RightToolbar/index.vue b/ruoyi-ui/src/components/RightToolbar/index.vue new file mode 100644 index 0000000..527e07c --- /dev/null +++ b/ruoyi-ui/src/components/RightToolbar/index.vue @@ -0,0 +1,104 @@ + + + diff --git a/ruoyi-ui/src/components/RuoYi/Doc/index.vue b/ruoyi-ui/src/components/RuoYi/Doc/index.vue new file mode 100644 index 0000000..75fa864 --- /dev/null +++ b/ruoyi-ui/src/components/RuoYi/Doc/index.vue @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/components/RuoYi/Git/index.vue b/ruoyi-ui/src/components/RuoYi/Git/index.vue new file mode 100644 index 0000000..bdafbae --- /dev/null +++ b/ruoyi-ui/src/components/RuoYi/Git/index.vue @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/components/Screenfull/index.vue b/ruoyi-ui/src/components/Screenfull/index.vue new file mode 100644 index 0000000..d4e539c --- /dev/null +++ b/ruoyi-ui/src/components/Screenfull/index.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/ruoyi-ui/src/components/SizeSelect/index.vue b/ruoyi-ui/src/components/SizeSelect/index.vue new file mode 100644 index 0000000..069b5de --- /dev/null +++ b/ruoyi-ui/src/components/SizeSelect/index.vue @@ -0,0 +1,56 @@ + + + diff --git a/ruoyi-ui/src/components/SvgIcon/index.vue b/ruoyi-ui/src/components/SvgIcon/index.vue new file mode 100644 index 0000000..e4bf5ad --- /dev/null +++ b/ruoyi-ui/src/components/SvgIcon/index.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/ruoyi-ui/src/components/ThemePicker/index.vue b/ruoyi-ui/src/components/ThemePicker/index.vue new file mode 100644 index 0000000..1714e1f --- /dev/null +++ b/ruoyi-ui/src/components/ThemePicker/index.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/ruoyi-ui/src/components/TopNav/index.vue b/ruoyi-ui/src/components/TopNav/index.vue new file mode 100644 index 0000000..daee1b8 --- /dev/null +++ b/ruoyi-ui/src/components/TopNav/index.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/ruoyi-ui/src/components/iFrame/index.vue b/ruoyi-ui/src/components/iFrame/index.vue new file mode 100644 index 0000000..426857f --- /dev/null +++ b/ruoyi-ui/src/components/iFrame/index.vue @@ -0,0 +1,36 @@ + + + diff --git a/ruoyi-ui/src/layout/components/Navbar.vue b/ruoyi-ui/src/layout/components/Navbar.vue new file mode 100644 index 0000000..85d3d09 --- /dev/null +++ b/ruoyi-ui/src/layout/components/Navbar.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/ruoyi-ui/src/layout/components/Settings/index.vue b/ruoyi-ui/src/layout/components/Settings/index.vue new file mode 100644 index 0000000..8b49842 --- /dev/null +++ b/ruoyi-ui/src/layout/components/Settings/index.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/ruoyi-ui/src/layout/components/Sidebar/FixiOSBug.js b/ruoyi-ui/src/layout/components/Sidebar/FixiOSBug.js new file mode 100644 index 0000000..6823726 --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/FixiOSBug.js @@ -0,0 +1,25 @@ +export default { + computed: { + device() { + return this.$store.state.app.device + } + }, + mounted() { + // In order to fix the click on menu on the ios device will trigger the mouseleave bug + this.fixBugIniOS() + }, + methods: { + fixBugIniOS() { + const $subMenu = this.$refs.subMenu + if ($subMenu) { + const handleMouseleave = $subMenu.handleMouseleave + $subMenu.handleMouseleave = (e) => { + if (this.device === 'mobile') { + return + } + handleMouseleave(e) + } + } + } + } +} diff --git a/ruoyi-ui/src/layout/components/Sidebar/Item.vue b/ruoyi-ui/src/layout/components/Sidebar/Item.vue new file mode 100644 index 0000000..be3285d --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/Item.vue @@ -0,0 +1,33 @@ + diff --git a/ruoyi-ui/src/layout/components/Sidebar/Link.vue b/ruoyi-ui/src/layout/components/Sidebar/Link.vue new file mode 100644 index 0000000..57ee904 --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/Link.vue @@ -0,0 +1,45 @@ + + + diff --git a/ruoyi-ui/src/layout/components/Sidebar/Logo.vue b/ruoyi-ui/src/layout/components/Sidebar/Logo.vue new file mode 100644 index 0000000..3c2b647 --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/Logo.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/ruoyi-ui/src/layout/components/Sidebar/SidebarItem.vue b/ruoyi-ui/src/layout/components/Sidebar/SidebarItem.vue new file mode 100644 index 0000000..4853fbb --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/SidebarItem.vue @@ -0,0 +1,100 @@ + + + diff --git a/ruoyi-ui/src/layout/components/Sidebar/index.vue b/ruoyi-ui/src/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..ba9003c --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/index.vue @@ -0,0 +1,55 @@ + + + diff --git a/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue b/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue new file mode 100644 index 0000000..bb753a1 --- /dev/null +++ b/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/ruoyi-ui/src/layout/components/TagsView/index.vue b/ruoyi-ui/src/layout/components/TagsView/index.vue new file mode 100644 index 0000000..6df0dbe --- /dev/null +++ b/ruoyi-ui/src/layout/components/TagsView/index.vue @@ -0,0 +1,383 @@ + + + + + + + diff --git a/ruoyi-ui/src/layout/components/index.js b/ruoyi-ui/src/layout/components/index.js new file mode 100644 index 0000000..104bd3a --- /dev/null +++ b/ruoyi-ui/src/layout/components/index.js @@ -0,0 +1,5 @@ +export { default as AppMain } from './AppMain' +export { default as Navbar } from './Navbar' +export { default as Settings } from './Settings' +export { default as Sidebar } from './Sidebar/index.vue' +export { default as TagsView } from './TagsView/index.vue' diff --git a/ruoyi-ui/src/layout/index.vue b/ruoyi-ui/src/layout/index.vue new file mode 100644 index 0000000..d012d41 --- /dev/null +++ b/ruoyi-ui/src/layout/index.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/ruoyi-ui/src/layout/mixin/ResizeHandler.js b/ruoyi-ui/src/layout/mixin/ResizeHandler.js new file mode 100644 index 0000000..e8d0df8 --- /dev/null +++ b/ruoyi-ui/src/layout/mixin/ResizeHandler.js @@ -0,0 +1,45 @@ +import store from '@/store' + +const { body } = document +const WIDTH = 992 // refer to Bootstrap's responsive design + +export default { + watch: { + $route(route) { + if (this.device === 'mobile' && this.sidebar.opened) { + store.dispatch('app/closeSideBar', { withoutAnimation: false }) + } + } + }, + beforeMount() { + window.addEventListener('resize', this.$_resizeHandler) + }, + beforeDestroy() { + window.removeEventListener('resize', this.$_resizeHandler) + }, + mounted() { + const isMobile = this.$_isMobile() + if (isMobile) { + store.dispatch('app/toggleDevice', 'mobile') + store.dispatch('app/closeSideBar', { withoutAnimation: true }) + } + }, + methods: { + // use $_ for mixins properties + // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential + $_isMobile() { + const rect = body.getBoundingClientRect() + return rect.width - 1 < WIDTH + }, + $_resizeHandler() { + if (!document.hidden) { + const isMobile = this.$_isMobile() + store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') + + if (isMobile) { + store.dispatch('app/closeSideBar', { withoutAnimation: true }) + } + } + } + } +} diff --git a/ruoyi-ui/src/main.js b/ruoyi-ui/src/main.js new file mode 100644 index 0000000..04c673a --- /dev/null +++ b/ruoyi-ui/src/main.js @@ -0,0 +1,103 @@ +import Vue from 'vue' + +import Cookies from 'js-cookie' + +import Element from 'element-ui' +import './assets/styles/element-variables.scss' + +import '@/assets/styles/index.scss' // global css +import '@/assets/styles/ruoyi.scss' // ruoyi css +import App from './App' +import store from './store' +import router from './router' +import directive from './directive' // directive +import plugins from './plugins' // plugins +import { download } from '@/utils/request' + +import './assets/icons' // icon +import './permission' // permission control +import { getDicts } from "@/api/system/dict/data"; +import { getConfigKey } from "@/api/system/config"; +import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi"; +// 分页组件 +import Pagination from "@/components/Pagination"; +// 自定义表格工具组件 +import RightToolbar from "@/components/RightToolbar" +// 富文本组件 +import Editor from "@/components/Editor" +// 文件上传组件 +import FileUpload from "@/components/FileUpload" +// 图片上传组件 +import ImageUpload from "@/components/ImageUpload" +// 图片预览组件 +import ImagePreview from "@/components/ImagePreview" +// 字典标签组件 +import DictTag from '@/components/DictTag' +// 头部标签组件 +import VueMeta from 'vue-meta' +// 字典数据组件 +import DictData from '@/components/DictData' + + +// 全局方法挂载 +Vue.prototype.getDicts = getDicts +Vue.prototype.getConfigKey = getConfigKey +Vue.prototype.parseTime = parseTime +Vue.prototype.resetForm = resetForm +Vue.prototype.addDateRange = addDateRange +Vue.prototype.selectDictLabel = selectDictLabel +Vue.prototype.selectDictLabels = selectDictLabels +Vue.prototype.download = download +Vue.prototype.handleTree = handleTree + +// 全局组件挂载 +Vue.component('DictTag', DictTag) +Vue.component('Pagination', Pagination) +Vue.component('RightToolbar', RightToolbar) +Vue.component('Editor', Editor) +Vue.component('FileUpload', FileUpload) +Vue.component('ImageUpload', ImageUpload) +Vue.component('ImagePreview', ImagePreview) + +Vue.use(directive) +Vue.use(plugins) +Vue.use(VueMeta) +DictData.install() + +/** + * If you don't want to use mock-server + * you want to use MockJs for mock api + * you can execute: mockXHR() + * + * Currently MockJs will be used in the production environment, + * please remove it before going online! ! ! + */ + +Vue.use(Element, { + size: Cookies.get('size') || 'medium' // set element-ui default size +}) + +// 顶部标签切换时,阻止组件的 activated 钩子执行 +// 避免标签切换触发数据请求,与 axios 拦截器形成双重保障 +Vue.mixin({ + beforeCreate() { + const activated = this.$options.activated + if (!activated || !activated.length) return + this.$options.activated = activated.map(fn => function(...args) { + if (this.$store.state.tagsView.fromTagsView) { + console.log('[tagSwitch] 跳过 activated:', this.$options.name || this.$vnode.tag) + return + } + return fn.call(this, ...args) + }) + } +}) + +Vue.config.productionTip = false + +new Vue({ + el: '#app', + router, + store, + render: h => h(App) +}) diff --git a/ruoyi-ui/src/permission.js b/ruoyi-ui/src/permission.js new file mode 100644 index 0000000..9ddb894 --- /dev/null +++ b/ruoyi-ui/src/permission.js @@ -0,0 +1,64 @@ +import router from './router' +import store from './store' +import { Message } from 'element-ui' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import { getToken } from '@/utils/auth' +import { isRelogin } from '@/utils/request' + +NProgress.configure({ showSpinner: false }) + +const whiteList = ['/login', '/register'] + +router.beforeEach((to, from, next) => { + NProgress.start() + if (getToken()) { + to.meta.title && store.dispatch('settings/setTitle', to.meta.title) + /* has token*/ + if (to.path === '/login') { + next({ path: '/' }) + NProgress.done() + } else { + if (store.getters.roles.length === 0) { + isRelogin.show = true + // 判断当前用户是否已拉取完user_info信息 + store.dispatch('GetInfo').then(() => { + isRelogin.show = false + store.dispatch('GenerateRoutes').then(accessRoutes => { + // 根据roles权限生成可访问的路由表 + router.addRoutes(accessRoutes) // 动态添加可访问路由表 + next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 + }) + }).catch(err => { + store.dispatch('LogOut').then(() => { + Message.error(err) + next({ path: '/' }) + }) + }) + } else { + next() + } + } + } else { + // 没有token + if (whiteList.indexOf(to.path) !== -1) { + // 在免登录白名单,直接进入 + next() + } else { + next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页 + NProgress.done() + } + } +}) + +router.afterEach(() => { + NProgress.done() + // 标签切换后,延迟清除标志,给组件初始请求留出拦截窗口 + if (store.state.tagsView.fromTagsView) { + console.log('[tagSwitch] afterEach,3秒后清除 fromTagsView') + setTimeout(() => { + console.log('[tagSwitch] 清除 fromTagsView') + store.commit('tagsView/SET_FROM_TAGS_VIEW', false) + }, 3000) + } +}) diff --git a/ruoyi-ui/src/plugins/auth.js b/ruoyi-ui/src/plugins/auth.js new file mode 100644 index 0000000..6c6bc24 --- /dev/null +++ b/ruoyi-ui/src/plugins/auth.js @@ -0,0 +1,60 @@ +import store from '@/store' + +function authPermission(permission) { + const all_permission = "*:*:*"; + const permissions = store.getters && store.getters.permissions + if (permission && permission.length > 0) { + return permissions.some(v => { + return all_permission === v || v === permission + }) + } else { + return false + } +} + +function authRole(role) { + const super_admin = "admin"; + const roles = store.getters && store.getters.roles + if (role && role.length > 0) { + return roles.some(v => { + return super_admin === v || v === role + }) + } else { + return false + } +} + +export default { + // 验证用户是否具备某权限 + hasPermi(permission) { + return authPermission(permission); + }, + // 验证用户是否含有指定权限,只需包含其中一个 + hasPermiOr(permissions) { + return permissions.some(item => { + return authPermission(item) + }) + }, + // 验证用户是否含有指定权限,必须全部拥有 + hasPermiAnd(permissions) { + return permissions.every(item => { + return authPermission(item) + }) + }, + // 验证用户是否具备某角色 + hasRole(role) { + return authRole(role); + }, + // 验证用户是否含有指定角色,只需包含其中一个 + hasRoleOr(roles) { + return roles.some(item => { + return authRole(item) + }) + }, + // 验证用户是否含有指定角色,必须全部拥有 + hasRoleAnd(roles) { + return roles.every(item => { + return authRole(item) + }) + } +} diff --git a/ruoyi-ui/src/plugins/cache.js b/ruoyi-ui/src/plugins/cache.js new file mode 100644 index 0000000..6b5c00b --- /dev/null +++ b/ruoyi-ui/src/plugins/cache.js @@ -0,0 +1,77 @@ +const sessionCache = { + set (key, value) { + if (!sessionStorage) { + return + } + if (key != null && value != null) { + sessionStorage.setItem(key, value) + } + }, + get (key) { + if (!sessionStorage) { + return null + } + if (key == null) { + return null + } + return sessionStorage.getItem(key) + }, + setJSON (key, jsonValue) { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON (key) { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + }, + remove (key) { + sessionStorage.removeItem(key); + } +} +const localCache = { + set (key, value) { + if (!localStorage) { + return + } + if (key != null && value != null) { + localStorage.setItem(key, value) + } + }, + get (key) { + if (!localStorage) { + return null + } + if (key == null) { + return null + } + return localStorage.getItem(key) + }, + setJSON (key, jsonValue) { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON (key) { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + }, + remove (key) { + localStorage.removeItem(key); + } +} + +export default { + /** + * 会话级缓存 + */ + session: sessionCache, + /** + * 本地缓存 + */ + local: localCache +} diff --git a/ruoyi-ui/src/plugins/download.js b/ruoyi-ui/src/plugins/download.js new file mode 100644 index 0000000..ffb8c14 --- /dev/null +++ b/ruoyi-ui/src/plugins/download.js @@ -0,0 +1,72 @@ +import axios from 'axios' +import { Message } from 'element-ui' +import { saveAs } from 'file-saver' +import { getToken } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { blobValidate } from "@/utils/ruoyi"; + +const baseURL = process.env.VUE_APP_BASE_API + +export default { + name(name, isDelete = true) { + var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then((res) => { + const isBlob = blobValidate(res.data); + if (isBlob) { + const blob = new Blob([res.data]) + this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) + } else { + this.printErrMsg(res.data); + } + }) + }, + resource(resource) { + var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource); + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then((res) => { + const isBlob = blobValidate(res.data); + if (isBlob) { + const blob = new Blob([res.data]) + this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) + } else { + this.printErrMsg(res.data); + } + }) + }, + zip(url, name) { + var url = baseURL + url + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then((res) => { + const isBlob = blobValidate(res.data); + if (isBlob) { + const blob = new Blob([res.data], { type: 'application/zip' }) + this.saveAs(blob, name) + } else { + this.printErrMsg(res.data); + } + }) + }, + saveAs(text, name, opts) { + saveAs(text, name, opts); + }, + async printErrMsg(data) { + const resText = await data.text(); + const rspObj = JSON.parse(resText); + const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] + Message.error(errMsg); + } +} + diff --git a/ruoyi-ui/src/plugins/index.js b/ruoyi-ui/src/plugins/index.js new file mode 100644 index 0000000..d000f2d --- /dev/null +++ b/ruoyi-ui/src/plugins/index.js @@ -0,0 +1,20 @@ +import tab from './tab' +import auth from './auth' +import cache from './cache' +import modal from './modal' +import download from './download' + +export default { + install(Vue) { + // 页签操作 + Vue.prototype.$tab = tab + // 认证对象 + Vue.prototype.$auth = auth + // 缓存对象 + Vue.prototype.$cache = cache + // 模态框对象 + Vue.prototype.$modal = modal + // 下载文件 + Vue.prototype.$download = download + } +} diff --git a/ruoyi-ui/src/plugins/modal.js b/ruoyi-ui/src/plugins/modal.js new file mode 100644 index 0000000..b37ca14 --- /dev/null +++ b/ruoyi-ui/src/plugins/modal.js @@ -0,0 +1,83 @@ +import { Message, MessageBox, Notification, Loading } from 'element-ui' + +let loadingInstance; + +export default { + // 消息提示 + msg(content) { + Message.info(content) + }, + // 错误消息 + msgError(content) { + Message.error(content) + }, + // 成功消息 + msgSuccess(content) { + Message.success(content) + }, + // 警告消息 + msgWarning(content) { + Message.warning(content) + }, + // 弹出提示 + alert(content) { + MessageBox.alert(content, "系统提示") + }, + // 错误提示 + alertError(content) { + MessageBox.alert(content, "系统提示", { type: 'error' }) + }, + // 成功提示 + alertSuccess(content) { + MessageBox.alert(content, "系统提示", { type: 'success' }) + }, + // 警告提示 + alertWarning(content) { + MessageBox.alert(content, "系统提示", { type: 'warning' }) + }, + // 通知提示 + notify(content) { + Notification.info(content) + }, + // 错误通知 + notifyError(content) { + Notification.error(content); + }, + // 成功通知 + notifySuccess(content) { + Notification.success(content) + }, + // 警告通知 + notifyWarning(content) { + Notification.warning(content) + }, + // 确认窗体 + confirm(content) { + return MessageBox.confirm(content, "系统提示", { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: "warning", + }) + }, + // 提交内容 + prompt(content) { + return MessageBox.prompt(content, "系统提示", { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: "warning", + }) + }, + // 打开遮罩层 + loading(content) { + loadingInstance = Loading.service({ + lock: true, + text: content, + spinner: "el-icon-loading", + background: "rgba(0, 0, 0, 0.7)", + }) + }, + // 关闭遮罩层 + closeLoading() { + loadingInstance.close(); + } +} diff --git a/ruoyi-ui/src/plugins/tab.js b/ruoyi-ui/src/plugins/tab.js new file mode 100644 index 0000000..b029c0e --- /dev/null +++ b/ruoyi-ui/src/plugins/tab.js @@ -0,0 +1,71 @@ +import store from '@/store' +import router from '@/router'; + +export default { + // 刷新当前tab页签 + refreshPage(obj) { + const { path, query, matched } = router.currentRoute; + if (obj === undefined) { + matched.forEach((m) => { + if (m.components && m.components.default && m.components.default.name) { + if (!['Layout', 'ParentView'].includes(m.components.default.name)) { + obj = { name: m.components.default.name, path: path, query: query }; + } + } + }); + } + return store.dispatch('tagsView/delCachedView', obj).then(() => { + const { path, query } = obj + router.replace({ + path: '/redirect' + path, + query: query + }) + }) + }, + // 关闭当前tab页签,打开新页签 + closeOpenPage(obj) { + store.dispatch("tagsView/delView", router.currentRoute); + if (obj !== undefined) { + return router.push(obj); + } + }, + // 关闭指定tab页签 + closePage(obj) { + if (obj === undefined) { + return store.dispatch('tagsView/delView', router.currentRoute).then(({ visitedViews }) => { + const latestView = visitedViews.slice(-1)[0] + if (latestView) { + return router.push(latestView.fullPath) + } + return router.push('/'); + }); + } + return store.dispatch('tagsView/delView', obj); + }, + // 关闭所有tab页签 + closeAllPage() { + return store.dispatch('tagsView/delAllViews'); + }, + // 关闭左侧tab页签 + closeLeftPage(obj) { + return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute); + }, + // 关闭右侧tab页签 + closeRightPage(obj) { + return store.dispatch('tagsView/delRightTags', obj || router.currentRoute); + }, + // 关闭其他tab页签 + closeOtherPage(obj) { + return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute); + }, + // 添加tab页签 + openPage(title, url, params) { + var obj = { path: url, meta: { title: title } } + store.dispatch('tagsView/addView', obj); + return router.push({ path: url, query: params }); + }, + // 修改tab页签 + updatePage(obj) { + return store.dispatch('tagsView/updateVisitedView', obj); + } +} diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js new file mode 100644 index 0000000..891f055 --- /dev/null +++ b/ruoyi-ui/src/router/index.js @@ -0,0 +1,184 @@ +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) + +/* Layout */ +import Layout from '@/layout' + +/** + * Note: 路由配置项 + * + * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1 + * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 + * // 若你想不管路由下面的 children 声明的个数都显示你的根路由 + * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由 + * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + * name:'router-name' // 设定路由的名字,一定要填写不然使用时会出现各种问题 + * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数 + * roles: ['admin', 'common'] // 访问路由的角色权限 + * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限 + * meta : { + noCache: true // 如果设置为true,则不会被 缓存(默认 false) + title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 + icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg + breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示 + activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。 + } + */ + +// 公共路由 +export const constantRoutes = [ + { + path: '/redirect', + component: Layout, + hidden: true, + children: [ + { + path: '/redirect/:path(.*)', + component: () => import('@/views/redirect') + } + ] + }, + { + path: '/login', + component: () => import('@/views/login'), + hidden: true + }, + { + path: '/register', + component: () => import('@/views/register'), + hidden: true + }, + { + path: '/404', + component: () => import('@/views/error/404'), + hidden: true + }, + { + path: '/401', + component: () => import('@/views/error/401'), + hidden: true + }, + { + path: '', + component: Layout, + redirect: 'index', + children: [ + { + path: 'index', + component: () => import('@/views/index'), + name: 'Index', + meta: { title: '首页', icon: 'dashboard', affix: true } + } + ] + }, + { + path: '/user', + component: Layout, + hidden: true, + redirect: 'noredirect', + children: [ + { + path: 'profile', + component: () => import('@/views/system/user/profile/index'), + name: 'Profile', + meta: { title: '个人中心', icon: 'user' } + } + ] + }, + +] + +// 动态路由,基于用户权限动态去加载 +export const dynamicRoutes = [ + { + path: '/system/user-auth', + component: Layout, + hidden: true, + permissions: ['system:user:edit'], + children: [ + { + path: 'role/:userId(\\d+)', + component: () => import('@/views/system/user/authRole'), + name: 'AuthRole', + meta: { title: '分配角色', activeMenu: '/system/user' } + } + ] + }, + { + path: '/system/role-auth', + component: Layout, + hidden: true, + permissions: ['system:role:edit'], + children: [ + { + path: 'user/:roleId(\\d+)', + component: () => import('@/views/system/role/authUser'), + name: 'AuthUser', + meta: { title: '分配用户', activeMenu: '/system/role' } + } + ] + }, + { + path: '/system/dict-data', + component: Layout, + hidden: true, + permissions: ['system:dict:list'], + children: [ + { + path: 'index/:dictId(\\d+)', + component: () => import('@/views/system/dict/data'), + name: 'Data', + meta: { title: '字典数据', activeMenu: '/system/dict' } + } + ] + }, + { + path: '/monitor/job-log', + component: Layout, + hidden: true, + permissions: ['monitor:job:list'], + children: [ + { + path: 'index/:jobId(\\d+)', + component: () => import('@/views/monitor/job/log'), + name: 'JobLog', + meta: { title: '调度日志', activeMenu: '/monitor/job' } + } + ] + }, + { + path: '/tool/gen-edit', + component: Layout, + hidden: true, + permissions: ['tool:gen:edit'], + children: [ + { + path: 'index/:tableId(\\d+)', + component: () => import('@/views/tool/gen/editTable'), + name: 'GenEdit', + meta: { title: '修改生成配置', activeMenu: '/tool/gen' } + } + ] + } +] + +// 防止连续点击多次路由报错 +let routerPush = Router.prototype.push; +let routerReplace = Router.prototype.replace; +// push +Router.prototype.push = function push(location) { + return routerPush.call(this, location).catch(err => err) +} +// replace +Router.prototype.replace = function push(location) { + return routerReplace.call(this, location).catch(err => err) +} + +export default new Router({ + mode: 'hash', // 使用hash模式避免刷新404(history模式需nginx try_files回退) + scrollBehavior: () => ({ y: 0 }), + routes: constantRoutes +}) diff --git a/ruoyi-ui/src/settings.js b/ruoyi-ui/src/settings.js new file mode 100644 index 0000000..6a0b09f --- /dev/null +++ b/ruoyi-ui/src/settings.js @@ -0,0 +1,44 @@ +module.exports = { + /** + * 侧边栏主题 深色主题theme-dark,浅色主题theme-light + */ + sideTheme: 'theme-dark', + + /** + * 是否系统布局配置 + */ + showSettings: false, + + /** + * 是否显示顶部导航 + */ + topNav: false, + + /** + * 是否显示 tagsView + */ + tagsView: true, + + /** + * 是否固定头部 + */ + fixedHeader: false, + + /** + * 是否显示logo + */ + sidebarLogo: true, + + /** + * 是否显示动态标题 + */ + dynamicTitle: false, + + /** + * @type {string | array} 'production' | ['production', 'development'] + * @description Need show err logs component. + * The default is only used in the production env + * If you want to also use it in dev, you can pass ['production', 'development'] + */ + errorLog: 'production' +} diff --git a/ruoyi-ui/src/store/getters.js b/ruoyi-ui/src/store/getters.js new file mode 100644 index 0000000..8adb1b6 --- /dev/null +++ b/ruoyi-ui/src/store/getters.js @@ -0,0 +1,19 @@ +const getters = { + sidebar: state => state.app.sidebar, + size: state => state.app.size, + device: state => state.app.device, + dict: state => state.dict.dict, + visitedViews: state => state.tagsView.visitedViews, + cachedViews: state => state.tagsView.cachedViews, + token: state => state.user.token, + avatar: state => state.user.avatar, + name: state => state.user.name, + introduction: state => state.user.introduction, + roles: state => state.user.roles, + permissions: state => state.user.permissions, + permission_routes: state => state.permission.routes, + topbarRouters:state => state.permission.topbarRouters, + defaultRoutes:state => state.permission.defaultRoutes, + sidebarRouters:state => state.permission.sidebarRouters, +} +export default getters diff --git a/ruoyi-ui/src/store/index.js b/ruoyi-ui/src/store/index.js new file mode 100644 index 0000000..97aaef8 --- /dev/null +++ b/ruoyi-ui/src/store/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import app from './modules/app' +import dict from './modules/dict' +import user from './modules/user' +import tagsView from './modules/tagsView' +import permission from './modules/permission' +import settings from './modules/settings' +import getters from './getters' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + modules: { + app, + dict, + user, + tagsView, + permission, + settings + }, + getters +}) + +export default store diff --git a/ruoyi-ui/src/store/modules/app.js b/ruoyi-ui/src/store/modules/app.js new file mode 100644 index 0000000..3e22d1c --- /dev/null +++ b/ruoyi-ui/src/store/modules/app.js @@ -0,0 +1,66 @@ +import Cookies from 'js-cookie' + +const state = { + sidebar: { + opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, + withoutAnimation: false, + hide: false + }, + device: 'desktop', + size: Cookies.get('size') || 'medium' +} + +const mutations = { + TOGGLE_SIDEBAR: state => { + if (state.sidebar.hide) { + return false; + } + state.sidebar.opened = !state.sidebar.opened + state.sidebar.withoutAnimation = false + if (state.sidebar.opened) { + Cookies.set('sidebarStatus', 1) + } else { + Cookies.set('sidebarStatus', 0) + } + }, + CLOSE_SIDEBAR: (state, withoutAnimation) => { + Cookies.set('sidebarStatus', 0) + state.sidebar.opened = false + state.sidebar.withoutAnimation = withoutAnimation + }, + TOGGLE_DEVICE: (state, device) => { + state.device = device + }, + SET_SIZE: (state, size) => { + state.size = size + Cookies.set('size', size) + }, + SET_SIDEBAR_HIDE: (state, status) => { + state.sidebar.hide = status + } +} + +const actions = { + toggleSideBar({ commit }) { + commit('TOGGLE_SIDEBAR') + }, + closeSideBar({ commit }, { withoutAnimation }) { + commit('CLOSE_SIDEBAR', withoutAnimation) + }, + toggleDevice({ commit }, device) { + commit('TOGGLE_DEVICE', device) + }, + setSize({ commit }, size) { + commit('SET_SIZE', size) + }, + toggleSideBarHide({ commit }, status) { + commit('SET_SIDEBAR_HIDE', status) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/ruoyi-ui/src/store/modules/dict.js b/ruoyi-ui/src/store/modules/dict.js new file mode 100644 index 0000000..7a1b2f0 --- /dev/null +++ b/ruoyi-ui/src/store/modules/dict.js @@ -0,0 +1,50 @@ +const state = { + dict: new Array() +} +const mutations = { + SET_DICT: (state, { key, value }) => { + if (key !== null && key !== "") { + state.dict.push({ + key: key, + value: value + }) + } + }, + REMOVE_DICT: (state, key) => { + try { + for (let i = 0; i < state.dict.length; i++) { + if (state.dict[i].key == key) { + state.dict.splice(i, 1) + return true + } + } + } catch (e) { + } + }, + CLEAN_DICT: (state) => { + state.dict = new Array() + } +} + +const actions = { + // 设置字典 + setDict({ commit }, data) { + commit('SET_DICT', data) + }, + // 删除字典 + removeDict({ commit }, key) { + commit('REMOVE_DICT', key) + }, + // 清空字典 + cleanDict({ commit }) { + commit('CLEAN_DICT') + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} + diff --git a/ruoyi-ui/src/store/modules/permission.js b/ruoyi-ui/src/store/modules/permission.js new file mode 100644 index 0000000..2287665 --- /dev/null +++ b/ruoyi-ui/src/store/modules/permission.js @@ -0,0 +1,133 @@ +import auth from '@/plugins/auth' +import router, { constantRoutes, dynamicRoutes } from '@/router' +import { getRouters } from '@/api/menu' +import Layout from '@/layout/index' +import ParentView from '@/components/ParentView' +import InnerLink from '@/layout/components/InnerLink' + +const permission = { + state: { + routes: [], + addRoutes: [], + defaultRoutes: [], + topbarRouters: [], + sidebarRouters: [] + }, + mutations: { + SET_ROUTES: (state, routes) => { + state.addRoutes = routes + state.routes = constantRoutes.concat(routes) + }, + SET_DEFAULT_ROUTES: (state, routes) => { + state.defaultRoutes = constantRoutes.concat(routes) + }, + SET_TOPBAR_ROUTES: (state, routes) => { + state.topbarRouters = routes + }, + SET_SIDEBAR_ROUTERS: (state, routes) => { + state.sidebarRouters = routes + }, + }, + actions: { + // 生成路由 + GenerateRoutes({ commit }) { + return new Promise(resolve => { + // 向后端请求路由数据 + getRouters().then(res => { + const sdata = JSON.parse(JSON.stringify(res.data)) + const rdata = JSON.parse(JSON.stringify(res.data)) + const sidebarRoutes = filterAsyncRouter(sdata) + const rewriteRoutes = filterAsyncRouter(rdata, false, true) + const asyncRoutes = filterDynamicRoutes(dynamicRoutes); + rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true }) + router.addRoutes(asyncRoutes); + commit('SET_ROUTES', rewriteRoutes) + commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes)) + commit('SET_DEFAULT_ROUTES', sidebarRoutes) + commit('SET_TOPBAR_ROUTES', sidebarRoutes) + resolve(rewriteRoutes) + }) + }) + } + } +} + +// 遍历后台传来的路由字符串,转换为组件对象 +function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { + return asyncRouterMap.filter(route => { + if (type && route.children) { + route.children = filterChildren(route.children) + } + if (route.component) { + // Layout ParentView 组件特殊处理 + if (route.component === 'Layout') { + route.component = Layout + } else if (route.component === 'ParentView') { + route.component = ParentView + } else if (route.component === 'InnerLink') { + route.component = InnerLink + } else { + route.component = loadView(route.component) + } + } + if (route.children != null && route.children && route.children.length) { + route.children = filterAsyncRouter(route.children, route, type) + } else { + delete route['children'] + delete route['redirect'] + } + return true + }) +} + +function filterChildren(childrenMap, lastRouter = false) { + var children = [] + childrenMap.forEach((el, index) => { + if (el.children && el.children.length) { + if (el.component === 'ParentView' && !lastRouter) { + el.children.forEach(c => { + c.path = el.path + '/' + c.path + if (c.children && c.children.length) { + children = children.concat(filterChildren(c.children, c)) + return + } + children.push(c) + }) + return + } + } + if (lastRouter) { + el.path = lastRouter.path + '/' + el.path + } + children = children.concat(el) + }) + return children +} + +// 动态路由遍历,验证是否具备权限 +export function filterDynamicRoutes(routes) { + const res = [] + routes.forEach(route => { + if (route.permissions) { + if (auth.hasPermiOr(route.permissions)) { + res.push(route) + } + } else if (route.roles) { + if (auth.hasRoleOr(route.roles)) { + res.push(route) + } + } + }) + return res +} + +export const loadView = (view) => { + if (process.env.NODE_ENV === 'development') { + return (resolve) => require([`@/views/${view}`], resolve) + } else { + // 使用 import 实现生产环境的路由懒加载 + return () => import(`@/views/${view}`) + } +} + +export default permission diff --git a/ruoyi-ui/src/store/modules/settings.js b/ruoyi-ui/src/store/modules/settings.js new file mode 100644 index 0000000..2455a1e --- /dev/null +++ b/ruoyi-ui/src/store/modules/settings.js @@ -0,0 +1,42 @@ +import defaultSettings from '@/settings' + +const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings + +const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || '' +const state = { + title: '', + theme: storageSetting.theme || '#409EFF', + sideTheme: storageSetting.sideTheme || sideTheme, + showSettings: showSettings, + topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav, + tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView, + fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader, + sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo, + dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle +} +const mutations = { + CHANGE_SETTING: (state, { key, value }) => { + if (state.hasOwnProperty(key)) { + state[key] = value + } + } +} + +const actions = { + // 修改布局设置 + changeSetting({ commit }, data) { + commit('CHANGE_SETTING', data) + }, + // 设置网页标题 + setTitle({ commit }, title) { + state.title = title + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} + diff --git a/ruoyi-ui/src/store/modules/tagsView.js b/ruoyi-ui/src/store/modules/tagsView.js new file mode 100644 index 0000000..7b9de76 --- /dev/null +++ b/ruoyi-ui/src/store/modules/tagsView.js @@ -0,0 +1,232 @@ +const state = { + visitedViews: [], + cachedViews: [], + iframeViews: [], + fromTagsView: false +} + +const mutations = { + SET_FROM_TAGS_VIEW: (state, value) => { + state.fromTagsView = value + }, + ADD_IFRAME_VIEW: (state, view) => { + if (state.iframeViews.some(v => v.path === view.path)) return + state.iframeViews.push( + Object.assign({}, view, { + title: view.meta.title || 'no-name' + }) + ) + }, + ADD_VISITED_VIEW: (state, view) => { + if (state.visitedViews.some(v => v.path === view.path)) return + state.visitedViews.push( + Object.assign({}, view, { + title: view.meta.title || 'no-name' + }) + ) + }, + ADD_CACHED_VIEW: (state, view) => { + if (state.cachedViews.includes(view.name)) return + if (view.meta && !view.meta.noCache) { + state.cachedViews.push(view.name) + } + }, + DEL_VISITED_VIEW: (state, view) => { + for (const [i, v] of state.visitedViews.entries()) { + if (v.path === view.path) { + state.visitedViews.splice(i, 1) + break + } + } + state.iframeViews = state.iframeViews.filter(item => item.path !== view.path) + }, + DEL_IFRAME_VIEW: (state, view) => { + state.iframeViews = state.iframeViews.filter(item => item.path !== view.path) + }, + DEL_CACHED_VIEW: (state, view) => { + const index = state.cachedViews.indexOf(view.name) + index > -1 && state.cachedViews.splice(index, 1) + }, + + DEL_OTHERS_VISITED_VIEWS: (state, view) => { + state.visitedViews = state.visitedViews.filter(v => { + return v.meta.affix || v.path === view.path + }) + state.iframeViews = state.iframeViews.filter(item => item.path === view.path) + }, + DEL_OTHERS_CACHED_VIEWS: (state, view) => { + const index = state.cachedViews.indexOf(view.name) + if (index > -1) { + state.cachedViews = state.cachedViews.slice(index, index + 1) + } else { + state.cachedViews = [] + } + }, + DEL_ALL_VISITED_VIEWS: state => { + // keep affix tags + const affixTags = state.visitedViews.filter(tag => tag.meta.affix) + state.visitedViews = affixTags + state.iframeViews = [] + }, + DEL_ALL_CACHED_VIEWS: state => { + state.cachedViews = [] + }, + UPDATE_VISITED_VIEW: (state, view) => { + for (let v of state.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + }, + DEL_RIGHT_VIEWS: (state, view) => { + const index = state.visitedViews.findIndex(v => v.path === view.path) + if (index === -1) { + return + } + state.visitedViews = state.visitedViews.filter((item, idx) => { + if (idx <= index || (item.meta && item.meta.affix)) { + return true + } + const i = state.cachedViews.indexOf(item.name) + if (i > -1) { + state.cachedViews.splice(i, 1) + } + if(item.meta.link) { + const fi = state.iframeViews.findIndex(v => v.path === item.path) + state.iframeViews.splice(fi, 1) + } + return false + }) + }, + DEL_LEFT_VIEWS: (state, view) => { + const index = state.visitedViews.findIndex(v => v.path === view.path) + if (index === -1) { + return + } + state.visitedViews = state.visitedViews.filter((item, idx) => { + if (idx >= index || (item.meta && item.meta.affix)) { + return true + } + const i = state.cachedViews.indexOf(item.name) + if (i > -1) { + state.cachedViews.splice(i, 1) + } + if(item.meta.link) { + const fi = state.iframeViews.findIndex(v => v.path === item.path) + state.iframeViews.splice(fi, 1) + } + return false + }) + } +} + +const actions = { + addView({ dispatch }, view) { + dispatch('addVisitedView', view) + dispatch('addCachedView', view) + }, + addIframeView({ commit }, view) { + commit('ADD_IFRAME_VIEW', view) + }, + addVisitedView({ commit }, view) { + commit('ADD_VISITED_VIEW', view) + }, + addCachedView({ commit }, view) { + commit('ADD_CACHED_VIEW', view) + }, + delView({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delVisitedView', view) + dispatch('delCachedView', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delVisitedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_VISITED_VIEW', view) + resolve([...state.visitedViews]) + }) + }, + delIframeView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_IFRAME_VIEW', view) + resolve([...state.iframeViews]) + }) + }, + delCachedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_CACHED_VIEW', view) + resolve([...state.cachedViews]) + }) + }, + delOthersViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delOthersVisitedViews', view) + dispatch('delOthersCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delOthersVisitedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_VISITED_VIEWS', view) + resolve([...state.visitedViews]) + }) + }, + delOthersCachedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_CACHED_VIEWS', view) + resolve([...state.cachedViews]) + }) + }, + delAllViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delAllVisitedViews', view) + dispatch('delAllCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delAllVisitedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_VISITED_VIEWS') + resolve([...state.visitedViews]) + }) + }, + delAllCachedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_CACHED_VIEWS') + resolve([...state.cachedViews]) + }) + }, + updateVisitedView({ commit }, view) { + commit('UPDATE_VISITED_VIEW', view) + }, + delRightTags({ commit }, view) { + return new Promise(resolve => { + commit('DEL_RIGHT_VIEWS', view) + resolve([...state.visitedViews]) + }) + }, + delLeftTags({ commit }, view) { + return new Promise(resolve => { + commit('DEL_LEFT_VIEWS', view) + resolve([...state.visitedViews]) + }) + }, +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/ruoyi-ui/src/store/modules/user.js b/ruoyi-ui/src/store/modules/user.js new file mode 100644 index 0000000..cdbab1e --- /dev/null +++ b/ruoyi-ui/src/store/modules/user.js @@ -0,0 +1,101 @@ +import { login, logout, getInfo } from '@/api/login' +import { getToken, setToken, removeToken } from '@/utils/auth' + +const user = { + state: { + token: getToken(), + id: '', + name: '', + avatar: '', + roles: [], + permissions: [] + }, + + mutations: { + SET_TOKEN: (state, token) => { + state.token = token + }, + SET_ID: (state, id) => { + state.id = id + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_AVATAR: (state, avatar) => { + state.avatar = avatar + }, + SET_ROLES: (state, roles) => { + state.roles = roles + }, + SET_PERMISSIONS: (state, permissions) => { + state.permissions = permissions + } + }, + + actions: { + // 登录 + Login({ commit }, userInfo) { + const username = userInfo.username.trim() + const password = userInfo.password + const code = userInfo.code + const uuid = userInfo.uuid + return new Promise((resolve, reject) => { + login(username, password, code, uuid).then(res => { + setToken(res.token) + commit('SET_TOKEN', res.token) + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 获取用户信息 + GetInfo({ commit, state }) { + return new Promise((resolve, reject) => { + getInfo().then(res => { + const user = res.user + const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar; + if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 + commit('SET_ROLES', res.roles) + commit('SET_PERMISSIONS', res.permissions) + } else { + commit('SET_ROLES', ['ROLE_DEFAULT']) + } + commit('SET_ID', user.userId) + commit('SET_NAME', user.userName) + commit('SET_AVATAR', avatar) + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + + // 退出系统 + LogOut({ commit, state }) { + return new Promise((resolve, reject) => { + logout(state.token).then(() => { + commit('SET_TOKEN', '') + commit('SET_ROLES', []) + commit('SET_PERMISSIONS', []) + removeToken() + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 前端 登出 + FedLogOut({ commit }) { + return new Promise(resolve => { + commit('SET_TOKEN', '') + removeToken() + resolve() + }) + } + } +} + +export default user diff --git a/ruoyi-ui/src/utils/accountSource.js b/ruoyi-ui/src/utils/accountSource.js new file mode 100644 index 0000000..316dd62 --- /dev/null +++ b/ruoyi-ui/src/utils/accountSource.js @@ -0,0 +1,75 @@ +// 账户流水来源 source 码 -> 中文名 映射 +// 与后端枚举保持一致:com.ruoyi.domain.common.constant.TtAccountRecordSource +export const ACCOUNT_SOURCE_MAP = { + 1: '充值收入', + 2: '经典开箱消费', + 3: '经典开箱游戏奖励', + 4: '对战游戏消费', + 5: '对战游戏奖励', + 6: 'roll房消费', + 7: 'roll房游戏奖励', + 8: '逐梦前行消费', + 9: '逐梦前行游戏奖励', + 10: '背包分解饰品收入', + 11: '首次下载任务奖励', + 12: '商城兑换消费', + 13: '推广福利收入', + 14: '后台操作', + 15: '奖励津贴', + 16: '积分兑换金币', + 17: '兑换CDKEY收入', + 18: '注册奖励', + 19: '综合流水排行榜奖励', + 20: '金币消费排行榜奖励', + 21: '积分消费排行榜奖励', + 22: '出货排行榜奖励', + 23: '首充赠送', + 24: '推广等级充值赠送', + 25: '充值红包收入', + 26: '推广等级达标赠送', + 27: '充值返利', + 28: '对战排行榜奖励', + 29: '消费排行榜奖励', + 30: 'VIP等级达标赠送', + 31: '主播日常发放工资', + 32: '充值金额达到门槛奖励', + 33: '玩转指数盲盒消费', + 34: '荒野争锋消费', + 37: '荒野争锋奖励', + 38: '疯狂转盘消费', + 39: '疯狂转盘奖励', + 64: '疯狂转盘注单退款', + 40: '幸运升级消费', + 41: '幸运升级奖励', + 42: '主播后台提取消费', + 43: '充值福利奖励', + 44: 'VIP流水奖励', + 45: '卡商生成卡密消费', + 46: '卡商充值收入', + 47: '特殊游戏返利', + 48: '推广CDKEY兑换收入', + 49: '经典游戏返利', + 50: '极速糖果消费', + 52: '极速糖果奖励', + 56: 'Roll房未发放退回主播', + 59: '主播Roll房奖池扣款', + 60: '玫瑰女郎消费', + 61: '玫瑰女郎奖励', + 62: '招商周结佣金', + 63: '流水抽奖奖励', + 65: '管理员调整余额', + 66: '管理员调整积分' +}; + +/** + * 格式化流水行的“变动来源”。 + * 优先按 source 码映射为中文名;映射缺失时回退显示 remark 原文;再无则显示 '-'。 + * @param {Object} row 流水行数据,需含 source / remark + * @returns {String} + */ +export function formatAccountSource(row) { + if (!row) return '-'; + const name = ACCOUNT_SOURCE_MAP[row.source]; + if (name) return name; + return row.remark || '-'; +} diff --git a/ruoyi-ui/src/utils/aes.js b/ruoyi-ui/src/utils/aes.js new file mode 100644 index 0000000..8aadd61 --- /dev/null +++ b/ruoyi-ui/src/utils/aes.js @@ -0,0 +1,50 @@ +import CryptoJS from 'crypto-js' + +// AES 密钥和 IV,需与后端 application.yml 中 aes.key / aes.iv 保持一致 +const AES_KEY = 'ruoyi@aes@202416' // 16位 +const AES_IV = 'ruoyi@aes@iv2024' // 16位 + +/** + * 全局开关:读取环境变量 VUE_APP_USE_AES_ENCRYPT + * 安全回退:undefined / 非 'true' 字符串均视为关闭 + */ +export const AES_ENABLED = process.env.VUE_APP_USE_AES_ENCRYPT === 'true' + +/** + * AES-CBC 加密(PKCS7Padding,输出 Base64 字符串) + * @param {string} plainText 明文字符串 + * @param {boolean} forceEncrypt 强制加密(忽略全局开关),默认 false + * @returns {string} Base64 密文 或 原文(未开启时) + */ +export function aesEncrypt(plainText, forceEncrypt = false) { + if (!AES_ENABLED && !forceEncrypt) { + console.log('[AES] 加密已关闭,发送明文') + return plainText + } + const key = CryptoJS.enc.Utf8.parse(AES_KEY) + const iv = CryptoJS.enc.Utf8.parse(AES_IV) + const encrypted = CryptoJS.AES.encrypt(plainText, key, { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }) + const result = encrypted.toString() + console.log('[AES] 加密已开启,密文前20位:', result.substring(0, 20) + '...') + return result +} + +/** + * AES-CBC 解密(PKCS7Padding,输入 Base64 字符串) + * @param {string} cipherText Base64 密文 + * @returns {string} 明文字符串 + */ +export function aesDecrypt(cipherText) { + const key = CryptoJS.enc.Utf8.parse(AES_KEY) + const iv = CryptoJS.enc.Utf8.parse(AES_IV) + const decrypted = CryptoJS.AES.decrypt(cipherText, key, { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }) + return decrypted.toString(CryptoJS.enc.Utf8) +} diff --git a/ruoyi-ui/src/utils/auth.js b/ruoyi-ui/src/utils/auth.js new file mode 100644 index 0000000..08a43d6 --- /dev/null +++ b/ruoyi-ui/src/utils/auth.js @@ -0,0 +1,15 @@ +import Cookies from 'js-cookie' + +const TokenKey = 'Admin-Token' + +export function getToken() { + return Cookies.get(TokenKey) +} + +export function setToken(token) { + return Cookies.set(TokenKey, token) +} + +export function removeToken() { + return Cookies.remove(TokenKey) +} diff --git a/ruoyi-ui/src/utils/dict/Dict.js b/ruoyi-ui/src/utils/dict/Dict.js new file mode 100644 index 0000000..104bd6e --- /dev/null +++ b/ruoyi-ui/src/utils/dict/Dict.js @@ -0,0 +1,82 @@ +import Vue from 'vue' +import { mergeRecursive } from "@/utils/ruoyi"; +import DictMeta from './DictMeta' +import DictData from './DictData' + +const DEFAULT_DICT_OPTIONS = { + types: [], +} + +/** + * @classdesc 字典 + * @property {Object} label 标签对象,内部属性名为字典类型名称 + * @property {Object} dict 字段数组,内部属性名为字典类型名称 + * @property {Array.} _dictMetas 字典元数据数组 + */ +export default class Dict { + constructor() { + this.owner = null + this.label = {} + this.type = {} + } + + init(options) { + if (options instanceof Array) { + options = { types: options } + } + const opts = mergeRecursive(DEFAULT_DICT_OPTIONS, options) + if (opts.types === undefined) { + throw new Error('need dict types') + } + const ps = [] + this._dictMetas = opts.types.map(t => DictMeta.parse(t)) + this._dictMetas.forEach(dictMeta => { + const type = dictMeta.type + Vue.set(this.label, type, {}) + Vue.set(this.type, type, []) + if (dictMeta.lazy) { + return + } + ps.push(loadDict(this, dictMeta)) + }) + return Promise.all(ps) + } + + /** + * 重新加载字典 + * @param {String} type 字典类型 + */ + reloadDict(type) { + const dictMeta = this._dictMetas.find(e => e.type === type) + if (dictMeta === undefined) { + return Promise.reject(`the dict meta of ${type} was not found`) + } + return loadDict(this, dictMeta) + } +} + +/** + * 加载字典 + * @param {Dict} dict 字典 + * @param {DictMeta} dictMeta 字典元数据 + * @returns {Promise} + */ +function loadDict(dict, dictMeta) { + return dictMeta.request(dictMeta) + .then(response => { + const type = dictMeta.type + let dicts = dictMeta.responseConverter(response, dictMeta) + if (!(dicts instanceof Array)) { + console.error('the return of responseConverter must be Array.') + dicts = [] + } else if (dicts.filter(d => d instanceof DictData).length !== dicts.length) { + console.error('the type of elements in dicts must be DictData') + dicts = [] + } + dict.type[type].splice(0, Number.MAX_SAFE_INTEGER, ...dicts) + dicts.forEach(d => { + Vue.set(dict.label[type], d.value, d.label) + }) + return dicts + }) +} diff --git a/ruoyi-ui/src/utils/dict/DictConverter.js b/ruoyi-ui/src/utils/dict/DictConverter.js new file mode 100644 index 0000000..0cf5df8 --- /dev/null +++ b/ruoyi-ui/src/utils/dict/DictConverter.js @@ -0,0 +1,17 @@ +import DictOptions from './DictOptions' +import DictData from './DictData' + +export default function(dict, dictMeta) { + const label = determineDictField(dict, dictMeta.labelField, ...DictOptions.DEFAULT_LABEL_FIELDS) + const value = determineDictField(dict, dictMeta.valueField, ...DictOptions.DEFAULT_VALUE_FIELDS) + return new DictData(dict[label], dict[value], dict) +} + +/** + * 确定字典字段 + * @param {DictData} dict + * @param {...String} fields + */ +function determineDictField(dict, ...fields) { + return fields.find(f => Object.prototype.hasOwnProperty.call(dict, f)) +} diff --git a/ruoyi-ui/src/utils/dict/DictData.js b/ruoyi-ui/src/utils/dict/DictData.js new file mode 100644 index 0000000..afc763e --- /dev/null +++ b/ruoyi-ui/src/utils/dict/DictData.js @@ -0,0 +1,13 @@ +/** + * @classdesc 字典数据 + * @property {String} label 标签 + * @property {*} value 标签 + * @property {Object} raw 原始数据 + */ +export default class DictData { + constructor(label, value, raw) { + this.label = label + this.value = value + this.raw = raw + } +} diff --git a/ruoyi-ui/src/utils/dict/DictMeta.js b/ruoyi-ui/src/utils/dict/DictMeta.js new file mode 100644 index 0000000..9779daa --- /dev/null +++ b/ruoyi-ui/src/utils/dict/DictMeta.js @@ -0,0 +1,38 @@ +import { mergeRecursive } from "@/utils/ruoyi"; +import DictOptions from './DictOptions' + +/** + * @classdesc 字典元数据 + * @property {String} type 类型 + * @property {Function} request 请求 + * @property {String} label 标签字段 + * @property {String} value 值字段 + */ +export default class DictMeta { + constructor(options) { + this.type = options.type + this.request = options.request + this.responseConverter = options.responseConverter + this.labelField = options.labelField + this.valueField = options.valueField + this.lazy = options.lazy === true + } +} + + +/** + * 解析字典元数据 + * @param {Object} options + * @returns {DictMeta} + */ +DictMeta.parse= function(options) { + let opts = null + if (typeof options === 'string') { + opts = DictOptions.metas[options] || {} + opts.type = options + } else if (typeof options === 'object') { + opts = options + } + opts = mergeRecursive(DictOptions.metas['*'], opts) + return new DictMeta(opts) +} diff --git a/ruoyi-ui/src/utils/dict/DictOptions.js b/ruoyi-ui/src/utils/dict/DictOptions.js new file mode 100644 index 0000000..338a94e --- /dev/null +++ b/ruoyi-ui/src/utils/dict/DictOptions.js @@ -0,0 +1,51 @@ +import { mergeRecursive } from "@/utils/ruoyi"; +import dictConverter from './DictConverter' + +export const options = { + metas: { + '*': { + /** + * 字典请求,方法签名为function(dictMeta: DictMeta): Promise + */ + request: (dictMeta) => { + console.log(`load dict ${dictMeta.type}`) + return Promise.resolve([]) + }, + /** + * 字典响应数据转换器,方法签名为function(response: Object, dictMeta: DictMeta): DictData + */ + responseConverter, + labelField: 'label', + valueField: 'value', + }, + }, + /** + * 默认标签字段 + */ + DEFAULT_LABEL_FIELDS: ['label', 'name', 'title'], + /** + * 默认值字段 + */ + DEFAULT_VALUE_FIELDS: ['value', 'id', 'uid', 'key'], +} + +/** + * 映射字典 + * @param {Object} response 字典数据 + * @param {DictMeta} dictMeta 字典元数据 + * @returns {DictData} + */ +function responseConverter(response, dictMeta) { + const dicts = response.content instanceof Array ? response.content : response + if (dicts === undefined) { + console.warn(`no dict data of "${dictMeta.type}" found in the response`) + return [] + } + return dicts.map(d => dictConverter(d, dictMeta)) +} + +export function mergeOptions(src) { + mergeRecursive(options, src) +} + +export default options diff --git a/ruoyi-ui/src/utils/dict/index.js b/ruoyi-ui/src/utils/dict/index.js new file mode 100644 index 0000000..215eb9e --- /dev/null +++ b/ruoyi-ui/src/utils/dict/index.js @@ -0,0 +1,33 @@ +import Dict from './Dict' +import { mergeOptions } from './DictOptions' + +export default function(Vue, options) { + mergeOptions(options) + Vue.mixin({ + data() { + if (this.$options === undefined || this.$options.dicts === undefined || this.$options.dicts === null) { + return {} + } + const dict = new Dict() + dict.owner = this + return { + dict + } + }, + created() { + if (!(this.dict instanceof Dict)) { + return + } + options.onCreated && options.onCreated(this.dict) + this.dict.init(this.$options.dicts).then(() => { + options.onReady && options.onReady(this.dict) + this.$nextTick(() => { + this.$emit('dictReady', this.dict) + if (this.$options.methods && this.$options.methods.onDictReady instanceof Function) { + this.$options.methods.onDictReady.call(this, this.dict) + } + }) + }) + }, + }) +} diff --git a/ruoyi-ui/src/utils/errorCode.js b/ruoyi-ui/src/utils/errorCode.js new file mode 100644 index 0000000..d2111ee --- /dev/null +++ b/ruoyi-ui/src/utils/errorCode.js @@ -0,0 +1,6 @@ +export default { + '401': '认证失败,无法访问系统资源', + '403': '当前操作没有权限', + '404': '访问资源不存在', + 'default': '系统未知错误,请反馈给管理员' +} diff --git a/ruoyi-ui/src/utils/generator/config.js b/ruoyi-ui/src/utils/generator/config.js new file mode 100644 index 0000000..7abf227 --- /dev/null +++ b/ruoyi-ui/src/utils/generator/config.js @@ -0,0 +1,438 @@ +export const formConf = { + formRef: 'elForm', + formModel: 'formData', + size: 'medium', + labelPosition: 'right', + labelWidth: 100, + formRules: 'rules', + gutter: 15, + disabled: false, + span: 24, + formBtns: true +} + +export const inputComponents = [ + { + label: '单行文本', + tag: 'el-input', + tagIcon: 'input', + placeholder: '请输入', + defaultValue: undefined, + span: 24, + labelWidth: null, + style: { width: '100%' }, + clearable: true, + prepend: '', + append: '', + 'prefix-icon': '', + 'suffix-icon': '', + maxlength: null, + 'show-word-limit': false, + readonly: false, + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/input' + }, + { + label: '多行文本', + tag: 'el-input', + tagIcon: 'textarea', + type: 'textarea', + placeholder: '请输入', + defaultValue: undefined, + span: 24, + labelWidth: null, + autosize: { + minRows: 4, + maxRows: 4 + }, + style: { width: '100%' }, + maxlength: null, + 'show-word-limit': false, + readonly: false, + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/input' + }, + { + label: '密码', + tag: 'el-input', + tagIcon: 'password', + placeholder: '请输入', + defaultValue: undefined, + span: 24, + 'show-password': true, + labelWidth: null, + style: { width: '100%' }, + clearable: true, + prepend: '', + append: '', + 'prefix-icon': '', + 'suffix-icon': '', + maxlength: null, + 'show-word-limit': false, + readonly: false, + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/input' + }, + { + label: '计数器', + tag: 'el-input-number', + tagIcon: 'number', + placeholder: '', + defaultValue: undefined, + span: 24, + labelWidth: null, + min: undefined, + max: undefined, + step: undefined, + 'step-strictly': false, + precision: undefined, + 'controls-position': '', + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/input-number' + } +] + +export const selectComponents = [ + { + label: '下拉选择', + tag: 'el-select', + tagIcon: 'select', + placeholder: '请选择', + defaultValue: undefined, + span: 24, + labelWidth: null, + style: { width: '100%' }, + clearable: true, + disabled: false, + required: true, + filterable: false, + multiple: false, + options: [{ + label: '选项一', + value: 1 + }, { + label: '选项二', + value: 2 + }], + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/select' + }, + { + label: '级联选择', + tag: 'el-cascader', + tagIcon: 'cascader', + placeholder: '请选择', + defaultValue: [], + span: 24, + labelWidth: null, + style: { width: '100%' }, + props: { + props: { + multiple: false + } + }, + 'show-all-levels': true, + disabled: false, + clearable: true, + filterable: false, + required: true, + options: [{ + id: 1, + value: 1, + label: '选项1', + children: [{ + id: 2, + value: 2, + label: '选项1-1' + }] + }], + dataType: 'dynamic', + labelKey: 'label', + valueKey: 'value', + childrenKey: 'children', + separator: '/', + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/cascader' + }, + { + label: '单选框组', + tag: 'el-radio-group', + tagIcon: 'radio', + defaultValue: undefined, + span: 24, + labelWidth: null, + style: {}, + optionType: 'default', + border: false, + size: 'medium', + disabled: false, + required: true, + options: [{ + label: '选项一', + value: 1 + }, { + label: '选项二', + value: 2 + }], + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/radio' + }, + { + label: '多选框组', + tag: 'el-checkbox-group', + tagIcon: 'checkbox', + defaultValue: [], + span: 24, + labelWidth: null, + style: {}, + optionType: 'default', + border: false, + size: 'medium', + disabled: false, + required: true, + options: [{ + label: '选项一', + value: 1 + }, { + label: '选项二', + value: 2 + }], + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/checkbox' + }, + { + label: '开关', + tag: 'el-switch', + tagIcon: 'switch', + defaultValue: false, + span: 24, + labelWidth: null, + style: {}, + disabled: false, + required: true, + 'active-text': '', + 'inactive-text': '', + 'active-color': null, + 'inactive-color': null, + 'active-value': true, + 'inactive-value': false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/switch' + }, + { + label: '滑块', + tag: 'el-slider', + tagIcon: 'slider', + defaultValue: null, + span: 24, + labelWidth: null, + disabled: false, + required: true, + min: 0, + max: 100, + step: 1, + 'show-stops': false, + range: false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/slider' + }, + { + label: '时间选择', + tag: 'el-time-picker', + tagIcon: 'time', + placeholder: '请选择', + defaultValue: null, + span: 24, + labelWidth: null, + style: { width: '100%' }, + disabled: false, + clearable: true, + required: true, + 'picker-options': { + selectableRange: '00:00:00-23:59:59' + }, + format: 'HH:mm:ss', + 'value-format': 'HH:mm:ss', + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/time-picker' + }, + { + label: '时间范围', + tag: 'el-time-picker', + tagIcon: 'time-range', + defaultValue: null, + span: 24, + labelWidth: null, + style: { width: '100%' }, + disabled: false, + clearable: true, + required: true, + 'is-range': true, + 'range-separator': '至', + 'start-placeholder': '开始时间', + 'end-placeholder': '结束时间', + format: 'HH:mm:ss', + 'value-format': 'HH:mm:ss', + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/time-picker' + }, + { + label: '日期选择', + tag: 'el-date-picker', + tagIcon: 'date', + placeholder: '请选择', + defaultValue: null, + type: 'date', + span: 24, + labelWidth: null, + style: { width: '100%' }, + disabled: false, + clearable: true, + required: true, + format: 'yyyy-MM-dd', + 'value-format': 'yyyy-MM-dd', + readonly: false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/date-picker' + }, + { + label: '日期范围', + tag: 'el-date-picker', + tagIcon: 'date-range', + defaultValue: null, + span: 24, + labelWidth: null, + style: { width: '100%' }, + type: 'daterange', + 'range-separator': '至', + 'start-placeholder': '开始日期', + 'end-placeholder': '结束日期', + disabled: false, + clearable: true, + required: true, + format: 'yyyy-MM-dd', + 'value-format': 'yyyy-MM-dd', + readonly: false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/date-picker' + }, + { + label: '评分', + tag: 'el-rate', + tagIcon: 'rate', + defaultValue: 0, + span: 24, + labelWidth: null, + style: {}, + max: 5, + 'allow-half': false, + 'show-text': false, + 'show-score': false, + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/rate' + }, + { + label: '颜色选择', + tag: 'el-color-picker', + tagIcon: 'color', + defaultValue: null, + labelWidth: null, + 'show-alpha': false, + 'color-format': '', + disabled: false, + required: true, + size: 'medium', + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/color-picker' + }, + { + label: '上传', + tag: 'el-upload', + tagIcon: 'upload', + action: 'https://jsonplaceholder.typicode.com/posts/', + defaultValue: null, + labelWidth: null, + disabled: false, + required: true, + accept: '', + name: 'file', + 'auto-upload': true, + showTip: false, + buttonText: '点击上传', + fileSize: 2, + sizeUnit: 'MB', + 'list-type': 'text', + multiple: false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/upload' + } +] + +export const layoutComponents = [ + { + layout: 'rowFormItem', + tagIcon: 'row', + type: 'default', + justify: 'start', + align: 'top', + label: '行容器', + layoutTree: true, + children: [], + document: 'https://element.eleme.cn/#/zh-CN/component/layout' + }, + { + layout: 'colFormItem', + label: '按钮', + changeTag: true, + labelWidth: null, + tag: 'el-button', + tagIcon: 'button', + span: 24, + default: '主要按钮', + type: 'primary', + icon: 'el-icon-search', + size: 'medium', + disabled: false, + document: 'https://element.eleme.cn/#/zh-CN/component/button' + } +] + +// 组件rule的触发方式,无触发方式的组件不生成rule +export const trigger = { + 'el-input': 'blur', + 'el-input-number': 'blur', + 'el-select': 'change', + 'el-radio-group': 'change', + 'el-checkbox-group': 'change', + 'el-cascader': 'change', + 'el-time-picker': 'change', + 'el-date-picker': 'change', + 'el-rate': 'change' +} diff --git a/ruoyi-ui/src/utils/generator/css.js b/ruoyi-ui/src/utils/generator/css.js new file mode 100644 index 0000000..c1c62e6 --- /dev/null +++ b/ruoyi-ui/src/utils/generator/css.js @@ -0,0 +1,18 @@ +const styles = { + 'el-rate': '.el-rate{display: inline-block; vertical-align: text-top;}', + 'el-upload': '.el-upload__tip{line-height: 1.2;}' +} + +function addCss(cssList, el) { + const css = styles[el.tag] + css && cssList.indexOf(css) === -1 && cssList.push(css) + if (el.children) { + el.children.forEach(el2 => addCss(cssList, el2)) + } +} + +export function makeUpCss(conf) { + const cssList = [] + conf.fields.forEach(el => addCss(cssList, el)) + return cssList.join('\n') +} diff --git a/ruoyi-ui/src/utils/generator/drawingDefault.js b/ruoyi-ui/src/utils/generator/drawingDefault.js new file mode 100644 index 0000000..09f133c --- /dev/null +++ b/ruoyi-ui/src/utils/generator/drawingDefault.js @@ -0,0 +1,29 @@ +export default [ + { + layout: 'colFormItem', + tagIcon: 'input', + label: '手机号', + vModel: 'mobile', + formId: 6, + tag: 'el-input', + placeholder: '请输入手机号', + defaultValue: '', + span: 24, + style: { width: '100%' }, + clearable: true, + prepend: '', + append: '', + 'prefix-icon': 'el-icon-mobile', + 'suffix-icon': '', + maxlength: 11, + 'show-word-limit': true, + readonly: false, + disabled: false, + required: true, + changeTag: true, + regList: [{ + pattern: '/^1(3|4|5|7|8|9)\\d{9}$/', + message: '手机号格式错误' + }] + } +] diff --git a/ruoyi-ui/src/utils/generator/html.js b/ruoyi-ui/src/utils/generator/html.js new file mode 100644 index 0000000..9bcc536 --- /dev/null +++ b/ruoyi-ui/src/utils/generator/html.js @@ -0,0 +1,359 @@ +/* eslint-disable max-len */ +import { trigger } from './config' + +let confGlobal +let someSpanIsNot24 + +export function dialogWrapper(str) { + return ` + ${str} +
+ 取消 + 确定 +
+
` +} + +export function vueTemplate(str) { + return `` +} + +export function vueScript(str) { + return `` +} + +export function cssStyle(cssStr) { + return `` +} + +function buildFormTemplate(conf, child, type) { + let labelPosition = '' + if (conf.labelPosition !== 'right') { + labelPosition = `label-position="${conf.labelPosition}"` + } + const disabled = conf.disabled ? `:disabled="${conf.disabled}"` : '' + let str = ` + ${child} + ${buildFromBtns(conf, type)} + ` + if (someSpanIsNot24) { + str = ` + ${str} + ` + } + return str +} + +function buildFromBtns(conf, type) { + let str = '' + if (conf.formBtns && type === 'file') { + str = ` + 提交 + 重置 + ` + if (someSpanIsNot24) { + str = ` + ${str} + ` + } + } + return str +} + +// span不为24的用el-col包裹 +function colWrapper(element, str) { + if (someSpanIsNot24 || element.span !== 24) { + return ` + ${str} + ` + } + return str +} + +const layouts = { + colFormItem(element) { + let labelWidth = '' + if (element.labelWidth && element.labelWidth !== confGlobal.labelWidth) { + labelWidth = `label-width="${element.labelWidth}px"` + } + const required = !trigger[element.tag] && element.required ? 'required' : '' + const tagDom = tags[element.tag] ? tags[element.tag](element) : null + let str = ` + ${tagDom} + ` + str = colWrapper(element, str) + return str + }, + rowFormItem(element) { + const type = element.type === 'default' ? '' : `type="${element.type}"` + const justify = element.type === 'default' ? '' : `justify="${element.justify}"` + const align = element.type === 'default' ? '' : `align="${element.align}"` + const gutter = element.gutter ? `gutter="${element.gutter}"` : '' + const children = element.children.map(el => layouts[el.layout](el)) + let str = ` + ${children.join('\n')} + ` + str = colWrapper(element, str) + return str + } +} + +const tags = { + 'el-button': el => { + const { + tag, disabled + } = attrBuilder(el) + const type = el.type ? `type="${el.type}"` : '' + const icon = el.icon ? `icon="${el.icon}"` : '' + const size = el.size ? `size="${el.size}"` : '' + let child = buildElButtonChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${type} ${icon} ${size} ${disabled}>${child}` + }, + 'el-input': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : '' + const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : '' + const readonly = el.readonly ? 'readonly' : '' + const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : '' + const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : '' + const showPassword = el['show-password'] ? 'show-password' : '' + const type = el.type ? `type="${el.type}"` : '' + const autosize = el.autosize && el.autosize.minRows + ? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"` + : '' + let child = buildElInputChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}` + }, + 'el-input-number': el => { + const { disabled, vModel, placeholder } = attrBuilder(el) + const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : '' + const min = el.min ? `:min='${el.min}'` : '' + const max = el.max ? `:max='${el.max}'` : '' + const step = el.step ? `:step='${el.step}'` : '' + const stepStrictly = el['step-strictly'] ? 'step-strictly' : '' + const precision = el.precision ? `:precision='${el.precision}'` : '' + + return `<${el.tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}>` + }, + 'el-select': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const filterable = el.filterable ? 'filterable' : '' + const multiple = el.multiple ? 'multiple' : '' + let child = buildElSelectChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}` + }, + 'el-radio-group': el => { + const { disabled, vModel } = attrBuilder(el) + const size = `size="${el.size}"` + let child = buildElRadioGroupChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${size} ${disabled}>${child}` + }, + 'el-checkbox-group': el => { + const { disabled, vModel } = attrBuilder(el) + const size = `size="${el.size}"` + const min = el.min ? `:min="${el.min}"` : '' + const max = el.max ? `:max="${el.max}"` : '' + let child = buildElCheckboxGroupChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}` + }, + 'el-switch': el => { + const { disabled, vModel } = attrBuilder(el) + const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : '' + const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : '' + const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : '' + const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : '' + const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : '' + const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : '' + + return `<${el.tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}>` + }, + 'el-cascader': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const options = el.options ? `:options="${el.vModel}Options"` : '' + const props = el.props ? `:props="${el.vModel}Props"` : '' + const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"' + const filterable = el.filterable ? 'filterable' : '' + const separator = el.separator === '/' ? '' : `separator="${el.separator}"` + + return `<${el.tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}>` + }, + 'el-slider': el => { + const { disabled, vModel } = attrBuilder(el) + const min = el.min ? `:min='${el.min}'` : '' + const max = el.max ? `:max='${el.max}'` : '' + const step = el.step ? `:step='${el.step}'` : '' + const range = el.range ? 'range' : '' + const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : '' + + return `<${el.tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}>` + }, + 'el-time-picker': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : '' + const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : '' + const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : '' + const isRange = el['is-range'] ? 'is-range' : '' + const format = el.format ? `format="${el.format}"` : '' + const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : '' + const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : '' + + return `<${el.tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}>` + }, + 'el-date-picker': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : '' + const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : '' + const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : '' + const format = el.format ? `format="${el.format}"` : '' + const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : '' + const type = el.type === 'date' ? '' : `type="${el.type}"` + const readonly = el.readonly ? 'readonly' : '' + + return `<${el.tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}>` + }, + 'el-rate': el => { + const { disabled, vModel } = attrBuilder(el) + const max = el.max ? `:max='${el.max}'` : '' + const allowHalf = el['allow-half'] ? 'allow-half' : '' + const showText = el['show-text'] ? 'show-text' : '' + const showScore = el['show-score'] ? 'show-score' : '' + + return `<${el.tag} ${vModel} ${allowHalf} ${showText} ${showScore} ${disabled}>` + }, + 'el-color-picker': el => { + const { disabled, vModel } = attrBuilder(el) + const size = `size="${el.size}"` + const showAlpha = el['show-alpha'] ? 'show-alpha' : '' + const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : '' + + return `<${el.tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}>` + }, + 'el-upload': el => { + const disabled = el.disabled ? ':disabled=\'true\'' : '' + const action = el.action ? `:action="${el.vModel}Action"` : '' + const multiple = el.multiple ? 'multiple' : '' + const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : '' + const accept = el.accept ? `accept="${el.accept}"` : '' + const name = el.name !== 'file' ? `name="${el.name}"` : '' + const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : '' + const beforeUpload = `:before-upload="${el.vModel}BeforeUpload"` + const fileList = `:file-list="${el.vModel}fileList"` + const ref = `ref="${el.vModel}"` + let child = buildElUploadChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}` + } +} + +function attrBuilder(el) { + return { + vModel: `v-model="${confGlobal.formModel}.${el.vModel}"`, + clearable: el.clearable ? 'clearable' : '', + placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '', + width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '', + disabled: el.disabled ? ':disabled=\'true\'' : '' + } +} + +// el-buttin 子级 +function buildElButtonChild(conf) { + const children = [] + if (conf.default) { + children.push(conf.default) + } + return children.join('\n') +} + +// el-input innerHTML +function buildElInputChild(conf) { + const children = [] + if (conf.prepend) { + children.push(``) + } + if (conf.append) { + children.push(``) + } + return children.join('\n') +} + +function buildElSelectChild(conf) { + const children = [] + if (conf.options && conf.options.length) { + children.push(``) + } + return children.join('\n') +} + +function buildElRadioGroupChild(conf) { + const children = [] + if (conf.options && conf.options.length) { + const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio' + const border = conf.border ? 'border' : '' + children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}`) + } + return children.join('\n') +} + +function buildElCheckboxGroupChild(conf) { + const children = [] + if (conf.options && conf.options.length) { + const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox' + const border = conf.border ? 'border' : '' + children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}`) + } + return children.join('\n') +} + +function buildElUploadChild(conf) { + const list = [] + if (conf['list-type'] === 'picture-card') list.push('') + else list.push(`${conf.buttonText}`) + if (conf.showTip) list.push(`
只能上传不超过 ${conf.fileSize}${conf.sizeUnit} 的${conf.accept}文件
`) + return list.join('\n') +} + +export function makeUpHtml(conf, type) { + const htmlList = [] + confGlobal = conf + someSpanIsNot24 = conf.fields.some(item => item.span !== 24) + conf.fields.forEach(el => { + htmlList.push(layouts[el.layout](el)) + }) + const htmlStr = htmlList.join('\n') + + let temp = buildFormTemplate(conf, htmlStr, type) + if (type === 'dialog') { + temp = dialogWrapper(temp) + } + confGlobal = null + return temp +} diff --git a/ruoyi-ui/src/utils/generator/icon.json b/ruoyi-ui/src/utils/generator/icon.json new file mode 100644 index 0000000..2d9999a --- /dev/null +++ b/ruoyi-ui/src/utils/generator/icon.json @@ -0,0 +1 @@ +["platform-eleme","eleme","delete-solid","delete","s-tools","setting","user-solid","user","phone","phone-outline","more","more-outline","star-on","star-off","s-goods","goods","warning","warning-outline","question","info","remove","circle-plus","success","error","zoom-in","zoom-out","remove-outline","circle-plus-outline","circle-check","circle-close","s-help","help","minus","plus","check","close","picture","picture-outline","picture-outline-round","upload","upload2","download","camera-solid","camera","video-camera-solid","video-camera","message-solid","bell","s-cooperation","s-order","s-platform","s-fold","s-unfold","s-operation","s-promotion","s-home","s-release","s-ticket","s-management","s-open","s-shop","s-marketing","s-flag","s-comment","s-finance","s-claim","s-custom","s-opportunity","s-data","s-check","s-grid","menu","share","d-caret","caret-left","caret-right","caret-bottom","caret-top","bottom-left","bottom-right","back","right","bottom","top","top-left","top-right","arrow-left","arrow-right","arrow-down","arrow-up","d-arrow-left","d-arrow-right","video-pause","video-play","refresh","refresh-right","refresh-left","finished","sort","sort-up","sort-down","rank","loading","view","c-scale-to-original","date","edit","edit-outline","folder","folder-opened","folder-add","folder-remove","folder-delete","folder-checked","tickets","document-remove","document-delete","document-copy","document-checked","document","document-add","printer","paperclip","takeaway-box","search","monitor","attract","mobile","scissors","umbrella","headset","brush","mouse","coordinate","magic-stick","reading","data-line","data-board","pie-chart","data-analysis","collection-tag","film","suitcase","suitcase-1","receiving","collection","files","notebook-1","notebook-2","toilet-paper","office-building","school","table-lamp","house","no-smoking","smoking","shopping-cart-full","shopping-cart-1","shopping-cart-2","shopping-bag-1","shopping-bag-2","sold-out","sell","present","box","bank-card","money","coin","wallet","discount","price-tag","news","guide","male","female","thumb","cpu","link","connection","open","turn-off","set-up","chat-round","chat-line-round","chat-square","chat-dot-round","chat-dot-square","chat-line-square","message","postcard","position","turn-off-microphone","microphone","close-notification","bangzhu","time","odometer","crop","aim","switch-button","full-screen","copy-document","mic","stopwatch","medal-1","medal","trophy","trophy-1","first-aid-kit","discover","place","location","location-outline","location-information","add-location","delete-location","map-location","alarm-clock","timer","watch-1","watch","lock","unlock","key","service","mobile-phone","bicycle","truck","ship","basketball","football","soccer","baseball","wind-power","light-rain","lightning","heavy-rain","sunrise","sunrise-1","sunset","sunny","cloudy","partly-cloudy","cloudy-and-sunny","moon","moon-night","dish","dish-1","food","chicken","fork-spoon","knife-fork","burger","tableware","sugar","dessert","ice-cream","hot-water","water-cup","coffee-cup","cold-drink","goblet","goblet-full","goblet-square","goblet-square-full","refrigerator","grape","watermelon","cherry","apple","pear","orange","coffee","ice-tea","ice-drink","milk-tea","potato-strips","lollipop","ice-cream-square","ice-cream-round"] \ No newline at end of file diff --git a/ruoyi-ui/src/utils/generator/js.js b/ruoyi-ui/src/utils/generator/js.js new file mode 100644 index 0000000..ee8668d --- /dev/null +++ b/ruoyi-ui/src/utils/generator/js.js @@ -0,0 +1,235 @@ +import { exportDefault, titleCase } from '@/utils/index' +import { trigger } from './config' + +const units = { + KB: '1024', + MB: '1024 / 1024', + GB: '1024 / 1024 / 1024' +} +let confGlobal +const inheritAttrs = { + file: '', + dialog: 'inheritAttrs: false,' +} + + +export function makeUpJs(conf, type) { + confGlobal = conf = JSON.parse(JSON.stringify(conf)) + const dataList = [] + const ruleList = [] + const optionsList = [] + const propsList = [] + const methodList = mixinMethod(type) + const uploadVarList = [] + + conf.fields.forEach(el => { + buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList) + }) + + const script = buildexport( + conf, + type, + dataList.join('\n'), + ruleList.join('\n'), + optionsList.join('\n'), + uploadVarList.join('\n'), + propsList.join('\n'), + methodList.join('\n') + ) + confGlobal = null + return script +} + +function buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList) { + buildData(el, dataList) + buildRules(el, ruleList) + + if (el.options && el.options.length) { + buildOptions(el, optionsList) + if (el.dataType === 'dynamic') { + const model = `${el.vModel}Options` + const options = titleCase(model) + buildOptionMethod(`get${options}`, model, methodList) + } + } + + if (el.props && el.props.props) { + buildProps(el, propsList) + } + + if (el.action && el.tag === 'el-upload') { + uploadVarList.push( + `${el.vModel}Action: '${el.action}', + ${el.vModel}fileList: [],` + ) + methodList.push(buildBeforeUpload(el)) + if (!el['auto-upload']) { + methodList.push(buildSubmitUpload(el)) + } + } + + if (el.children) { + el.children.forEach(el2 => { + buildAttributes(el2, dataList, ruleList, optionsList, methodList, propsList, uploadVarList) + }) + } +} + +function mixinMethod(type) { + const list = []; const + minxins = { + file: confGlobal.formBtns ? { + submitForm: `submitForm() { + this.$refs['${confGlobal.formRef}'].validate(valid => { + if(!valid) return + // TODO 提交表单 + }) + },`, + resetForm: `resetForm() { + this.$refs['${confGlobal.formRef}'].resetFields() + },` + } : null, + dialog: { + onOpen: 'onOpen() {},', + onClose: `onClose() { + this.$refs['${confGlobal.formRef}'].resetFields() + },`, + close: `close() { + this.$emit('update:visible', false) + },`, + handleConfirm: `handleConfirm() { + this.$refs['${confGlobal.formRef}'].validate(valid => { + if(!valid) return + this.close() + }) + },` + } + } + + const methods = minxins[type] + if (methods) { + Object.keys(methods).forEach(key => { + list.push(methods[key]) + }) + } + + return list +} + +function buildData(conf, dataList) { + if (conf.vModel === undefined) return + let defaultValue + if (typeof (conf.defaultValue) === 'string' && !conf.multiple) { + defaultValue = `'${conf.defaultValue}'` + } else { + defaultValue = `${JSON.stringify(conf.defaultValue)}` + } + dataList.push(`${conf.vModel}: ${defaultValue},`) +} + +function buildRules(conf, ruleList) { + if (conf.vModel === undefined) return + const rules = [] + if (trigger[conf.tag]) { + if (conf.required) { + const type = Array.isArray(conf.defaultValue) ? 'type: \'array\',' : '' + let message = Array.isArray(conf.defaultValue) ? `请至少选择一个${conf.vModel}` : conf.placeholder + if (message === undefined) message = `${conf.label}不能为空` + rules.push(`{ required: true, ${type} message: '${message}', trigger: '${trigger[conf.tag]}' }`) + } + if (conf.regList && Array.isArray(conf.regList)) { + conf.regList.forEach(item => { + if (item.pattern) { + rules.push(`{ pattern: ${eval(item.pattern)}, message: '${item.message}', trigger: '${trigger[conf.tag]}' }`) + } + }) + } + ruleList.push(`${conf.vModel}: [${rules.join(',')}],`) + } +} + +function buildOptions(conf, optionsList) { + if (conf.vModel === undefined) return + if (conf.dataType === 'dynamic') { conf.options = [] } + const str = `${conf.vModel}Options: ${JSON.stringify(conf.options)},` + optionsList.push(str) +} + +function buildProps(conf, propsList) { + if (conf.dataType === 'dynamic') { + conf.valueKey !== 'value' && (conf.props.props.value = conf.valueKey) + conf.labelKey !== 'label' && (conf.props.props.label = conf.labelKey) + conf.childrenKey !== 'children' && (conf.props.props.children = conf.childrenKey) + } + const str = `${conf.vModel}Props: ${JSON.stringify(conf.props.props)},` + propsList.push(str) +} + +function buildBeforeUpload(conf) { + const unitNum = units[conf.sizeUnit]; let rightSizeCode = ''; let acceptCode = ''; const + returnList = [] + if (conf.fileSize) { + rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${conf.fileSize} + if(!isRightSize){ + this.$message.error('文件大小超过 ${conf.fileSize}${conf.sizeUnit}') + }` + returnList.push('isRightSize') + } + if (conf.accept) { + acceptCode = `let isAccept = new RegExp('${conf.accept}').test(file.type) + if(!isAccept){ + this.$message.error('应该选择${conf.accept}类型的文件') + }` + returnList.push('isAccept') + } + const str = `${conf.vModel}BeforeUpload(file) { + ${rightSizeCode} + ${acceptCode} + return ${returnList.join('&&')} + },` + return returnList.length ? str : '' +} + +function buildSubmitUpload(conf) { + const str = `submitUpload() { + this.$refs['${conf.vModel}'].submit() + },` + return str +} + +function buildOptionMethod(methodName, model, methodList) { + const str = `${methodName}() { + // TODO 发起请求获取数据 + this.${model} + },` + methodList.push(str) +} + +function buildexport(conf, type, data, rules, selectOptions, uploadVar, props, methods) { + const str = `${exportDefault}{ + ${inheritAttrs[type]} + components: {}, + props: [], + data () { + return { + ${conf.formModel}: { + ${data} + }, + ${conf.formRules}: { + ${rules} + }, + ${uploadVar} + ${selectOptions} + ${props} + } + }, + computed: {}, + watch: {}, + created () {}, + mounted () {}, + methods: { + ${methods} + } +}` + return str +} diff --git a/ruoyi-ui/src/utils/generator/render.js b/ruoyi-ui/src/utils/generator/render.js new file mode 100644 index 0000000..e8640f0 --- /dev/null +++ b/ruoyi-ui/src/utils/generator/render.js @@ -0,0 +1,126 @@ +import { makeMap } from '@/utils/index' + +// 参考https://github.com/vuejs/vue/blob/v2.6.10/src/platforms/web/server/util.js +const isAttr = makeMap( + 'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' + + 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' + + 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' + + 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' + + 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' + + 'form,formaction,headers,height,hidden,high,href,hreflang,http-equiv,' + + 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' + + 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' + + 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' + + 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' + + 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' + + 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' + + 'target,title,type,usemap,value,width,wrap' +) + +function vModel(self, dataObject, defaultValue) { + dataObject.props.value = defaultValue + + dataObject.on.input = val => { + self.$emit('input', val) + } +} + +const componentChild = { + 'el-button': { + default(h, conf, key) { + return conf[key] + }, + }, + 'el-input': { + prepend(h, conf, key) { + return + }, + append(h, conf, key) { + return + } + }, + 'el-select': { + options(h, conf, key) { + const list = [] + conf.options.forEach(item => { + list.push() + }) + return list + } + }, + 'el-radio-group': { + options(h, conf, key) { + const list = [] + conf.options.forEach(item => { + if (conf.optionType === 'button') list.push({item.label}) + else list.push({item.label}) + }) + return list + } + }, + 'el-checkbox-group': { + options(h, conf, key) { + const list = [] + conf.options.forEach(item => { + if (conf.optionType === 'button') { + list.push({item.label}) + } else { + list.push({item.label}) + } + }) + return list + } + }, + 'el-upload': { + 'list-type': (h, conf, key) => { + const list = [] + if (conf['list-type'] === 'picture-card') { + list.push() + } else { + list.push({conf.buttonText}) + } + if (conf.showTip) { + list.push(
只能上传不超过 {conf.fileSize}{conf.sizeUnit} 的{conf.accept}文件
) + } + return list + } + } +} + +export default { + render(h) { + const dataObject = { + attrs: {}, + props: {}, + on: {}, + style: {} + } + const confClone = JSON.parse(JSON.stringify(this.conf)) + const children = [] + + const childObjs = componentChild[confClone.tag] + if (childObjs) { + Object.keys(childObjs).forEach(key => { + const childFunc = childObjs[key] + if (confClone[key]) { + children.push(childFunc(h, confClone, key)) + } + }) + } + + Object.keys(confClone).forEach(key => { + const val = confClone[key] + if (key === 'vModel') { + vModel(this, dataObject, confClone.defaultValue) + } else if (dataObject[key]) { + dataObject[key] = val + } else if (!isAttr(key)) { + dataObject.props[key] = val + } else { + dataObject.attrs[key] = val + } + }) + return h(this.conf.tag, dataObject, children) + }, + props: ['conf'] +} diff --git a/ruoyi-ui/src/utils/index.js b/ruoyi-ui/src/utils/index.js new file mode 100644 index 0000000..4e65504 --- /dev/null +++ b/ruoyi-ui/src/utils/index.js @@ -0,0 +1,390 @@ +import { parseTime } from './ruoyi' + +/** + * 表格时间格式化 + */ +export function formatDate(cellValue) { + if (cellValue == null || cellValue == "") return ""; + var date = new Date(cellValue) + var year = date.getFullYear() + var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1 + var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() + var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() + var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() + var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() + return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds +} + +/** + * @param {number} time + * @param {string} option + * @returns {string} + */ +export function formatTime(time, option) { + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } + const d = new Date(time) + const now = Date.now() + + const diff = (now - d) / 1000 + + if (diff < 30) { + return '刚刚' + } else if (diff < 3600) { + // less 1 hour + return Math.ceil(diff / 60) + '分钟前' + } else if (diff < 3600 * 24) { + return Math.ceil(diff / 3600) + '小时前' + } else if (diff < 3600 * 24 * 2) { + return '1天前' + } + if (option) { + return parseTime(time, option) + } else { + return ( + d.getMonth() + + 1 + + '月' + + d.getDate() + + '日' + + d.getHours() + + '时' + + d.getMinutes() + + '分' + ) + } +} + +/** + * @param {string} url + * @returns {Object} + */ +export function getQueryObject(url) { + url = url == null ? window.location.href : url + const search = url.substring(url.lastIndexOf('?') + 1) + const obj = {} + const reg = /([^?&=]+)=([^?&=]*)/g + search.replace(reg, (rs, $1, $2) => { + const name = decodeURIComponent($1) + let val = decodeURIComponent($2) + val = String(val) + obj[name] = val + return rs + }) + return obj +} + +/** + * @param {string} input value + * @returns {number} output value + */ +export function byteLength(str) { + // returns the byte length of an utf8 string + let s = str.length + for (var i = str.length - 1; i >= 0; i--) { + const code = str.charCodeAt(i) + if (code > 0x7f && code <= 0x7ff) s++ + else if (code > 0x7ff && code <= 0xffff) s += 2 + if (code >= 0xDC00 && code <= 0xDFFF) i-- + } + return s +} + +/** + * @param {Array} actual + * @returns {Array} + */ +export function cleanArray(actual) { + const newArray = [] + for (let i = 0; i < actual.length; i++) { + if (actual[i]) { + newArray.push(actual[i]) + } + } + return newArray +} + +/** + * @param {Object} json + * @returns {Array} + */ +export function param(json) { + if (!json) return '' + return cleanArray( + Object.keys(json).map(key => { + if (json[key] === undefined) return '' + return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]) + }) + ).join('&') +} + +/** + * @param {string} url + * @returns {Object} + */ +export function param2Obj(url) { + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') + if (!search) { + return {} + } + const obj = {} + const searchArr = search.split('&') + searchArr.forEach(v => { + const index = v.indexOf('=') + if (index !== -1) { + const name = v.substring(0, index) + const val = v.substring(index + 1, v.length) + obj[name] = val + } + }) + return obj +} + +/** + * @param {string} val + * @returns {string} + */ +export function html2Text(val) { + const div = document.createElement('div') + div.innerHTML = val + return div.textContent || div.innerText +} + +/** + * Merges two objects, giving the last one precedence + * @param {Object} target + * @param {(Object|Array)} source + * @returns {Object} + */ +export function objectMerge(target, source) { + if (typeof target !== 'object') { + target = {} + } + if (Array.isArray(source)) { + return source.slice() + } + Object.keys(source).forEach(property => { + const sourceProperty = source[property] + if (typeof sourceProperty === 'object') { + target[property] = objectMerge(target[property], sourceProperty) + } else { + target[property] = sourceProperty + } + }) + return target +} + +/** + * @param {HTMLElement} element + * @param {string} className + */ +export function toggleClass(element, className) { + if (!element || !className) { + return + } + let classString = element.className + const nameIndex = classString.indexOf(className) + if (nameIndex === -1) { + classString += '' + className + } else { + classString = + classString.substr(0, nameIndex) + + classString.substr(nameIndex + className.length) + } + element.className = classString +} + +/** + * @param {string} type + * @returns {Date} + */ +export function getTime(type) { + if (type === 'start') { + return new Date().getTime() - 3600 * 1000 * 24 * 90 + } else { + return new Date(new Date().toDateString()) + } +} + +/** + * @param {Function} func + * @param {number} wait + * @param {boolean} immediate + * @return {*} + */ +export function debounce(func, wait, immediate) { + let timeout, args, context, timestamp, result + + const later = function() { + // 据上一次触发时间间隔 + const last = +new Date() - timestamp + + // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait + if (last < wait && last > 0) { + timeout = setTimeout(later, wait - last) + } else { + timeout = null + // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用 + if (!immediate) { + result = func.apply(context, args) + if (!timeout) context = args = null + } + } + } + + return function(...args) { + context = this + timestamp = +new Date() + const callNow = immediate && !timeout + // 如果延时不存在,重新设定延时 + if (!timeout) timeout = setTimeout(later, wait) + if (callNow) { + result = func.apply(context, args) + context = args = null + } + + return result + } +} + +/** + * This is just a simple version of deep copy + * Has a lot of edge cases bug + * If you want to use a perfect deep copy, use lodash's _.cloneDeep + * @param {Object} source + * @returns {Object} + */ +export function deepClone(source) { + if (!source && typeof source !== 'object') { + throw new Error('error arguments', 'deepClone') + } + const targetObj = source.constructor === Array ? [] : {} + Object.keys(source).forEach(keys => { + if (source[keys] && typeof source[keys] === 'object') { + targetObj[keys] = deepClone(source[keys]) + } else { + targetObj[keys] = source[keys] + } + }) + return targetObj +} + +/** + * @param {Array} arr + * @returns {Array} + */ +export function uniqueArr(arr) { + return Array.from(new Set(arr)) +} + +/** + * @returns {string} + */ +export function createUniqueString() { + const timestamp = +new Date() + '' + const randomNum = parseInt((1 + Math.random()) * 65536) + '' + return (+(randomNum + timestamp)).toString(32) +} + +/** + * Check if an element has a class + * @param {HTMLElement} elm + * @param {string} cls + * @returns {boolean} + */ +export function hasClass(ele, cls) { + return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) +} + +/** + * Add class to element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function addClass(ele, cls) { + if (!hasClass(ele, cls)) ele.className += ' ' + cls +} + +/** + * Remove class from element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function removeClass(ele, cls) { + if (hasClass(ele, cls)) { + const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)') + ele.className = ele.className.replace(reg, ' ') + } +} + +export function makeMap(str, expectsLowerCase) { + const map = Object.create(null) + const list = str.split(',') + for (let i = 0; i < list.length; i++) { + map[list[i]] = true + } + return expectsLowerCase + ? val => map[val.toLowerCase()] + : val => map[val] +} + +export const exportDefault = 'export default ' + +export const beautifierConf = { + html: { + indent_size: '2', + indent_char: ' ', + max_preserve_newlines: '-1', + preserve_newlines: false, + keep_array_indentation: false, + break_chained_methods: false, + indent_scripts: 'separate', + brace_style: 'end-expand', + space_before_conditional: true, + unescape_strings: false, + jslint_happy: false, + end_with_newline: true, + wrap_line_length: '110', + indent_inner_html: true, + comma_first: false, + e4x: true, + indent_empty_lines: true + }, + js: { + indent_size: '2', + indent_char: ' ', + max_preserve_newlines: '-1', + preserve_newlines: false, + keep_array_indentation: false, + break_chained_methods: false, + indent_scripts: 'normal', + brace_style: 'end-expand', + space_before_conditional: true, + unescape_strings: false, + jslint_happy: true, + end_with_newline: true, + wrap_line_length: '110', + indent_inner_html: true, + comma_first: false, + e4x: true, + indent_empty_lines: true + } +} + +// 首字母大小 +export function titleCase(str) { + return str.replace(/( |^)[a-z]/g, L => L.toUpperCase()) +} + +// 下划转驼峰 +export function camelCase(str) { + return str.replace(/_[a-z]/g, str1 => str1.substr(-1).toUpperCase()) +} + +export function isNumberStr(str) { + return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str) +} + diff --git a/ruoyi-ui/src/utils/jsencrypt.js b/ruoyi-ui/src/utils/jsencrypt.js new file mode 100644 index 0000000..78d9523 --- /dev/null +++ b/ruoyi-ui/src/utils/jsencrypt.js @@ -0,0 +1,30 @@ +import JSEncrypt from 'jsencrypt/bin/jsencrypt.min' + +// 密钥对生成 http://web.chacuo.net/netrsakeypair + +const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' + +const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' + + '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' + + 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' + + 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' + + 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' + + 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' + + 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' + + 'UP8iWi1Qw0Y=' + +// 加密 +export function encrypt(txt) { + const encryptor = new JSEncrypt() + encryptor.setPublicKey(publicKey) // 设置公钥 + return encryptor.encrypt(txt) // 对数据进行加密 +} + +// 解密 +export function decrypt(txt) { + const encryptor = new JSEncrypt() + encryptor.setPrivateKey(privateKey) // 设置私钥 + return encryptor.decrypt(txt) // 对数据进行解密 +} + diff --git a/ruoyi-ui/src/utils/operationAudit.js b/ruoyi-ui/src/utils/operationAudit.js new file mode 100644 index 0000000..9bbfc57 --- /dev/null +++ b/ruoyi-ui/src/utils/operationAudit.js @@ -0,0 +1,39 @@ +const states = { + unchanged: '已核对,业务字段没有变化', + unavailable: '未能取得完整的修改前后数据', + failed: '操作失败,未确认生效的改动', + notCaptured: '此操作未记录字段对比' +} + +function toParamRows(params) { + if (!params || typeof params !== 'object') return [] + return Object.keys(params).map(key => ({ label: key, value: String(params[key]) })) +} + +export function parseOperationAudit(text) { + const empty = { action: '', target: '', changes: [], params: [], notice: '', structured: false } + if (!text) return empty + try { + const data = JSON.parse(text) + if (data && data.auditVersion === 1) { + const target = data.target || {} + const changes = data.state === 'changed' && Array.isArray(data.changes) + ? data.changes.filter(c => c && typeof c.label === 'string' && typeof c.before === 'string' && typeof c.after === 'string') : [] + return { + action: typeof data.action === 'string' ? data.action : '操作', + target: target.id == null ? '' : '用户ID:' + target.id + (typeof target.name === 'string' && target.name ? '(' + target.name + ')' : ''), + changes, + params: toParamRows(data.params), + notice: [states[data.state] || '', data.truncated ? '内容超出存储上限,部分改动未展示' : ''].filter(Boolean).join(';'), + structured: true + } + } + return { ...empty, action: '历史记录', notice: '历史记录未保存可验证的前后值' } + } catch (e) { /* Legacy action strings are not JSON. */ } + if (!text.startsWith('点击【')) return { ...empty, action: '历史记录', notice: '历史记录未保存可验证的前后值' } + const separator = text.indexOf('|') + if (separator < 0) return { ...empty, action: text, notice: '此记录未保存字段对比' } + const changes = text.slice(separator + 1).split('\n').map(line => line.split('\t')) + .filter(parts => parts.length === 3).map(parts => ({ label: parts[0], before: parts[1], after: parts[2] })) + return { ...empty, action: text.slice(0, separator), changes, notice: changes.length ? '' : '历史记录未保存可验证的前后值' } +} diff --git a/ruoyi-ui/src/utils/permission.js b/ruoyi-ui/src/utils/permission.js new file mode 100644 index 0000000..1730e33 --- /dev/null +++ b/ruoyi-ui/src/utils/permission.js @@ -0,0 +1,51 @@ +import store from '@/store' + +/** + * 字符权限校验 + * @param {Array} value 校验值 + * @returns {Boolean} + */ +export function checkPermi(value) { + if (value && value instanceof Array && value.length > 0) { + const permissions = store.getters && store.getters.permissions + const permissionDatas = value + const all_permission = "*:*:*"; + + const hasPermission = permissions.some(permission => { + return all_permission === permission || permissionDatas.includes(permission) + }) + + if (!hasPermission) { + return false + } + return true + } else { + console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`) + return false + } +} + +/** + * 角色权限校验 + * @param {Array} value 校验值 + * @returns {Boolean} + */ +export function checkRole(value) { + if (value && value instanceof Array && value.length > 0) { + const roles = store.getters && store.getters.roles + const permissionRoles = value + const super_admin = "admin"; + + const hasRole = roles.some(role => { + return super_admin === role || permissionRoles.includes(role) + }) + + if (!hasRole) { + return false + } + return true + } else { + console.error(`need roles! Like checkRole="['admin','editor']"`) + return false + } +} \ No newline at end of file diff --git a/ruoyi-ui/src/utils/request.js b/ruoyi-ui/src/utils/request.js new file mode 100644 index 0000000..4df3e31 --- /dev/null +++ b/ruoyi-ui/src/utils/request.js @@ -0,0 +1,184 @@ +import axios from 'axios' +import { Notification, MessageBox, Message, Loading } from 'element-ui' +import store from '@/store' +import { getToken } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { tansParams, blobValidate } from "@/utils/ruoyi"; +import cache from '@/plugins/cache' +import { saveAs } from 'file-saver' + +let downloadLoadingInstance; +// 是否显示重新登录 +export let isRelogin = { show: false }; + +// ---------- 标签切换防抖:检测请求是否由用户主动操作触发 ---------- +// 用户点击(搜索、分页)或按键(Enter搜索)标记为用户主动操作 +// 注意:顶部标签栏的点击不算"用户主动操作",因为那是触发导航的点击 +let _isUserClick = false +let _clickTimer = null +function markUserAction() { + _isUserClick = true + if (_clickTimer) clearTimeout(_clickTimer) + _clickTimer = setTimeout(() => { _isUserClick = false }, 500) +} +document.addEventListener('click', (e) => { + const tagsView = document.getElementById('tags-view-container') + if (tagsView && tagsView.contains(e.target)) return + markUserAction() +}, true) +document.addEventListener('keydown', (e) => { + if (e.key === 'Enter') markUserAction() +}, true) + +axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' +// 创建axios实例 +const service = axios.create({ + // axios中请求配置有baseURL选项,表示请求URL公共部分 + baseURL: process.env.VUE_APP_BASE_API, + // 超时 + timeout: 10000 +}) + +// request拦截器 +service.interceptors.request.use(config => { + // 是否需要设置 token + const isToken = (config.headers || {}).isToken === false + // 是否需要防止数据重复提交 + const isRepeatSubmit = (config.headers || {}).repeatSubmit === false + if (getToken() && !isToken) { + config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 + } + // 顶部标签切换后,跳过所有非用户主动触发的请求 + // 用户手动点击搜索/分页等操作触发 _isUserClick,请求正常放行 + if (store.state.tagsView.fromTagsView && !_isUserClick) { + console.log('[tagSwitch] 拦截请求:', config.method, config.url, 'fromTagsView:', store.state.tagsView.fromTagsView, 'isUserClick:', _isUserClick) + return Promise.reject(new Error('tagSwitchSkip')) + } + if (store.state.tagsView.fromTagsView) { + console.log('[tagSwitch] 放行请求(用户操作):', config.method, config.url) + } + // get请求映射params参数 + if (config.method === 'get' && config.params) { + let url = config.url + '?' + tansParams(config.params); + url = url.slice(0, -1); + config.params = {}; + config.url = url; + } + if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { + const requestObj = { + url: config.url, + data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, + time: new Date().getTime() + } + const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小 + const limitSize = 5 * 1024 * 1024; // 限制存放数据5M + if (requestSize >= limitSize) { + console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。') + return config; + } + const sessionObj = cache.session.getJSON('sessionObj') + if (sessionObj === undefined || sessionObj === null || sessionObj === '') { + cache.session.setJSON('sessionObj', requestObj) + } else { + const s_url = sessionObj.url; // 请求地址 + const s_data = sessionObj.data; // 请求数据 + const s_time = sessionObj.time; // 请求时间 + const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交 + if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { + const message = '数据正在处理,请勿重复提交'; + console.warn(`[${s_url}]: ` + message) + return Promise.reject(new Error(message)) + } else { + cache.session.setJSON('sessionObj', requestObj) + } + } + } + return config +}, error => { + console.log(error) + Promise.reject(error) +}) + +// 响应拦截器 +service.interceptors.response.use(res => { + // 未设置状态码则默认成功状态 + const code = res.data.code || 200; + // 获取错误信息 + const msg = errorCode[code] || res.data.msg || errorCode['default'] + // 二进制数据则直接返回 + if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') { + return res.data + } + if (code === 401) { + if (!isRelogin.show) { + isRelogin.show = true; + MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { + isRelogin.show = false; + store.dispatch('LogOut').then(() => { + location.href = '/#/login'; + }) + }).catch(() => { + isRelogin.show = false; + }); + } + return Promise.reject('无效的会话,或者会话已过期,请重新登录。') + } else if (code === 500) { + Message({ message: msg, type: 'error' }) + return Promise.reject(new Error(msg)) + } else if (code === 601) { + Message({ message: msg, type: 'warning' }) + return Promise.reject('error') + } else if (code !== 200) { + Notification.error({ title: msg }) + return Promise.reject('error') + } else { + return res.data + } + }, + error => { + // 顶部标签切换时跳过的自动请求,不提示错误 + if (error && error.message === 'tagSwitchSkip') { + return Promise.reject(error) + } + console.log('err' + error) + let { message } = error; + if (message == "Network Error") { + message = "后端接口连接异常"; + } else if (message.includes("timeout")) { + message = "系统接口请求超时"; + } else if (message.includes("Request failed with status code")) { + message = "系统接口" + message.substr(message.length - 3) + "异常"; + } + Message({ message: message, type: 'error', duration: 5 * 1000 }) + return Promise.reject(error) + } +) + +// 通用下载方法 +export function download(url, params, filename, config) { + downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", }) + return service.post(url, params, { + transformRequest: [(params) => { return tansParams(params) }], + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + responseType: 'blob', + ...config + }).then(async (data) => { + const isBlob = blobValidate(data); + if (isBlob) { + const blob = new Blob([data]) + saveAs(blob, filename) + } else { + const resText = await data.text(); + const rspObj = JSON.parse(resText); + const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] + Message.error(errMsg); + } + downloadLoadingInstance.close(); + }).catch((r) => { + console.error(r) + Message.error('下载文件出现错误,请联系管理员!') + downloadLoadingInstance.close(); + }) +} + +export default service diff --git a/ruoyi-ui/src/utils/ruoyi.js b/ruoyi-ui/src/utils/ruoyi.js new file mode 100644 index 0000000..44bf9c4 --- /dev/null +++ b/ruoyi-ui/src/utils/ruoyi.js @@ -0,0 +1,233 @@ + + +/** + * 通用js方法封装处理 + * Copyright (c) 2019 ruoyi + */ + +// 日期格式化 +export function parseTime(time, pattern) { + if (arguments.length === 0 || !time) { + return null + } + const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), ''); + } + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} + +// 表单重置 +export function resetForm(refName) { + if (this.$refs[refName]) { + this.$refs[refName].resetFields(); + } +} + +// 添加日期范围 +export function addDateRange(params, dateRange, propName) { + let search = params; + search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {}; + dateRange = Array.isArray(dateRange) ? dateRange : []; + if (typeof (propName) === 'undefined') { + search.params['beginTime'] = dateRange[0]; + search.params['endTime'] = dateRange[1]; + } else { + search.params['begin' + propName] = dateRange[0]; + search.params['end' + propName] = dateRange[1]; + } + return search; +} + +// 回显数据字典 +export function selectDictLabel(datas, value) { + if (value === undefined) { + return ""; + } + var actions = []; + Object.keys(datas).some((key) => { + if (datas[key].value == ('' + value)) { + actions.push(datas[key].label); + return true; + } + }) + if (actions.length === 0) { + actions.push(value); + } + return actions.join(''); +} + +// 回显数据字典(字符串、数组) +export function selectDictLabels(datas, value, separator) { + if (value === undefined || value.length ===0) { + return ""; + } + if (Array.isArray(value)) { + value = value.join(","); + } + var actions = []; + var currentSeparator = undefined === separator ? "," : separator; + var temp = value.split(currentSeparator); + Object.keys(value.split(currentSeparator)).some((val) => { + var match = false; + Object.keys(datas).some((key) => { + if (datas[key].value == ('' + temp[val])) { + actions.push(datas[key].label + currentSeparator); + match = true; + } + }) + if (!match) { + actions.push(temp[val] + currentSeparator); + } + }) + return actions.join('').substring(0, actions.join('').length - 1); +} + +// 字符串格式化(%s ) +export function sprintf(str) { + var args = arguments, flag = true, i = 1; + str = str.replace(/%s/g, function () { + var arg = args[i++]; + if (typeof arg === 'undefined') { + flag = false; + return ''; + } + return arg; + }); + return flag ? str : ''; +} + +// 转换字符串,undefined,null等转化为"" +export function parseStrEmpty(str) { + if (!str || str == "undefined" || str == "null") { + return ""; + } + return str; +} + +// 数据合并 +export function mergeRecursive(source, target) { + for (var p in target) { + try { + if (target[p].constructor == Object) { + source[p] = mergeRecursive(source[p], target[p]); + } else { + source[p] = target[p]; + } + } catch (e) { + source[p] = target[p]; + } + } + return source; +}; + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + */ +export function handleTree(data, id, parentId, children) { + let config = { + id: id || 'id', + parentId: parentId || 'parentId', + childrenList: children || 'children' + }; + + var childrenListMap = {}; + var nodeIds = {}; + var tree = []; + + for (let d of data) { + let parentId = d[config.parentId]; + if (childrenListMap[parentId] == null) { + childrenListMap[parentId] = []; + } + nodeIds[d[config.id]] = d; + childrenListMap[parentId].push(d); + } + + for (let d of data) { + let parentId = d[config.parentId]; + if (nodeIds[parentId] == null) { + tree.push(d); + } + } + + for (let t of tree) { + adaptToChildrenList(t); + } + + function adaptToChildrenList(o) { + if (childrenListMap[o[config.id]] !== null) { + o[config.childrenList] = childrenListMap[o[config.id]]; + } + if (o[config.childrenList]) { + for (let c of o[config.childrenList]) { + adaptToChildrenList(c); + } + } + } + return tree; +} + +/** +* 参数处理 +* @param {*} params 参数 +*/ +export function tansParams(params) { + let result = '' + for (const propName of Object.keys(params)) { + const value = params[propName]; + var part = encodeURIComponent(propName) + "="; + if (value !== null && value !== "" && typeof (value) !== "undefined") { + if (typeof value === 'object') { + for (const key of Object.keys(value)) { + if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') { + let params = propName + '[' + key + ']'; + var subPart = encodeURIComponent(params) + "="; + result += subPart + encodeURIComponent(value[key]) + "&"; + } + } + } else { + result += part + encodeURIComponent(value) + "&"; + } + } + } + return result +} + +// 验证是否为blob格式 +export function blobValidate(data) { + return data.type !== 'application/json' +} diff --git a/ruoyi-ui/src/utils/scroll-to.js b/ruoyi-ui/src/utils/scroll-to.js new file mode 100644 index 0000000..c5d8e04 --- /dev/null +++ b/ruoyi-ui/src/utils/scroll-to.js @@ -0,0 +1,58 @@ +Math.easeInOutQuad = function(t, b, c, d) { + t /= d / 2 + if (t < 1) { + return c / 2 * t * t + b + } + t-- + return -c / 2 * (t * (t - 2) - 1) + b +} + +// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts +var requestAnimFrame = (function() { + return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } +})() + +/** + * Because it's so fucking difficult to detect the scrolling element, just move them all + * @param {number} amount + */ +function move(amount) { + document.documentElement.scrollTop = amount + document.body.parentNode.scrollTop = amount + document.body.scrollTop = amount +} + +function position() { + return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop +} + +/** + * @param {number} to + * @param {number} duration + * @param {Function} callback + */ +export function scrollTo(to, duration, callback) { + const start = position() + const change = to - start + const increment = 20 + let currentTime = 0 + duration = (typeof (duration) === 'undefined') ? 500 : duration + var animateScroll = function() { + // increment the time + currentTime += increment + // find the value with the quadratic in-out easing function + var val = Math.easeInOutQuad(currentTime, start, change, duration) + // move the document.body + move(val) + // do the animation unless its over + if (currentTime < duration) { + requestAnimFrame(animateScroll) + } else { + if (callback && typeof (callback) === 'function') { + // the animation is done so lets callback + callback() + } + } + } + animateScroll() +} diff --git a/ruoyi-ui/src/utils/validate.js b/ruoyi-ui/src/utils/validate.js new file mode 100644 index 0000000..adfa254 --- /dev/null +++ b/ruoyi-ui/src/utils/validate.js @@ -0,0 +1,83 @@ +/** + * @param {string} path + * @returns {Boolean} + */ +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUsername(str) { + const valid_map = ['admin', 'editor'] + return valid_map.indexOf(str.trim()) >= 0 +} + +/** + * @param {string} url + * @returns {Boolean} + */ +export function validURL(url) { + const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ + return reg.test(url) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validLowerCase(str) { + const reg = /^[a-z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUpperCase(str) { + const reg = /^[A-Z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validAlphabets(str) { + const reg = /^[A-Za-z]+$/ + return reg.test(str) +} + +/** + * @param {string} email + * @returns {Boolean} + */ +export function validEmail(email) { + const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + return reg.test(email) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function isString(str) { + if (typeof str === 'string' || str instanceof String) { + return true + } + return false +} + +/** + * @param {Array} arg + * @returns {Boolean} + */ +export function isArray(arg) { + if (typeof Array.isArray === 'undefined') { + return Object.prototype.toString.call(arg) === '[object Array]' + } + return Array.isArray(arg) +} diff --git a/ruoyi-ui/src/views/components/icons/element-icons.js b/ruoyi-ui/src/views/components/icons/element-icons.js new file mode 100644 index 0000000..9ea4d63 --- /dev/null +++ b/ruoyi-ui/src/views/components/icons/element-icons.js @@ -0,0 +1,3 @@ +const elementIcons = ['platform-eleme', 'eleme', 'delete-solid', 'delete', 's-tools', 'setting', 'user-solid', 'user', 'phone', 'phone-outline', 'more', 'more-outline', 'star-on', 'star-off', 's-goods', 'goods', 'warning', 'warning-outline', 'question', 'info', 'remove', 'circle-plus', 'success', 'error', 'zoom-in', 'zoom-out', 'remove-outline', 'circle-plus-outline', 'circle-check', 'circle-close', 's-help', 'help', 'minus', 'plus', 'check', 'close', 'picture', 'picture-outline', 'picture-outline-round', 'upload', 'upload2', 'download', 'camera-solid', 'camera', 'video-camera-solid', 'video-camera', 'message-solid', 'bell', 's-cooperation', 's-order', 's-platform', 's-fold', 's-unfold', 's-operation', 's-promotion', 's-home', 's-release', 's-ticket', 's-management', 's-open', 's-shop', 's-marketing', 's-flag', 's-comment', 's-finance', 's-claim', 's-custom', 's-opportunity', 's-data', 's-check', 's-grid', 'menu', 'share', 'd-caret', 'caret-left', 'caret-right', 'caret-bottom', 'caret-top', 'bottom-left', 'bottom-right', 'back', 'right', 'bottom', 'top', 'top-left', 'top-right', 'arrow-left', 'arrow-right', 'arrow-down', 'arrow-up', 'd-arrow-left', 'd-arrow-right', 'video-pause', 'video-play', 'refresh', 'refresh-right', 'refresh-left', 'finished', 'sort', 'sort-up', 'sort-down', 'rank', 'loading', 'view', 'c-scale-to-original', 'date', 'edit', 'edit-outline', 'folder', 'folder-opened', 'folder-add', 'folder-remove', 'folder-delete', 'folder-checked', 'tickets', 'document-remove', 'document-delete', 'document-copy', 'document-checked', 'document', 'document-add', 'printer', 'paperclip', 'takeaway-box', 'search', 'monitor', 'attract', 'mobile', 'scissors', 'umbrella', 'headset', 'brush', 'mouse', 'coordinate', 'magic-stick', 'reading', 'data-line', 'data-board', 'pie-chart', 'data-analysis', 'collection-tag', 'film', 'suitcase', 'suitcase-1', 'receiving', 'collection', 'files', 'notebook-1', 'notebook-2', 'toilet-paper', 'office-building', 'school', 'table-lamp', 'house', 'no-smoking', 'smoking', 'shopping-cart-full', 'shopping-cart-1', 'shopping-cart-2', 'shopping-bag-1', 'shopping-bag-2', 'sold-out', 'sell', 'present', 'box', 'bank-card', 'money', 'coin', 'wallet', 'discount', 'price-tag', 'news', 'guide', 'male', 'female', 'thumb', 'cpu', 'link', 'connection', 'open', 'turn-off', 'set-up', 'chat-round', 'chat-line-round', 'chat-square', 'chat-dot-round', 'chat-dot-square', 'chat-line-square', 'message', 'postcard', 'position', 'turn-off-microphone', 'microphone', 'close-notification', 'bangzhu', 'time', 'odometer', 'crop', 'aim', 'switch-button', 'full-screen', 'copy-document', 'mic', 'stopwatch', 'medal-1', 'medal', 'trophy', 'trophy-1', 'first-aid-kit', 'discover', 'place', 'location', 'location-outline', 'location-information', 'add-location', 'delete-location', 'map-location', 'alarm-clock', 'timer', 'watch-1', 'watch', 'lock', 'unlock', 'key', 'service', 'mobile-phone', 'bicycle', 'truck', 'ship', 'basketball', 'football', 'soccer', 'baseball', 'wind-power', 'light-rain', 'lightning', 'heavy-rain', 'sunrise', 'sunrise-1', 'sunset', 'sunny', 'cloudy', 'partly-cloudy', 'cloudy-and-sunny', 'moon', 'moon-night', 'dish', 'dish-1', 'food', 'chicken', 'fork-spoon', 'knife-fork', 'burger', 'tableware', 'sugar', 'dessert', 'ice-cream', 'hot-water', 'water-cup', 'coffee-cup', 'cold-drink', 'goblet', 'goblet-full', 'goblet-square', 'goblet-square-full', 'refrigerator', 'grape', 'watermelon', 'cherry', 'apple', 'pear', 'orange', 'coffee', 'ice-tea', 'ice-drink', 'milk-tea', 'potato-strips', 'lollipop', 'ice-cream-square', 'ice-cream-round'] + +export default elementIcons diff --git a/ruoyi-ui/src/views/components/icons/index.vue b/ruoyi-ui/src/views/components/icons/index.vue new file mode 100644 index 0000000..d3c9a71 --- /dev/null +++ b/ruoyi-ui/src/views/components/icons/index.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/ruoyi-ui/src/views/components/icons/svg-icons.js b/ruoyi-ui/src/views/components/icons/svg-icons.js new file mode 100644 index 0000000..724cd8e --- /dev/null +++ b/ruoyi-ui/src/views/components/icons/svg-icons.js @@ -0,0 +1,10 @@ +const req = require.context('../../../assets/icons/svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys() + +const re = /\.\/(.*)\.svg/ + +const svgIcons = requireAll(req).map(i => { + return i.match(re)[1] +}) + +export default svgIcons diff --git a/ruoyi-ui/src/views/dashboard/BarChart.vue b/ruoyi-ui/src/views/dashboard/BarChart.vue new file mode 100644 index 0000000..cd33d2d --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/BarChart.vue @@ -0,0 +1,102 @@ + + + diff --git a/ruoyi-ui/src/views/dashboard/LineChart.vue b/ruoyi-ui/src/views/dashboard/LineChart.vue new file mode 100644 index 0000000..ddd1063 --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/LineChart.vue @@ -0,0 +1,135 @@ + + + diff --git a/ruoyi-ui/src/views/dashboard/PanelGroup.vue b/ruoyi-ui/src/views/dashboard/PanelGroup.vue new file mode 100644 index 0000000..1a1081f --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/PanelGroup.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/ruoyi-ui/src/views/dashboard/PieChart.vue b/ruoyi-ui/src/views/dashboard/PieChart.vue new file mode 100644 index 0000000..c360057 --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/PieChart.vue @@ -0,0 +1,79 @@ + + + diff --git a/ruoyi-ui/src/views/dashboard/RaddarChart.vue b/ruoyi-ui/src/views/dashboard/RaddarChart.vue new file mode 100644 index 0000000..b1790ca --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/RaddarChart.vue @@ -0,0 +1,116 @@ + + + diff --git a/ruoyi-ui/src/views/dashboard/mixins/resize.js b/ruoyi-ui/src/views/dashboard/mixins/resize.js new file mode 100644 index 0000000..b1e76e9 --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/mixins/resize.js @@ -0,0 +1,56 @@ +import { debounce } from '@/utils' + +export default { + data() { + return { + $_sidebarElm: null, + $_resizeHandler: null + } + }, + mounted() { + this.initListener() + }, + activated() { + if (!this.$_resizeHandler) { + // avoid duplication init + this.initListener() + } + + // when keep-alive chart activated, auto resize + this.resize() + }, + beforeDestroy() { + this.destroyListener() + }, + deactivated() { + this.destroyListener() + }, + methods: { + // use $_ for mixins properties + // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential + $_sidebarResizeHandler(e) { + if (e.propertyName === 'width') { + this.$_resizeHandler() + } + }, + initListener() { + this.$_resizeHandler = debounce(() => { + this.resize() + }, 100) + window.addEventListener('resize', this.$_resizeHandler) + + this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] + this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) + }, + destroyListener() { + window.removeEventListener('resize', this.$_resizeHandler) + this.$_resizeHandler = null + + this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) + }, + resize() { + const { chart } = this + chart && chart.resize() + } + } +} diff --git a/ruoyi-ui/src/views/error/401.vue b/ruoyi-ui/src/views/error/401.vue new file mode 100644 index 0000000..448b6ec --- /dev/null +++ b/ruoyi-ui/src/views/error/401.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/ruoyi-ui/src/views/error/404.vue b/ruoyi-ui/src/views/error/404.vue new file mode 100644 index 0000000..96f075c --- /dev/null +++ b/ruoyi-ui/src/views/error/404.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/ruoyi-ui/src/views/game/box/mock/index.vue b/ruoyi-ui/src/views/game/box/mock/index.vue new file mode 100644 index 0000000..d1903a6 --- /dev/null +++ b/ruoyi-ui/src/views/game/box/mock/index.vue @@ -0,0 +1,160 @@ + + + diff --git a/ruoyi-ui/src/views/game/bullet/box/index.vue b/ruoyi-ui/src/views/game/bullet/box/index.vue new file mode 100644 index 0000000..72c94d6 --- /dev/null +++ b/ruoyi-ui/src/views/game/bullet/box/index.vue @@ -0,0 +1,281 @@ + + + diff --git a/ruoyi-ui/src/views/game/bullet/dailywin/index.vue b/ruoyi-ui/src/views/game/bullet/dailywin/index.vue new file mode 100644 index 0000000..b9ac830 --- /dev/null +++ b/ruoyi-ui/src/views/game/bullet/dailywin/index.vue @@ -0,0 +1,61 @@ + + + diff --git a/ruoyi-ui/src/views/game/bullet/ornament/index.vue b/ruoyi-ui/src/views/game/bullet/ornament/index.vue new file mode 100644 index 0000000..4aaf525 --- /dev/null +++ b/ruoyi-ui/src/views/game/bullet/ornament/index.vue @@ -0,0 +1,23 @@ + + + diff --git a/ruoyi-ui/src/views/game/bullet/prize/index.vue b/ruoyi-ui/src/views/game/bullet/prize/index.vue new file mode 100644 index 0000000..e0e970e --- /dev/null +++ b/ruoyi-ui/src/views/game/bullet/prize/index.vue @@ -0,0 +1,74 @@ + + + diff --git a/ruoyi-ui/src/views/game/bullet/record/index.vue b/ruoyi-ui/src/views/game/bullet/record/index.vue new file mode 100644 index 0000000..cf36c86 --- /dev/null +++ b/ruoyi-ui/src/views/game/bullet/record/index.vue @@ -0,0 +1,148 @@ + + + diff --git a/ruoyi-ui/src/views/game/bullet/round/index.vue b/ruoyi-ui/src/views/game/bullet/round/index.vue new file mode 100644 index 0000000..5ddd128 --- /dev/null +++ b/ruoyi-ui/src/views/game/bullet/round/index.vue @@ -0,0 +1,80 @@ + + + diff --git a/ruoyi-ui/src/views/game/olympus/box/index.vue b/ruoyi-ui/src/views/game/olympus/box/index.vue new file mode 100644 index 0000000..6159019 --- /dev/null +++ b/ruoyi-ui/src/views/game/olympus/box/index.vue @@ -0,0 +1,451 @@ + + + diff --git a/ruoyi-ui/src/views/game/olympus/dailywin/index.vue b/ruoyi-ui/src/views/game/olympus/dailywin/index.vue new file mode 100644 index 0000000..fdb67bd --- /dev/null +++ b/ruoyi-ui/src/views/game/olympus/dailywin/index.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/ruoyi-ui/src/views/game/olympus/ornament/index.vue b/ruoyi-ui/src/views/game/olympus/ornament/index.vue new file mode 100644 index 0000000..4cf23a9 --- /dev/null +++ b/ruoyi-ui/src/views/game/olympus/ornament/index.vue @@ -0,0 +1,32 @@ + + + diff --git a/ruoyi-ui/src/views/game/olympus/prize/index.vue b/ruoyi-ui/src/views/game/olympus/prize/index.vue new file mode 100644 index 0000000..6a069f8 --- /dev/null +++ b/ruoyi-ui/src/views/game/olympus/prize/index.vue @@ -0,0 +1,121 @@ + + + diff --git a/ruoyi-ui/src/views/game/olympus/record/index.vue b/ruoyi-ui/src/views/game/olympus/record/index.vue new file mode 100644 index 0000000..92cbc61 --- /dev/null +++ b/ruoyi-ui/src/views/game/olympus/record/index.vue @@ -0,0 +1,108 @@ + + + diff --git a/ruoyi-ui/src/views/game/olympus/rewardPool/index.vue b/ruoyi-ui/src/views/game/olympus/rewardPool/index.vue new file mode 100644 index 0000000..2f976c1 --- /dev/null +++ b/ruoyi-ui/src/views/game/olympus/rewardPool/index.vue @@ -0,0 +1,451 @@ + + + + + diff --git a/ruoyi-ui/src/views/game/olympus/round/index.vue b/ruoyi-ui/src/views/game/olympus/round/index.vue new file mode 100644 index 0000000..f699450 --- /dev/null +++ b/ruoyi-ui/src/views/game/olympus/round/index.vue @@ -0,0 +1,82 @@ + + + diff --git a/ruoyi-ui/src/views/game/sugar/config/index.vue b/ruoyi-ui/src/views/game/sugar/config/index.vue new file mode 100644 index 0000000..d398662 --- /dev/null +++ b/ruoyi-ui/src/views/game/sugar/config/index.vue @@ -0,0 +1,95 @@ + + + diff --git a/ruoyi-ui/src/views/game/sugar/dailywin/index.vue b/ruoyi-ui/src/views/game/sugar/dailywin/index.vue new file mode 100644 index 0000000..a781900 --- /dev/null +++ b/ruoyi-ui/src/views/game/sugar/dailywin/index.vue @@ -0,0 +1,200 @@ + + + + diff --git a/ruoyi-ui/src/views/game/sugar/mock/index.vue b/ruoyi-ui/src/views/game/sugar/mock/index.vue new file mode 100644 index 0000000..a92511e --- /dev/null +++ b/ruoyi-ui/src/views/game/sugar/mock/index.vue @@ -0,0 +1,296 @@ + + + + diff --git a/ruoyi-ui/src/views/game/sugar/record/index.vue b/ruoyi-ui/src/views/game/sugar/record/index.vue new file mode 100644 index 0000000..5232494 --- /dev/null +++ b/ruoyi-ui/src/views/game/sugar/record/index.vue @@ -0,0 +1,343 @@ + + + + diff --git a/ruoyi-ui/src/views/game/sugar/user/index.vue b/ruoyi-ui/src/views/game/sugar/user/index.vue new file mode 100644 index 0000000..8e5a903 --- /dev/null +++ b/ruoyi-ui/src/views/game/sugar/user/index.vue @@ -0,0 +1,440 @@ + + + + diff --git a/ruoyi-ui/src/views/game/sweet/box/index.vue b/ruoyi-ui/src/views/game/sweet/box/index.vue new file mode 100644 index 0000000..7886f3c --- /dev/null +++ b/ruoyi-ui/src/views/game/sweet/box/index.vue @@ -0,0 +1,578 @@ + + + diff --git a/ruoyi-ui/src/views/game/sweet/dailywin/index.vue b/ruoyi-ui/src/views/game/sweet/dailywin/index.vue new file mode 100644 index 0000000..2304ed9 --- /dev/null +++ b/ruoyi-ui/src/views/game/sweet/dailywin/index.vue @@ -0,0 +1,80 @@ + + + diff --git a/ruoyi-ui/src/views/game/sweet/ornament/index.vue b/ruoyi-ui/src/views/game/sweet/ornament/index.vue new file mode 100644 index 0000000..8e1c0ce --- /dev/null +++ b/ruoyi-ui/src/views/game/sweet/ornament/index.vue @@ -0,0 +1,22 @@ + + + diff --git a/ruoyi-ui/src/views/game/sweet/prize/index.vue b/ruoyi-ui/src/views/game/sweet/prize/index.vue new file mode 100644 index 0000000..414bb73 --- /dev/null +++ b/ruoyi-ui/src/views/game/sweet/prize/index.vue @@ -0,0 +1,125 @@ + + + + diff --git a/ruoyi-ui/src/views/game/sweet/record/index.vue b/ruoyi-ui/src/views/game/sweet/record/index.vue new file mode 100644 index 0000000..86eee24 --- /dev/null +++ b/ruoyi-ui/src/views/game/sweet/record/index.vue @@ -0,0 +1,101 @@ + + + + diff --git a/ruoyi-ui/src/views/game/sweet/round/index.vue b/ruoyi-ui/src/views/game/sweet/round/index.vue new file mode 100644 index 0000000..2d2ee9b --- /dev/null +++ b/ruoyi-ui/src/views/game/sweet/round/index.vue @@ -0,0 +1,85 @@ + + + diff --git a/ruoyi-ui/src/views/game/wheel/config/index.vue b/ruoyi-ui/src/views/game/wheel/config/index.vue new file mode 100644 index 0000000..03f682b --- /dev/null +++ b/ruoyi-ui/src/views/game/wheel/config/index.vue @@ -0,0 +1,557 @@ + + + diff --git a/ruoyi-ui/src/views/game/wheel/prize/index.vue b/ruoyi-ui/src/views/game/wheel/prize/index.vue new file mode 100644 index 0000000..c23db79 --- /dev/null +++ b/ruoyi-ui/src/views/game/wheel/prize/index.vue @@ -0,0 +1,228 @@ + + + diff --git a/ruoyi-ui/src/views/game/wheel/records/index.vue b/ruoyi-ui/src/views/game/wheel/records/index.vue new file mode 100644 index 0000000..dd9974b --- /dev/null +++ b/ruoyi-ui/src/views/game/wheel/records/index.vue @@ -0,0 +1,230 @@ + + + diff --git a/ruoyi-ui/src/views/index.vue b/ruoyi-ui/src/views/index.vue new file mode 100644 index 0000000..3f08ed4 --- /dev/null +++ b/ruoyi-ui/src/views/index.vue @@ -0,0 +1,777 @@ + + + + + diff --git a/ruoyi-ui/src/views/index_v1.vue b/ruoyi-ui/src/views/index_v1.vue new file mode 100644 index 0000000..adad227 --- /dev/null +++ b/ruoyi-ui/src/views/index_v1.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/ruoyi-ui/src/views/login.vue b/ruoyi-ui/src/views/login.vue new file mode 100644 index 0000000..de09ae1 --- /dev/null +++ b/ruoyi-ui/src/views/login.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/ruoyi-ui/src/views/monitor/cache/index.vue b/ruoyi-ui/src/views/monitor/cache/index.vue new file mode 100644 index 0000000..e81da2e --- /dev/null +++ b/ruoyi-ui/src/views/monitor/cache/index.vue @@ -0,0 +1,144 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/cache/list.vue b/ruoyi-ui/src/views/monitor/cache/list.vue new file mode 100644 index 0000000..29a7c74 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/cache/list.vue @@ -0,0 +1,241 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/druid/index.vue b/ruoyi-ui/src/views/monitor/druid/index.vue new file mode 100644 index 0000000..c6ad585 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/druid/index.vue @@ -0,0 +1,15 @@ + + diff --git a/ruoyi-ui/src/views/monitor/job/index.vue b/ruoyi-ui/src/views/monitor/job/index.vue new file mode 100644 index 0000000..892c727 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/job/index.vue @@ -0,0 +1,513 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/job/log.vue b/ruoyi-ui/src/views/monitor/job/log.vue new file mode 100644 index 0000000..60bee1d --- /dev/null +++ b/ruoyi-ui/src/views/monitor/job/log.vue @@ -0,0 +1,295 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/logininfor/index.vue b/ruoyi-ui/src/views/monitor/logininfor/index.vue new file mode 100644 index 0000000..d6af834 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/logininfor/index.vue @@ -0,0 +1,246 @@ + + + + diff --git a/ruoyi-ui/src/views/monitor/online/index.vue b/ruoyi-ui/src/views/monitor/online/index.vue new file mode 100644 index 0000000..ad613c9 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/online/index.vue @@ -0,0 +1,122 @@ + + + + diff --git a/ruoyi-ui/src/views/monitor/operlog/index.vue b/ruoyi-ui/src/views/monitor/operlog/index.vue new file mode 100644 index 0000000..2a3c3cb --- /dev/null +++ b/ruoyi-ui/src/views/monitor/operlog/index.vue @@ -0,0 +1,395 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/server/index.vue b/ruoyi-ui/src/views/monitor/server/index.vue new file mode 100644 index 0000000..15ffc9a --- /dev/null +++ b/ruoyi-ui/src/views/monitor/server/index.vue @@ -0,0 +1,207 @@ + + + diff --git a/ruoyi-ui/src/views/redirect.vue b/ruoyi-ui/src/views/redirect.vue new file mode 100644 index 0000000..db4c1d6 --- /dev/null +++ b/ruoyi-ui/src/views/redirect.vue @@ -0,0 +1,12 @@ + diff --git a/ruoyi-ui/src/views/register.vue b/ruoyi-ui/src/views/register.vue new file mode 100644 index 0000000..b50f238 --- /dev/null +++ b/ruoyi-ui/src/views/register.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/blankAccount/index.vue b/ruoyi-ui/src/views/skins/blankAccount/index.vue new file mode 100644 index 0000000..2faa0a5 --- /dev/null +++ b/ruoyi-ui/src/views/skins/blankAccount/index.vue @@ -0,0 +1,298 @@ + + + diff --git a/ruoyi-ui/src/views/skins/blankCardKey/index.vue b/ruoyi-ui/src/views/skins/blankCardKey/index.vue new file mode 100644 index 0000000..8af2cb8 --- /dev/null +++ b/ruoyi-ui/src/views/skins/blankCardKey/index.vue @@ -0,0 +1,164 @@ + + + diff --git a/ruoyi-ui/src/views/skins/delivery/index.vue b/ruoyi-ui/src/views/skins/delivery/index.vue new file mode 100644 index 0000000..e12c479 --- /dev/null +++ b/ruoyi-ui/src/views/skins/delivery/index.vue @@ -0,0 +1,289 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/delivery/sendGoods.vue b/ruoyi-ui/src/views/skins/delivery/sendGoods.vue new file mode 100644 index 0000000..b5e2a47 --- /dev/null +++ b/ruoyi-ui/src/views/skins/delivery/sendGoods.vue @@ -0,0 +1,218 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/delivery/transferApply.vue b/ruoyi-ui/src/views/skins/delivery/transferApply.vue new file mode 100644 index 0000000..982ba23 --- /dev/null +++ b/ruoyi-ui/src/views/skins/delivery/transferApply.vue @@ -0,0 +1,355 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/delivery/transferApplyProcessed.vue b/ruoyi-ui/src/views/skins/delivery/transferApplyProcessed.vue new file mode 100644 index 0000000..b3ff524 --- /dev/null +++ b/ruoyi-ui/src/views/skins/delivery/transferApplyProcessed.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/delivery/zbtrecord.vue b/ruoyi-ui/src/views/skins/delivery/zbtrecord.vue new file mode 100644 index 0000000..2654fca --- /dev/null +++ b/ruoyi-ui/src/views/skins/delivery/zbtrecord.vue @@ -0,0 +1,276 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/promoCdkeyRedeem/index.vue b/ruoyi-ui/src/views/skins/promoCdkeyRedeem/index.vue new file mode 100644 index 0000000..492c052 --- /dev/null +++ b/ruoyi-ui/src/views/skins/promoCdkeyRedeem/index.vue @@ -0,0 +1,102 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttBonus/firstRecharge.vue b/ruoyi-ui/src/views/skins/ttBonus/firstRecharge.vue new file mode 100644 index 0000000..5dbbe9a --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttBonus/firstRecharge.vue @@ -0,0 +1,279 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttBonus/index.vue b/ruoyi-ui/src/views/skins/ttBonus/index.vue new file mode 100644 index 0000000..82cfa0a --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttBonus/index.vue @@ -0,0 +1,494 @@ + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttBox/boxRecords.vue b/ruoyi-ui/src/views/skins/ttBox/boxRecords.vue new file mode 100644 index 0000000..06b5524 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttBox/boxRecords.vue @@ -0,0 +1,168 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttBox/boxType.vue b/ruoyi-ui/src/views/skins/ttBox/boxType.vue new file mode 100644 index 0000000..6184c25 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttBox/boxType.vue @@ -0,0 +1,280 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttBox/boxrPize.vue b/ruoyi-ui/src/views/skins/ttBox/boxrPize.vue new file mode 100644 index 0000000..5d04983 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttBox/boxrPize.vue @@ -0,0 +1,1171 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttBox/index.vue b/ruoyi-ui/src/views/skins/ttBox/index.vue new file mode 100644 index 0000000..9ad3af1 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttBox/index.vue @@ -0,0 +1,1489 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttCardMerchantRecharge/index.vue b/ruoyi-ui/src/views/skins/ttCardMerchantRecharge/index.vue new file mode 100644 index 0000000..586d78b --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttCardMerchantRecharge/index.vue @@ -0,0 +1,430 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttFight/fightBox.vue b/ruoyi-ui/src/views/skins/ttFight/fightBox.vue new file mode 100644 index 0000000..982c947 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttFight/fightBox.vue @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttFight/fightMock.vue b/ruoyi-ui/src/views/skins/ttFight/fightMock.vue new file mode 100644 index 0000000..54a86fb --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttFight/fightMock.vue @@ -0,0 +1,158 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttFight/fightPrize.vue b/ruoyi-ui/src/views/skins/ttFight/fightPrize.vue new file mode 100644 index 0000000..075e55b --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttFight/fightPrize.vue @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttFight/fightRankingReward.vue b/ruoyi-ui/src/views/skins/ttFight/fightRankingReward.vue new file mode 100644 index 0000000..4760f8d --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttFight/fightRankingReward.vue @@ -0,0 +1,211 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttFight/fightResult.vue b/ruoyi-ui/src/views/skins/ttFight/fightResult.vue new file mode 100644 index 0000000..bc3fa3e --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttFight/fightResult.vue @@ -0,0 +1,145 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttFight/index.vue b/ruoyi-ui/src/views/skins/ttFight/index.vue new file mode 100644 index 0000000..c5bddcd --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttFight/index.vue @@ -0,0 +1,179 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttFight/robotFightConfig.vue b/ruoyi-ui/src/views/skins/ttFight/robotFightConfig.vue new file mode 100644 index 0000000..311d759 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttFight/robotFightConfig.vue @@ -0,0 +1,433 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttFight/robotFightGroup.vue b/ruoyi-ui/src/views/skins/ttFight/robotFightGroup.vue new file mode 100644 index 0000000..7b2ee7a --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttFight/robotFightGroup.vue @@ -0,0 +1,365 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttFight/robotFightGroupBox.vue b/ruoyi-ui/src/views/skins/ttFight/robotFightGroupBox.vue new file mode 100644 index 0000000..2fbfdbe --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttFight/robotFightGroupBox.vue @@ -0,0 +1,333 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttGenerateCard/index.vue b/ruoyi-ui/src/views/skins/ttGenerateCard/index.vue new file mode 100644 index 0000000..3b07aee --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttGenerateCard/index.vue @@ -0,0 +1,422 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttOrder/index.vue b/ruoyi-ui/src/views/skins/ttOrder/index.vue new file mode 100644 index 0000000..e98afc4 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttOrder/index.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttOrnaments/boxOpenChance.vue b/ruoyi-ui/src/views/skins/ttOrnaments/boxOpenChance.vue new file mode 100644 index 0000000..62528af --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttOrnaments/boxOpenChance.vue @@ -0,0 +1,253 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttOrnaments/index.vue b/ruoyi-ui/src/views/skins/ttOrnaments/index.vue new file mode 100644 index 0000000..3715c6c --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttOrnaments/index.vue @@ -0,0 +1,803 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttOrnaments/websiteProperty.vue b/ruoyi-ui/src/views/skins/ttOrnaments/websiteProperty.vue new file mode 100644 index 0000000..75cfe41 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttOrnaments/websiteProperty.vue @@ -0,0 +1,499 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttOrnamentsLevel/index.vue b/ruoyi-ui/src/views/skins/ttOrnamentsLevel/index.vue new file mode 100644 index 0000000..47630a4 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttOrnamentsLevel/index.vue @@ -0,0 +1,248 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttOrnamentsType/index.vue b/ruoyi-ui/src/views/skins/ttOrnamentsType/index.vue new file mode 100644 index 0000000..efda524 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttOrnamentsType/index.vue @@ -0,0 +1,285 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttRechargeList/index.vue b/ruoyi-ui/src/views/skins/ttRechargeList/index.vue new file mode 100644 index 0000000..915da38 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRechargeList/index.vue @@ -0,0 +1,275 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttRechargeRecord/index.vue b/ruoyi-ui/src/views/skins/ttRechargeRecord/index.vue new file mode 100644 index 0000000..f7f10e4 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRechargeRecord/index.vue @@ -0,0 +1,188 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttRedPack/index.vue b/ruoyi-ui/src/views/skins/ttRedPack/index.vue new file mode 100644 index 0000000..139aba1 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRedPack/index.vue @@ -0,0 +1,350 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttReplacementRecord/index.vue b/ruoyi-ui/src/views/skins/ttReplacementRecord/index.vue new file mode 100644 index 0000000..100b14a --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttReplacementRecord/index.vue @@ -0,0 +1,340 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttRoll/awarddetali.vue b/ruoyi-ui/src/views/skins/ttRoll/awarddetali.vue new file mode 100644 index 0000000..a627d35 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRoll/awarddetali.vue @@ -0,0 +1,245 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttRoll/index.vue b/ruoyi-ui/src/views/skins/ttRoll/index.vue new file mode 100644 index 0000000..b2c8042 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRoll/index.vue @@ -0,0 +1,655 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttRoll/officialRollConfig.vue b/ruoyi-ui/src/views/skins/ttRoll/officialRollConfig.vue new file mode 100644 index 0000000..694edd4 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRoll/officialRollConfig.vue @@ -0,0 +1,307 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttRoll/prize.vue b/ruoyi-ui/src/views/skins/ttRoll/prize.vue new file mode 100644 index 0000000..a791829 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRoll/prize.vue @@ -0,0 +1,646 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttRoll/prizedetail.vue b/ruoyi-ui/src/views/skins/ttRoll/prizedetail.vue new file mode 100644 index 0000000..9a036eb --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRoll/prizedetail.vue @@ -0,0 +1,445 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttRoll/rollUser.vue b/ruoyi-ui/src/views/skins/ttRoll/rollUser.vue new file mode 100644 index 0000000..44c8938 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRoll/rollUser.vue @@ -0,0 +1,206 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttRoll/timeRoll.vue b/ruoyi-ui/src/views/skins/ttRoll/timeRoll.vue new file mode 100644 index 0000000..53294af --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttRoll/timeRoll.vue @@ -0,0 +1,481 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttSetting/announcement.vue b/ruoyi-ui/src/views/skins/ttSetting/announcement.vue new file mode 100644 index 0000000..52da149 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/announcement.vue @@ -0,0 +1,307 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttSetting/avatar.vue b/ruoyi-ui/src/views/skins/ttSetting/avatar.vue new file mode 100644 index 0000000..78621bb --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/avatar.vue @@ -0,0 +1,244 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttSetting/banner.vue b/ruoyi-ui/src/views/skins/ttSetting/banner.vue new file mode 100644 index 0000000..44aee04 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/banner.vue @@ -0,0 +1,305 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttSetting/contentList.vue b/ruoyi-ui/src/views/skins/ttSetting/contentList.vue new file mode 100644 index 0000000..7bd3de6 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/contentList.vue @@ -0,0 +1,429 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttSetting/contentType.vue b/ruoyi-ui/src/views/skins/ttSetting/contentType.vue new file mode 100644 index 0000000..4bc9b53 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/contentType.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttSetting/index.vue b/ruoyi-ui/src/views/skins/ttSetting/index.vue new file mode 100644 index 0000000..962c513 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/index.vue @@ -0,0 +1,339 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttSetting/maintenance.vue b/ruoyi-ui/src/views/skins/ttSetting/maintenance.vue new file mode 100644 index 0000000..670d089 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/maintenance.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttSetting/promotionLevel.vue b/ruoyi-ui/src/views/skins/ttSetting/promotionLevel.vue new file mode 100644 index 0000000..03092fe --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/promotionLevel.vue @@ -0,0 +1,242 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttSetting/setting.vue b/ruoyi-ui/src/views/skins/ttSetting/setting.vue new file mode 100644 index 0000000..f9d8dba --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/setting.vue @@ -0,0 +1,136 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttSetting/vips.vue b/ruoyi-ui/src/views/skins/ttSetting/vips.vue new file mode 100644 index 0000000..96e7cec --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttSetting/vips.vue @@ -0,0 +1,182 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttShopping/index.vue b/ruoyi-ui/src/views/skins/ttShopping/index.vue new file mode 100644 index 0000000..6431379 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttShopping/index.vue @@ -0,0 +1,356 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttTaskCenter/index.vue b/ruoyi-ui/src/views/skins/ttTaskCenter/index.vue new file mode 100644 index 0000000..411b846 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttTaskCenter/index.vue @@ -0,0 +1,275 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttUpgrade/index.vue b/ruoyi-ui/src/views/skins/ttUpgrade/index.vue new file mode 100644 index 0000000..c4ea5fd --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttUpgrade/index.vue @@ -0,0 +1,917 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttUpgrade/upgradeFall.vue b/ruoyi-ui/src/views/skins/ttUpgrade/upgradeFall.vue new file mode 100644 index 0000000..241b148 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttUpgrade/upgradeFall.vue @@ -0,0 +1,537 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttUpgrade/upgradeRecord.vue b/ruoyi-ui/src/views/skins/ttUpgrade/upgradeRecord.vue new file mode 100644 index 0000000..4fde655 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttUpgrade/upgradeRecord.vue @@ -0,0 +1,253 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttWelfare/index.vue b/ruoyi-ui/src/views/skins/ttWelfare/index.vue new file mode 100644 index 0000000..007c051 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttWelfare/index.vue @@ -0,0 +1,270 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttWelfare/monthlyRecharges.vue b/ruoyi-ui/src/views/skins/ttWelfare/monthlyRecharges.vue new file mode 100644 index 0000000..f163231 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttWelfare/monthlyRecharges.vue @@ -0,0 +1,344 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttWelfare/rechargeRankingReward.vue b/ruoyi-ui/src/views/skins/ttWelfare/rechargeRankingReward.vue new file mode 100644 index 0000000..964108d --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttWelfare/rechargeRankingReward.vue @@ -0,0 +1,209 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttWelfare/welfareBox.vue b/ruoyi-ui/src/views/skins/ttWelfare/welfareBox.vue new file mode 100644 index 0000000..0031aba --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttWelfare/welfareBox.vue @@ -0,0 +1,949 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttWelfare/welfareBoxRecords.vue b/ruoyi-ui/src/views/skins/ttWelfare/welfareBoxRecords.vue new file mode 100644 index 0000000..b69d074 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttWelfare/welfareBoxRecords.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttredPacket/index.vue b/ruoyi-ui/src/views/skins/ttredPacket/index.vue new file mode 100644 index 0000000..52c82c4 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttredPacket/index.vue @@ -0,0 +1,654 @@ + + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttredPacket/lossRebate.vue b/ruoyi-ui/src/views/skins/ttredPacket/lossRebate.vue new file mode 100644 index 0000000..543d4c8 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttredPacket/lossRebate.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttredPacket/packetRecord.vue b/ruoyi-ui/src/views/skins/ttredPacket/packetRecord.vue new file mode 100644 index 0000000..bd97ca0 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttredPacket/packetRecord.vue @@ -0,0 +1,116 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttuser/amountRecords.vue b/ruoyi-ui/src/views/skins/ttuser/amountRecords.vue new file mode 100644 index 0000000..b5d000a --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/amountRecords.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttuser/blendErcashArchive.vue b/ruoyi-ui/src/views/skins/ttuser/blendErcashArchive.vue new file mode 100644 index 0000000..5bc6972 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/blendErcashArchive.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttuser/creditsRecords.vue b/ruoyi-ui/src/views/skins/ttuser/creditsRecords.vue new file mode 100644 index 0000000..b5cd522 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/creditsRecords.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/ruoyi-ui/src/views/skins/ttuser/index.vue b/ruoyi-ui/src/views/skins/ttuser/index.vue new file mode 100644 index 0000000..316d70d --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/index.vue @@ -0,0 +1,976 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttuser/ipAddr.vue b/ruoyi-ui/src/views/skins/ttuser/ipAddr.vue new file mode 100644 index 0000000..641e280 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/ipAddr.vue @@ -0,0 +1,107 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttuser/promotionRecord.vue b/ruoyi-ui/src/views/skins/ttuser/promotionRecord.vue new file mode 100644 index 0000000..27ee20f --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/promotionRecord.vue @@ -0,0 +1,474 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttuser/spreadLevel.vue b/ruoyi-ui/src/views/skins/ttuser/spreadLevel.vue new file mode 100644 index 0000000..8724cbf --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/spreadLevel.vue @@ -0,0 +1,70 @@ + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttuser/userPack.vue b/ruoyi-ui/src/views/skins/ttuser/userPack.vue new file mode 100644 index 0000000..aec92c3 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/userPack.vue @@ -0,0 +1,391 @@ + + + + diff --git a/ruoyi-ui/src/views/skins/ttuser/userTurnover.vue b/ruoyi-ui/src/views/skins/ttuser/userTurnover.vue new file mode 100644 index 0000000..ed0dcd7 --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/userTurnover.vue @@ -0,0 +1,208 @@ + + + diff --git a/ruoyi-ui/src/views/skins/ttuser/vips.vue b/ruoyi-ui/src/views/skins/ttuser/vips.vue new file mode 100644 index 0000000..d76763b --- /dev/null +++ b/ruoyi-ui/src/views/skins/ttuser/vips.vue @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/config/index.vue b/ruoyi-ui/src/views/system/config/index.vue new file mode 100644 index 0000000..3ab81fc --- /dev/null +++ b/ruoyi-ui/src/views/system/config/index.vue @@ -0,0 +1,343 @@ + + + diff --git a/ruoyi-ui/src/views/system/dept/index.vue b/ruoyi-ui/src/views/system/dept/index.vue new file mode 100644 index 0000000..e502b4e --- /dev/null +++ b/ruoyi-ui/src/views/system/dept/index.vue @@ -0,0 +1,340 @@ + + + diff --git a/ruoyi-ui/src/views/system/dict/data.vue b/ruoyi-ui/src/views/system/dict/data.vue new file mode 100644 index 0000000..3befe4a --- /dev/null +++ b/ruoyi-ui/src/views/system/dict/data.vue @@ -0,0 +1,402 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/dict/index.vue b/ruoyi-ui/src/views/system/dict/index.vue new file mode 100644 index 0000000..6ca5457 --- /dev/null +++ b/ruoyi-ui/src/views/system/dict/index.vue @@ -0,0 +1,347 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/menu/index.vue b/ruoyi-ui/src/views/system/menu/index.vue new file mode 100644 index 0000000..c703fa0 --- /dev/null +++ b/ruoyi-ui/src/views/system/menu/index.vue @@ -0,0 +1,452 @@ + + + diff --git a/ruoyi-ui/src/views/system/notice/index.vue b/ruoyi-ui/src/views/system/notice/index.vue new file mode 100644 index 0000000..7982b54 --- /dev/null +++ b/ruoyi-ui/src/views/system/notice/index.vue @@ -0,0 +1,312 @@ + + + diff --git a/ruoyi-ui/src/views/system/post/index.vue b/ruoyi-ui/src/views/system/post/index.vue new file mode 100644 index 0000000..444bf63 --- /dev/null +++ b/ruoyi-ui/src/views/system/post/index.vue @@ -0,0 +1,309 @@ + + + diff --git a/ruoyi-ui/src/views/system/role/authUser.vue b/ruoyi-ui/src/views/system/role/authUser.vue new file mode 100644 index 0000000..147aa33 --- /dev/null +++ b/ruoyi-ui/src/views/system/role/authUser.vue @@ -0,0 +1,199 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/role/index.vue b/ruoyi-ui/src/views/system/role/index.vue new file mode 100644 index 0000000..fb3b5ef --- /dev/null +++ b/ruoyi-ui/src/views/system/role/index.vue @@ -0,0 +1,605 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/role/selectUser.vue b/ruoyi-ui/src/views/system/role/selectUser.vue new file mode 100644 index 0000000..b2b072f --- /dev/null +++ b/ruoyi-ui/src/views/system/role/selectUser.vue @@ -0,0 +1,138 @@ + + + diff --git a/ruoyi-ui/src/views/system/user/authRole.vue b/ruoyi-ui/src/views/system/user/authRole.vue new file mode 100644 index 0000000..ab5e72f --- /dev/null +++ b/ruoyi-ui/src/views/system/user/authRole.vue @@ -0,0 +1,117 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/user/index.vue b/ruoyi-ui/src/views/system/user/index.vue new file mode 100644 index 0000000..aff9566 --- /dev/null +++ b/ruoyi-ui/src/views/system/user/index.vue @@ -0,0 +1,620 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/user/profile/index.vue b/ruoyi-ui/src/views/system/user/profile/index.vue new file mode 100644 index 0000000..529c564 --- /dev/null +++ b/ruoyi-ui/src/views/system/user/profile/index.vue @@ -0,0 +1,91 @@ + + + diff --git a/ruoyi-ui/src/views/system/user/profile/resetPwd.vue b/ruoyi-ui/src/views/system/user/profile/resetPwd.vue new file mode 100644 index 0000000..64e8f8c --- /dev/null +++ b/ruoyi-ui/src/views/system/user/profile/resetPwd.vue @@ -0,0 +1,68 @@ + + + diff --git a/ruoyi-ui/src/views/system/user/profile/userAvatar.vue b/ruoyi-ui/src/views/system/user/profile/userAvatar.vue new file mode 100644 index 0000000..96aa01f --- /dev/null +++ b/ruoyi-ui/src/views/system/user/profile/userAvatar.vue @@ -0,0 +1,182 @@ + + + + diff --git a/ruoyi-ui/src/views/system/user/profile/userInfo.vue b/ruoyi-ui/src/views/system/user/profile/userInfo.vue new file mode 100644 index 0000000..c09a20b --- /dev/null +++ b/ruoyi-ui/src/views/system/user/profile/userInfo.vue @@ -0,0 +1,75 @@ + + + diff --git a/ruoyi-ui/src/views/tool/build/CodeTypeDialog.vue b/ruoyi-ui/src/views/tool/build/CodeTypeDialog.vue new file mode 100644 index 0000000..b5c2e2e --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/CodeTypeDialog.vue @@ -0,0 +1,106 @@ + + diff --git a/ruoyi-ui/src/views/tool/build/DraggableItem.vue b/ruoyi-ui/src/views/tool/build/DraggableItem.vue new file mode 100644 index 0000000..e881778 --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/DraggableItem.vue @@ -0,0 +1,100 @@ + diff --git a/ruoyi-ui/src/views/tool/build/IconsDialog.vue b/ruoyi-ui/src/views/tool/build/IconsDialog.vue new file mode 100644 index 0000000..958be50 --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/IconsDialog.vue @@ -0,0 +1,123 @@ + + + diff --git a/ruoyi-ui/src/views/tool/build/RightPanel.vue b/ruoyi-ui/src/views/tool/build/RightPanel.vue new file mode 100644 index 0000000..c2760eb --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/RightPanel.vue @@ -0,0 +1,946 @@ + + + + + diff --git a/ruoyi-ui/src/views/tool/build/TreeNodeDialog.vue b/ruoyi-ui/src/views/tool/build/TreeNodeDialog.vue new file mode 100644 index 0000000..fa7f0b2 --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/TreeNodeDialog.vue @@ -0,0 +1,149 @@ + + diff --git a/ruoyi-ui/src/views/tool/build/index.vue b/ruoyi-ui/src/views/tool/build/index.vue new file mode 100644 index 0000000..2bd298b --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/index.vue @@ -0,0 +1,768 @@ + + + + + diff --git a/ruoyi-ui/src/views/tool/gen/basicInfoForm.vue b/ruoyi-ui/src/views/tool/gen/basicInfoForm.vue new file mode 100644 index 0000000..7029529 --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/basicInfoForm.vue @@ -0,0 +1,60 @@ + + + diff --git a/ruoyi-ui/src/views/tool/gen/editTable.vue b/ruoyi-ui/src/views/tool/gen/editTable.vue new file mode 100644 index 0000000..951497a --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/editTable.vue @@ -0,0 +1,234 @@ + + + diff --git a/ruoyi-ui/src/views/tool/gen/genInfoForm.vue b/ruoyi-ui/src/views/tool/gen/genInfoForm.vue new file mode 100644 index 0000000..3f8d67f --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/genInfoForm.vue @@ -0,0 +1,299 @@ + + + diff --git a/ruoyi-ui/src/views/tool/gen/importTable.vue b/ruoyi-ui/src/views/tool/gen/importTable.vue new file mode 100644 index 0000000..3ea9532 --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/importTable.vue @@ -0,0 +1,120 @@ + + + diff --git a/ruoyi-ui/src/views/tool/gen/index.vue b/ruoyi-ui/src/views/tool/gen/index.vue new file mode 100644 index 0000000..3f1f930 --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/index.vue @@ -0,0 +1,337 @@ + + + diff --git a/ruoyi-ui/src/views/tool/swagger/index.vue b/ruoyi-ui/src/views/tool/swagger/index.vue new file mode 100644 index 0000000..aa32064 --- /dev/null +++ b/ruoyi-ui/src/views/tool/swagger/index.vue @@ -0,0 +1,17 @@ + + diff --git a/ruoyi-ui/src/views/ttRollHome.vue b/ruoyi-ui/src/views/ttRollHome.vue new file mode 100644 index 0000000..9eabf34 --- /dev/null +++ b/ruoyi-ui/src/views/ttRollHome.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/views/turnoverLottery/list.vue b/ruoyi-ui/src/views/turnoverLottery/list.vue new file mode 100644 index 0000000..cc53ceb --- /dev/null +++ b/ruoyi-ui/src/views/turnoverLottery/list.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/ruoyi-ui/src/views/turnoverLottery/prize.vue b/ruoyi-ui/src/views/turnoverLottery/prize.vue new file mode 100644 index 0000000..07dfb29 --- /dev/null +++ b/ruoyi-ui/src/views/turnoverLottery/prize.vue @@ -0,0 +1,562 @@ + + + + + diff --git a/ruoyi-ui/src/views/turnoverLottery/records.vue b/ruoyi-ui/src/views/turnoverLottery/records.vue new file mode 100644 index 0000000..0aa85bd --- /dev/null +++ b/ruoyi-ui/src/views/turnoverLottery/records.vue @@ -0,0 +1,126 @@ + + + diff --git a/ruoyi-ui/vue.config.js b/ruoyi-ui/vue.config.js new file mode 100644 index 0000000..3b07796 --- /dev/null +++ b/ruoyi-ui/vue.config.js @@ -0,0 +1,134 @@ +'use strict' +const path = require('path') + +function resolve(dir) { + return path.join(__dirname, dir) +} + +const CompressionPlugin = require('compression-webpack-plugin') + +const name = process.env.VUE_APP_TITLE || '后台管理系统' // 网页标题 + +const port = process.env.port || process.env.npm_config_port || 80 // 端口 + +// vue.config.js 配置说明 +//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions +// 这里只列一部分,具体配置参考文档 +module.exports = { + // 部署生产环境和开发环境下的URL。 + // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上 + // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。 + publicPath: process.env.NODE_ENV === "production" ? "/" : "/", + // 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist) + outputDir: 'dist', + // 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下) + assetsDir: 'static', + // 是否开启eslint保存检测,有效值:ture | false | 'error' + lintOnSave: process.env.NODE_ENV === 'development', + // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。 + productionSourceMap: false, + // webpack-dev-server 相关配置 + devServer: { + host: '0.0.0.0', + port: port, + open: true, + proxy: { + // detail: https://cli.vuejs.org/config/#devserver-proxy + [process.env.VUE_APP_BASE_API]: { + target: `http://localhost:8080`, + changeOrigin: true, + pathRewrite: { + ['^' + process.env.VUE_APP_BASE_API]: '' + } + } + }, + disableHostCheck: true + }, + css: { + loaderOptions: { + sass: { + sassOptions: { outputStyle: "expanded" } + } + } + }, + configureWebpack: { + name: name, + resolve: { + alias: { + '@': resolve('src') + } + }, + plugins: [ + // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件 + new CompressionPlugin({ + cache: false, // 不启用文件缓存 + test: /\.(js|css|html)?$/i, // 压缩文件格式 + filename: '[path].gz[query]', // 压缩后的文件名 + algorithm: 'gzip', // 使用gzip压缩 + minRatio: 0.8 // 压缩率小于1才会压缩 + }) + ], + }, + chainWebpack(config) { + config.plugins.delete('preload') // TODO: need test + config.plugins.delete('prefetch') // TODO: need test + + // set svg-sprite-loader + config.module + .rule('svg') + .exclude.add(resolve('src/assets/icons')) + .end() + config.module + .rule('icons') + .test(/\.svg$/) + .include.add(resolve('src/assets/icons')) + .end() + .use('svg-sprite-loader') + .loader('svg-sprite-loader') + .options({ + symbolId: 'icon-[name]' + }) + .end() + + config.when(process.env.NODE_ENV !== 'development', config => { + config + .plugin('ScriptExtHtmlWebpackPlugin') + .after('html') + .use('script-ext-html-webpack-plugin', [{ + // `runtime` must same as runtimeChunk name. default is `runtime` + inline: /runtime\..*\.js$/ + }]) + .end() + + config.optimization.splitChunks({ + chunks: 'all', + cacheGroups: { + libs: { + name: 'chunk-libs', + test: /[\\/]node_modules[\\/]/, + priority: 10, + chunks: 'initial' // only package third parties that are initially dependent + }, + elementUI: { + name: 'chunk-elementUI', // split elementUI into a single package + test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm + priority: 20 // the weight needs to be larger than libs and app or it will be packaged into libs or app + }, + commons: { + name: 'chunk-commons', + test: resolve('src/components'), // can customize your rules + minChunks: 3, // minimum common number + priority: 5, + reuseExistingChunk: true + } + } + }) + + config.optimization.runtimeChunk('single'), + { + from: path.resolve(__dirname, './public/robots.txt'), //防爬虫文件 + to: './' //到根目录下 + } + }) + } +} diff --git a/ry.bat b/ry.bat new file mode 100644 index 0000000..8294e13 --- /dev/null +++ b/ry.bat @@ -0,0 +1,67 @@ +@echo off + +rem jar平级目录 +set AppName=ruoyi-admin.jar + +rem JVM参数 +set JVM_OPTS="-Dname=%AppName% -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:NewRatio=1 -XX:SurvivorRatio=30 -XX:+UseParallelGC -XX:+UseParallelOldGC" + + +ECHO. + ECHO. [1] 启动%AppName% + ECHO. [2] 关闭%AppName% + ECHO. [3] 重启%AppName% + ECHO. [4] 启动状态 %AppName% + ECHO. [5] 退 出 +ECHO. + +ECHO.请输入选择项目的序号: +set /p ID= + IF "%id%"=="1" GOTO start + IF "%id%"=="2" GOTO stop + IF "%id%"=="3" GOTO restart + IF "%id%"=="4" GOTO status + IF "%id%"=="5" EXIT +PAUSE +:start + for /f "usebackq tokens=1-2" %%a in (`jps -l ^| findstr %AppName%`) do ( + set pid=%%a + set image_name=%%b + ) + if defined pid ( + echo %%is running + PAUSE + ) + +start javaw %JVM_OPTS% -jar %AppName% + +echo starting…… +echo Start %AppName% success... +goto:eof + +rem 函数stop通过jps命令查找pid并结束进程 +:stop + for /f "usebackq tokens=1-2" %%a in (`jps -l ^| findstr %AppName%`) do ( + set pid=%%a + set image_name=%%b + ) + if not defined pid (echo process %AppName% does not exists) else ( + echo prepare to kill %image_name% + echo start kill %pid% ... + rem 根据进程ID,kill进程 + taskkill /f /pid %pid% + ) +goto:eof +:restart + call :stop + call :start +goto:eof +:status + for /f "usebackq tokens=1-2" %%a in (`jps -l ^| findstr %AppName%`) do ( + set pid=%%a + set image_name=%%b + ) + if not defined pid (echo process %AppName% is dead ) else ( + echo %image_name% is running + ) +goto:eof diff --git a/ry.sh b/ry.sh new file mode 100644 index 0000000..282cc9e --- /dev/null +++ b/ry.sh @@ -0,0 +1,140 @@ +#!/bin/bash +# ./ry.sh start 启动 stop 停止 restart 重启 status 状态 +AppName=ruoyi-admin.jar + +export JAVA_HOME=/project/jdk-21.0.10 +export PATH=$JAVA_HOME/bin:$PATH + +# 自动检测 CPU 核数,选择 JVM 规格(可用 JVM_PROFILE 环境变量覆盖) +if [ -z "$JVM_PROFILE" ]; then + CPUS=$(nproc) + if [ "$CPUS" -le 16 ]; then + JVM_PROFILE="16c32g" + else + JVM_PROFILE="32c64g" + fi +fi + +case "$JVM_PROFILE" in + 16c32g) + HEAP_SIZE="10g" + GC_REGION="8m" + GC_PARALLEL="5" + GC_CONC="2" + ;; + 32c64g) + HEAP_SIZE="22g" + GC_REGION="16m" + GC_PARALLEL="10" + GC_CONC="3" + ;; + *) + echo "ERROR: unknown JVM_PROFILE=$JVM_PROFILE (supported: 16c32g, 32c64g)" + exit 1 + ;; +esac + +echo "JVM Profile: ${JVM_PROFILE} | Heap: ${HEAP_SIZE} | ParallelGCThreads=${GC_PARALLEL} ConcGCThreads=${GC_CONC}" + +# Spring Profile(可用 SPRING_PROFILE 环境变量覆盖) +SPRING_PROFILE="${SPRING_PROFILE:-pro}" + +# JVM参数 +LOG_DIR=/www/ruoyi/logs +JVM_OPTS="-Dname=$AppName -Duser.timezone=Asia/Shanghai -Xms${HEAP_SIZE} -Xmx${HEAP_SIZE} -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=${GC_REGION} -XX:ParallelGCThreads=${GC_PARALLEL} -XX:ConcGCThreads=${GC_CONC} -XX:InitiatingHeapOccupancyPercent=35 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/ -Xlog:gc*=info:file=${LOG_DIR}/gc.log:time,uptime:filecount=10,filesize=50M" + +if [ "$1" = "" ]; then + echo -e "\033[0;31m 未输入操作名 \033[0m \033[0;34m {start|stop|restart|status} \033[0m" + exit 1 +fi + +if [ "$AppName" = "" ]; then + echo -e "\033[0;31m 未输入应用名 \033[0m" + exit 1 +fi + +get_pid() { + ps -ef |grep java|grep $AppName|grep -v grep|awk '{print $2}' +} + +start() { + PID=`get_pid` + if [ x"$PID" != x"" ]; then + echo "$AppName is running..." + exit 1 + fi + + mkdir -p $LOG_DIR + + nohup java $JVM_OPTS -Dspring.profiles.active=$SPRING_PROFILE -jar $AppName > /dev/null 2>&1 & + + sleep 3 + PID=`get_pid` + if [ x"$PID" != x"" ]; then + echo "Start $AppName success..." + else + echo "ERROR: $AppName failed to start, check logs" + exit 1 + fi +} + +stop() { + echo "Stop $AppName" + + PID=`get_pid` + if [ x"$PID" = x"" ]; then + echo "$AppName already stopped." + return + fi + + TARGET_PID=$PID + kill -TERM $TARGET_PID + echo "$AppName (pid:$TARGET_PID) exiting..." + + timeout=10 + count=0 + while kill -0 $TARGET_PID 2>/dev/null; do + count=$((count + 1)) + if [ $count -gt $timeout ]; then + echo "$AppName graceful shutdown timed out after ${timeout}s, force killing..." + kill -9 $TARGET_PID 2>/dev/null || true + sleep 2 + if kill -0 $TARGET_PID 2>/dev/null; then + echo "ERROR: $AppName still alive (pid:$TARGET_PID) after kill -9, abort." + exit 1 + fi + echo "$AppName force killed." + return + fi + sleep 1 + done + echo "$AppName exited." +} + +restart() { + stop + start +} + +status() { + PID=`ps -ef |grep java|grep $AppName|grep -v grep|wc -l` + if [ $PID != 0 ];then + echo "$AppName is running..." + else + echo "$AppName is not running..." + fi +} + +case $1 in + start) + start;; + stop) + stop;; + restart) + restart;; + status) + status;; + *) + echo -e "\033[0;31m 未输入操作名 \033[0m \033[0;34m {start|stop|restart|status} \033[0m" + exit 1;; +esac diff --git a/skins-model/.gitignore b/skins-model/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/skins-model/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/skins-model/pom.xml b/skins-model/pom.xml new file mode 100644 index 0000000..a51cf0d --- /dev/null +++ b/skins-model/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.ruoyi + ruoyi + 4.8.2 + + + skins-model + + + + + com.ruoyi + ruoyi-common + + + cloud.tianai.captcha + tianai-captcha + 1.5.5 + compile + + + \ No newline at end of file diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/DeliveryOrderStatus.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/DeliveryOrderStatus.java new file mode 100644 index 0000000..b2e555c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/DeliveryOrderStatus.java @@ -0,0 +1,45 @@ +package com.ruoyi.domain.common.constant; + +// 发货订单状态(0发起提货 1待发货 3待收货 10订单完成 11订单取消) +public enum DeliveryOrderStatus { + + // 0发起提货(多余) + //CREATE_ORDER(0,"0发起提货"), + + // 1待发货 + DELIVERY_BEFORE(1,"1待发货"), + + // 2已发货 + DELIVERY_AFTER(2,"2已发货"), + + // 3客户确认已收货 + OWN_BEFORE(3,"3客户确认已收货"), + + // // 4客户确认已收货 + // OWN_BEFORE(4,"3客户确认已收货"), + + // 10订单完成 + ORDER_COMPLETE(10,"10订单完成"), + + //11订单取消 + ORDER_CANCEL(11,"11订单取消"), + + // 12订单异常冻结 + ORDER_FREEZE(12,"12订单异常冻结"); + + private final Integer code; + private final String msg; + + DeliveryOrderStatus(Integer code,String msg){ + this.code = code; + this.msg = msg; + } + + public Integer getCode(){ + return this.code; + } + public String getMsg(){ + return this.msg; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/DeliveryPattern.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/DeliveryPattern.java new file mode 100644 index 0000000..17dfcdc --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/DeliveryPattern.java @@ -0,0 +1,23 @@ +package com.ruoyi.domain.common.constant; + +// 提货方式 +public enum DeliveryPattern { + + // 手动 + MANUAL(1), + // 自动 + AUTO(2), + // 主播提货 + ANCHOR(3); + + private Integer code; + + DeliveryPattern(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/LockKey/UpgradeLock.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/LockKey/UpgradeLock.java new file mode 100644 index 0000000..73d2446 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/LockKey/UpgradeLock.java @@ -0,0 +1,17 @@ +package com.ruoyi.domain.common.constant.LockKey; + +public enum UpgradeLock { + + UPGRADE_LOCK("upgrade_lock:"); + + private String lock; + + UpgradeLock(String lock){ + this.lock = lock; + } + + public String getLock(){ + return this.lock; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/MessageCategoryEnum.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/MessageCategoryEnum.java new file mode 100644 index 0000000..e6191f9 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/MessageCategoryEnum.java @@ -0,0 +1,78 @@ +package com.ruoyi.domain.common.constant; + +/** + * 邮件/消息分类枚举 + * + * 用于区分邮件弹窗中“系统”与“游戏”两个 Tab: + * - GAME_* 前缀:归入游戏 Tab + * - SYS_* 前缀:归入系统 Tab + */ +public enum MessageCategoryEnum { + + // ==================== 游戏类消息 ==================== + GAME_PVP_TOP("GAME_PVP_TOP", "天选之战"), + GAME_PVP_LOW("GAME_PVP_LOW", "逆袭之战"), + GAME_BOX("GAME_BOX", "常规补给"), + GAME_LUCKY_UPGRADE("GAME_LUCKY_UPGRADE", "进阶风暴"), + GAME_WHEEL("GAME_WHEEL", "狂欢转盘"), + GAME_OLYMPUS("GAME_OLYMPUS", "玫瑰女郎"), + + // ==================== 系统类消息 ==================== + SYS_ROLL_OFFICIAL("SYS_ROLL_OFFICIAL", "Roll房"), + SYS_ROLL_STREAMER("SYS_ROLL_STREAMER", "Roll房"), + SYS_ANNOUNCEMENT("SYS_ANNOUNCEMENT", "系统公告"), + SYS_PERSONAL("SYS_PERSONAL", "个人公告"), + SYS_RANK_REWARD("SYS_RANK_REWARD", "消费排行"), + SYS_VIP_REBATE("SYS_VIP_REBATE", "VIP返利"), + SYS_PROMO_REBATE("SYS_PROMO_REBATE", "招商返利"), + SYS_RECHARGE("SYS_RECHARGE", "充值"), + SYS_EVENT_LOTTERY("SYS_EVENT_LOTTERY", "活动抽奖"), + SYS_TRANSFER("SYS_TRANSFER", "转赠"), + SYS_PICKUP("SYS_PICKUP", "提货"), + SYS_LOSS_REBATE("SYS_LOSS_REBATE", "亏损返利"); + + private final String code; + private final String title; + + MessageCategoryEnum(String code, String title) { + this.code = code; + this.title = title; + } + + public String getCode() { + return code; + } + + public String getTitle() { + return title; + } + + /** + * 是否属于游戏 Tab + */ + public boolean isGame() { + return code != null && code.startsWith("GAME_"); + } + + /** + * 是否属于系统 Tab + */ + public boolean isSystem() { + return code != null && code.startsWith("SYS_"); + } + + /** + * 根据 code 查找枚举 + */ + public static MessageCategoryEnum fromCode(String code) { + if (code == null || code.trim().isEmpty()) { + return null; + } + for (MessageCategoryEnum value : values()) { + if (value.code.equals(code)) { + return value; + } + } + return null; + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/PartyType.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/PartyType.java new file mode 100644 index 0000000..6228a0f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/PartyType.java @@ -0,0 +1,41 @@ +package com.ruoyi.domain.common.constant; + +// 第三方平台 +public enum PartyType { + + // 发货平台----------------- + // 扎比特 + ZBT(1), + + // yy有品 + YY_YOU_PING(2), + + // 支付平台------------------ + // 支付宝 + ZFB(3), + + // 微信 + VX(4), + + // 聚合支付 + JU_HE_ZHI_FU(5), + + // 九嘉支付宝 + JIU_JIA_ZFB(6), + + V5ITEM(7), + CS340(8), + INNER_PICTURE_PAY(9), + ; + + private final Integer code; + + PartyType(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/PayOrderStatus.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/PayOrderStatus.java new file mode 100644 index 0000000..7eab995 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/PayOrderStatus.java @@ -0,0 +1,35 @@ +package com.ruoyi.domain.common.constant; + +import lombok.Getter; + +// 支付订单状态 +@Getter +public enum PayOrderStatus { + + /** 订单状态:-1超时未付款 0未付款,1已付款,2已取消 */ + + // 0未付款 + NO_PAY("0"), + + // 用户支付成功 + PAY_YET("1"), + + // 用户支付失败 + PAY_FAIL("-1"), + + // 用户取消(超时未付款) + CANCEL("2"), + + // 用户已支付,但回调处理异常 + CALL_BACK_ERRO("3"), + + // 支付完成 + PAY_COMPLE("4"); + + private final String code; + + PayOrderStatus(String code){ + this.code = code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/PayType.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/PayType.java new file mode 100644 index 0000000..ffa3557 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/PayType.java @@ -0,0 +1,61 @@ +package com.ruoyi.domain.common.constant; + +// 支付方式 +public enum PayType { + + // 中云 支付宝 + ZY_ZFB("1"), + + // 卡密 + MK_CARD("2"), + + // 支付宝 + ZFB("3"), + + // 微信 + VX("4"), + + // 聚合支付(蚂蚁支付) + JU_HE_ZHI_FU("5"), + + // 九嘉支付宝 + JIU_JIA_ZFB("6"), + + // 星火支付宝 + XIN_HUO_ZFB("7"), + + //QS聚合支付 + QS_PAY("8"), + + // 国富汇通 + GFHT_PAY("9"), + + // hipay + HI_PAY("10"), + + // chengduhipay + CHENGDU_HI_PAY("11"), + + // RichKu + RICH_KU("12"), + + // itpay + IT_PAY("13"), + + // abkpay + ABK_PAY("14"), + + // snappay + SNAP_PAY("15"); + + private final String code; + + PayType(String code){ + this.code = code; + } + + public String getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtAccountRecordSource.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtAccountRecordSource.java new file mode 100644 index 0000000..bc1fd80 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtAccountRecordSource.java @@ -0,0 +1,281 @@ +package com.ruoyi.domain.common.constant; + +import java.util.Set; + +// 账户流水来源类型(这个类型金币和积分通用) +public enum TtAccountRecordSource { + + // 充值 + RECHARGE(1, TtAccountRecordType.INPUT, "充值收入"), + + // 经典开箱 + GAME_TYPE_01(2, TtAccountRecordType.OUTPUT, "经典开箱消费"), + + // 经典开箱游戏奖励 + GAME_TYPE_01_AWARD(3, TtAccountRecordType.INPUT, "经典开箱游戏奖励"), + + // 对战 + GAME_TYPE_02(4, TtAccountRecordType.OUTPUT, "对战消费"), + + // 对战游戏奖励 + GAME_TYPE_02_AWARD(5, TtAccountRecordType.INPUT, "对战游戏奖励"), + + // roll房 + GAME_TYPE_03(6, TtAccountRecordType.OUTPUT, "roll房消费"), + + // roll房游戏奖励 + GAME_TYPE_03_AWARD(7, TtAccountRecordType.INPUT, "roll房游戏奖励"), + + // 逐梦前行 + GAME_TYPE_04(8, TtAccountRecordType.OUTPUT, "逐梦前行消费"), + + // 逐梦前行游戏奖励 + GAME_TYPE_04_AWARD(9, TtAccountRecordType.INPUT, "逐梦前行游戏奖励"), + + // 分解饰品 + DECOMPOSE_ORNAMENT(10, TtAccountRecordType.INPUT, "背包分解饰品收入"), + + // 任务 + TASK(11, TtAccountRecordType.INPUT, "首次下载任务奖励"), + + // 商城兑换 + EXCHANGE(12, TtAccountRecordType.OUTPUT, "商城兑换消费"), + + // 推广福利 + P_WELFARE(13, TtAccountRecordType.INPUT, "推广福利收入"), + + // 后台操作 + ADMIN_UPDATA(14, TtAccountRecordType.INPUT, "后台操作"), + + // 奖励津贴(暂时没啥用) + BONUS(15, TtAccountRecordType.INPUT, "奖励津贴"), + + // 积分兑换金币 + INTEGRATING_CONVERSION(16, TtAccountRecordType.INPUT, "积分兑换金币"), + + // 口令红包 + RECEIVE_RED_PACKET(17, TtAccountRecordType.INPUT, "福利CDK收入"), + + // 注册奖励 + REGIST_AWARD(18, TtAccountRecordType.INPUT, "注册奖励"), + + RANK_BLEND_ERCASH(19, TtAccountRecordType.INPUT, "综合流水排行榜奖励"), + RANK_AMOUNT(20, TtAccountRecordType.INPUT, "金币消费排行榜奖励"), + RANK_CREDITS(21, TtAccountRecordType.INPUT, "积分消费排行榜奖励"), + RANK_PROD_ORN(22, TtAccountRecordType.INPUT, "出货排行榜奖励"), + + FIRST_CHARGE(23, TtAccountRecordType.INPUT, "首充赠送"), + + PROMOTION_LEVEL_CHARGE(24, TtAccountRecordType.INPUT, "推广等级充值赠送"), + + // 充值红包 + CHARGE_RED_PACKET(25, TtAccountRecordType.INPUT, "充值红包收入"), + + // 推广等级达标 + PROMOTION_LEVEL_REACH(26, TtAccountRecordType.INPUT, "推广等级达标赠送"), + + // 推广充值返佣 + PROMOTION_CHARGE_COMMISSION(27, TtAccountRecordType.INPUT, "充值返利"), + + // 对战排行榜奖励 + FIGHT_RANKING_REWARD(28, TtAccountRecordType.INPUT, "对战排行榜奖励"), + + // 消费排行榜奖励 + RECHARGE_RANKING_REWARD(29, TtAccountRecordType.INPUT, "消费排行榜奖励"), + + // VIP等级达标 + VIP_LEVEL_REACH(30, TtAccountRecordType.INPUT, "VIP等级达标赠送"), + + // 主播发放工资 + PROMOTION_RECHARGE(31, TtAccountRecordType.INPUT, "主播日常发放工资"), + + // 充值金额达到门槛奖励 + RECHARGE_RESEARCH_BONUS(32, TtAccountRecordType.INPUT, "充值金额达到门槛奖励"), + + // 玩转指数盲盒 + EXPONENT_GAME_TYPE_01(33, TtAccountRecordType.OUTPUT, "玩转指数盲盒消费"), + + BULLET_SPIN(34, TtAccountRecordType.OUTPUT, "荒野争锋消费"), + BULLET_REWARD(37, TtAccountRecordType.INPUT, "荒野争锋奖励"), + + LUCKLY_WHEEL_SPIN(38, TtAccountRecordType.OUTPUT, "疯狂转盘消费"), + LUCKLY_WHEEL_REWARD(39, TtAccountRecordType.INPUT, "疯狂转盘奖励"), + + GOLD_DEVICE(40, TtAccountRecordType.OUTPUT, "幸运升级消费"), + GOLD_DEVICE_REWARD(41, TtAccountRecordType.INPUT, "幸运升级奖励"), + + ZHUBO_EXTRACT(42, TtAccountRecordType.OUTPUT, "主播后台提取消费"), + + RECHARGE_AWARD(43, TtAccountRecordType.INPUT, "充值福利奖励"), + VIP_CONSUME_AWARD(44, TtAccountRecordType.INPUT, "VIP流水奖励"), + + CARD_MERCHANT_GENERATE(45, TtAccountRecordType.OUTPUT, "卡商生成卡密消费"), + + CARD_MERCHANT_RECHARGE(46, TtAccountRecordType.INPUT, "卡商充值收入"), + + // 游戏消费返佣(下级游戏消费产生的佣金发放到上级)- 特殊游戏(新游戏) + GAME_CONSUME_COMMISSION(47, TtAccountRecordType.INPUT, "特殊游戏返利"), + + // 经典游戏消费返佣(经典玩法消费产生的佣金发放到上级) + CLASSIC_GAME_CONSUME_COMMISSION(49, TtAccountRecordType.INPUT, "经典游戏返利"), + + // 推广CDKEY兑换收入 + PROMO_CDKEY_REDEEM(48, TtAccountRecordType.INPUT, "推广CDKEY兑换收入"), + + // 主播创建CDKEY消费 + PROMO_CDKEY_GENERATE(53, TtAccountRecordType.OUTPUT, "主播创建CDKEY消费"), + + // 主播CDKEY过期/兑完结束后的预扣差额退还 + PROMO_CDKEY_REFUND(54, TtAccountRecordType.INPUT, "主播CDKEY结束退还"), + + // 卡商主动销毁未使用卡密,按面值退回卡商余额 + CARD_MERCHANT_DESTROY_REFUND(55, TtAccountRecordType.INPUT, "卡商销毁卡密退还"), + + // Roll房未发放饰品折算退回主播(开奖后未抽出的剩余饰品 / 未开奖删除全额退回),与玩家Roll房奖励(7)区分 + ROLL_UNISSUED_REFUND(56, TtAccountRecordType.INPUT, "Roll房未发放退回主播"), + + // 管理后台生成CDK口令红包扣款 + ADMIN_RED_PACKET_GENERATE(57, TtAccountRecordType.OUTPUT, "管理后台生成主播CDK红包扣款"), + + // 福利开箱奖励 + WELFARE_BOX_REWARD(58, TtAccountRecordType.INPUT, "福利开箱奖励"), + + // 主播Roll房奖池扣款(创建房/向奖池追加饰品时扣减主播余额)。 + // 注意:不能复用 GAME_TYPE_03(6),否则会被计入 getGameBetCodes() 的"游戏投注流水", + // 导致主播建房+删房循环刷 VIP 流水返利/消费排行榜奖励并污染投注统计。 + PROMO_ROLL_DEDUCT(59, TtAccountRecordType.OUTPUT, "主播Roll房奖池扣款"), + + // 玫瑰女郎电子游戏消费 + OLYMPUS_SPIN(60, TtAccountRecordType.OUTPUT, "玫瑰女郎消费"), + + // 玫瑰女郎电子游戏奖励 + OLYMPUS_REWARD(61, TtAccountRecordType.INPUT, "玫瑰女郎奖励"), + + // 招商周结佣金(每周一结算上一周,与主播实时返佣 27/47/49 区分) + BUSINESS_WEEKLY_COMMISSION(62, TtAccountRecordType.INPUT, "招商周结佣金"), + + // 流水抽奖奖励 (仅用于奖励发放记录) + TURNOVER_LOTTERY_REWARD(63, TtAccountRecordType.INPUT, "流水抽奖奖励"), + + // 疯狂转盘注单退款(兜底结算专用,与消费/奖励区分以便审计) + WHEEL_BET_REFUND(64, TtAccountRecordType.INPUT, "疯狂转盘注单退款"), + + // 管理员手动调整用户余额(写入余额变动流水,操作人/IP 见 remark) + ADMIN_ADJUST_BALANCE(65, TtAccountRecordType.INPUT, "管理员调整余额"), + + // 管理员手动调整用户积分(写入积分变动流水,操作人/IP 见 remark) + ADMIN_ADJUST_CREDITS(66, TtAccountRecordType.INPUT, "管理员调整积分"), + + // 管理员手动调整卡商余额(写入卡商余额变动流水,操作人/IP 见 remark;与 45/46/55 一样复用 credits 列记账) + ADMIN_ADJUST_MERCHANT_BALANCE(67, TtAccountRecordType.INPUT, "管理员调整卡商余额") + ; + + private final Integer code; + private final TtAccountRecordType type; + private final String msg; + + + TtAccountRecordSource(Integer code, TtAccountRecordType t, String msg) { + this.code = code; + this.msg = msg; + this.type = t; + } + + public Integer getCode() { + return this.code; + } + + public String getMsg() { + return this.msg; + } + + // 计算推广福利的流水类型 + public static Set getWelfarePrizeCodes() { + return Set.of(RECHARGE.getCode(), + GAME_TYPE_01.getCode(), + GAME_TYPE_02.getCode(), + GOLD_DEVICE.getCode(), + LUCKLY_WHEEL_SPIN.getCode(), + OLYMPUS_SPIN.getCode()); + } + + // 计算流水返利的游戏类型 + public static Set getGameConsumeCodes() { + return Set.of(GAME_TYPE_01.getCode(), + GAME_TYPE_02.getCode(), + GOLD_DEVICE.getCode(), + LUCKLY_WHEEL_SPIN.getCode(), + OLYMPUS_SPIN.getCode()); + } + + // 今日总投注:所有游戏消费 source + public static Set getGameBetCodes() { + return Set.of( + GAME_TYPE_01.getCode(), // 经典开箱消费 + GAME_TYPE_02.getCode(), // 对战消费 + GAME_TYPE_03.getCode(), // roll 房消费 + LUCKLY_WHEEL_SPIN.getCode(), // 疯狂转盘消费 + GOLD_DEVICE.getCode(), // 幸运升级消费 + OLYMPUS_SPIN.getCode() // 玫瑰女郎消费 + ); + } + + // 今日总赢奖:所有游戏奖励 source + public static Set getGameRewardCodes() { + return Set.of( + GAME_TYPE_01_AWARD.getCode(), // 经典开箱奖励 + GAME_TYPE_02_AWARD.getCode(), // 对战奖励 + GAME_TYPE_03_AWARD.getCode(), // roll房奖励 + GOLD_DEVICE_REWARD.getCode(), // 幸运升级奖励 + LUCKLY_WHEEL_REWARD.getCode(), // 疯狂转盘奖励 + OLYMPUS_REWARD.getCode() // 玫瑰女郎奖励 + ); + } + + /** 普通游戏消费:经典开箱、对战、roll房、幸运升级、疯狂转盘 */ + public static boolean isNormalGameConsume(int code) { + return code == GAME_TYPE_01.getCode() + || code == GAME_TYPE_02.getCode() + || code == GAME_TYPE_03.getCode() + || code == GOLD_DEVICE.getCode() + || code == LUCKLY_WHEEL_SPIN.getCode(); + } + + /** 新游戏消费:玫瑰女郎 */ + public static boolean isSpecialGameConsume(int code) { + return code == OLYMPUS_SPIN.getCode(); + } + + /** 新游戏消费(对应 newGameCommissionRate):玫瑰女郎 */ + public static boolean isNewGameConsumeForCommission(int code) { + return code == OLYMPUS_SPIN.getCode(); + } + + /** 经典游戏消费(对应 classicGameCommissionRate):经典开箱、对战、幸运升级、疯狂转盘 */ + public static boolean isClassicGameConsumeForCommission(int code) { + return code == GAME_TYPE_01.getCode() + || code == GAME_TYPE_02.getCode() + || code == GOLD_DEVICE.getCode() + || code == LUCKLY_WHEEL_SPIN.getCode(); + } + + /** 是否是需要分发佣金的游戏消费类型 */ + public static boolean isGameConsumeForCommission(int code) { + return isNewGameConsumeForCommission(code) || isClassicGameConsumeForCommission(code); + } + + /** + * 获取符合流水抽奖条件的游戏消费类型 + * 排除 Roll 房 (6)、商城 (12)、福利类等 + */ + public static Set getTurnoverLotteryCodes() { + return Set.of( + GAME_TYPE_01.getCode(), // 经典开箱消费 + GAME_TYPE_02.getCode(), // 对战消费 + GOLD_DEVICE.getCode(), // 幸运升级消费 + LUCKLY_WHEEL_SPIN.getCode(), // 疯狂转盘消费 + OLYMPUS_SPIN.getCode() // 玫瑰女郎消费 + ); + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtAccountRecordType.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtAccountRecordType.java new file mode 100644 index 0000000..8e6c4ca --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtAccountRecordType.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.common.constant; + +// 流水类型 +public enum TtAccountRecordType { + + // 收入 + INPUT(1), + // 支出 + OUTPUT(0); + + private Integer code; + + TtAccountRecordType(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtboxRecordSource.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtboxRecordSource.java new file mode 100644 index 0000000..d5ebf41 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtboxRecordSource.java @@ -0,0 +1,50 @@ +package com.ruoyi.domain.common.constant; + +// 账户流水来源类型 +// 注意和TtboxRecordSource区分 +public enum TtboxRecordSource { + + // 玩转盲盒 + BLIND_BOX(1,"玩转盲盒"), + + // 急速对战 + FIGHT(2,"急速对战"), + + // roll房 + ROLL(3,"roll房"), + + // 急速对战 + UPGRADE(4,"幸运升级"), + // roll房 + SYS_GRANT(5,"系统发放"), + + MALL_EXCHANGE(6,"商城兑换"), + + REPLACEMENT(7,"汰换"), + EXPONENT_BOX(8,"指数盲盒"), + TRANSFER(9,"转赠"), + WELFARE_BOX(10,"福利开箱"), + BULLET_BOX(12,"荒野争锋"), + OLYMPUS_BOX(13,"玫瑰女郎"), + WHEEL_BOX(99,"疯狂转盘"), + TURNOVER_LOTTERY(14,"流水抽奖"), + ; + + private Integer code; + + private String msg; + + TtboxRecordSource(Integer code, String msg){ + this.code = code; + this.msg = msg; + } + + public Integer getCode(){ + return this.code; + } + + public String getMsg(){ + return this.msg; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtboxRecordStatus.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtboxRecordStatus.java new file mode 100644 index 0000000..33ae730 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/TtboxRecordStatus.java @@ -0,0 +1,34 @@ +package com.ruoyi.domain.common.constant; + +// 开箱记录状态/背包饰品状态 +public enum TtboxRecordStatus { + + IN_PACKSACK_ON(0,"在背包显示"), + + // 用于对战模式,游戏完全结束才显示 + IN_PACKSACK_OFF(1,"在背包不显示"), + APPLY_DELIVERY(2,"申请提货"), + DELIVERY_YET(3,"已经提货"), + RESOLVE(5,"分解"), + ADMIN_DELETE(10,"管理员删除"), + // 转赠审核中:饰品不在任何人背包显示,等待后台审核通过或驳回 + TRANSFER_PENDING(20,"转赠审核中"); + + private final Integer code; + + private final String msg; + + TtboxRecordStatus(Integer code, String msg){ + this.code = code; + this.msg = msg; + } + + public Integer getCode(){ + return this.code; + } + + public String getMsg(){ + return this.msg; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/UserType.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/UserType.java new file mode 100644 index 0000000..4d19ee2 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/UserType.java @@ -0,0 +1,41 @@ +package com.ruoyi.domain.common.constant; + +//用户类型 +public enum UserType { + + // 主播 + ANCHOR("01"), + // 普通用户 + COMMON_USER("02"), + // 机器人 + ROBOT_USER("03"), + // 招商号 + BUSINESS_USER("04"), + // 卡商 + CARD_MERCHANT("05"), + // 白号 + BLANK_ACCOUNT("06"), + // 超级管理员 + SUPER_ADMIN("99"), + ; + + private String code; + + UserType(String code){ + this.code = code; + } + + public String getCode(){ + return this.code; + } + + public static UserType fromCode(String code){ + for (UserType userType : UserType.values()){ + if (userType.getCode().equals(code)){ + return userType; + } + } + return null; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/roll/RollGetPrizeWay.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/roll/RollGetPrizeWay.java new file mode 100644 index 0000000..1f8ef39 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/roll/RollGetPrizeWay.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.common.constant.roll; + +// roll房玩家获奖方式 +public enum RollGetPrizeWay { + + // 系统指定 + SYS(0), + // 随机 + COMMON(1); + + private final Integer code; + + RollGetPrizeWay(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/roll/RollStatus.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/roll/RollStatus.java new file mode 100644 index 0000000..612fc26 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/roll/RollStatus.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.common.constant.roll; + +// roll房状态 +public enum RollStatus { + + // 未开奖 + ING("0"), + // 已开奖 + END("1"); + + private final String code; + + RollStatus(String code){ + this.code = code; + } + + public String getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/roll/RollType.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/roll/RollType.java new file mode 100644 index 0000000..df98885 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/roll/RollType.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.common.constant.roll; + +// roll房状态 +public enum RollType { + + // 主播 + ANCHOR("1"), + // 官方 + OFFICIAL("0"); + + private final String code; + + RollType(String code){ + this.code = code; + } + + public String getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/sys/MoneyType.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/sys/MoneyType.java new file mode 100644 index 0000000..74275e7 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/sys/MoneyType.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.common.constant.sys; + +//用户类型 +public enum MoneyType { + + // 金币 + GOLD(1), + // 积分 + CREDITS(2); + + private final Integer code; + + MoneyType(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/common/constant/sys/UserStatus.java b/skins-model/src/main/java/com/ruoyi/domain/common/constant/sys/UserStatus.java new file mode 100644 index 0000000..4838991 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/common/constant/sys/UserStatus.java @@ -0,0 +1,18 @@ +package com.ruoyi.domain.common.constant.sys; + +public enum UserStatus { + + // 金币 + NORMAL(0); + + private final Integer code; + + UserStatus(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/ChildDetailByBoss.java b/skins-model/src/main/java/com/ruoyi/domain/dto/ChildDetailByBoss.java new file mode 100644 index 0000000..b68f6a1 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/ChildDetailByBoss.java @@ -0,0 +1,40 @@ +package com.ruoyi.domain.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ChildDetailByBoss { + + private Integer bossId; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private String beginTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private String endTime; + + @Min(value = 1,message = "最小1") + @NotNull(message = "页码不能为空") + private Integer page; + + @Range(min = 1,max = 20) + @NotNull(message = "分页长度不能为空") + private Integer size; + + // 消费正序1 消费逆序2 + @NotNull(message = "排序方式不能为空") + private Integer orderType; + + private Integer lim; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/BoxRecordPageRequest.java b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/BoxRecordPageRequest.java new file mode 100644 index 0000000..80c4755 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/BoxRecordPageRequest.java @@ -0,0 +1,23 @@ +package com.ruoyi.domain.dto.boxRecords; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.Min; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class BoxRecordPageRequest { + + @Min(value = 1, message = "页码最小1") + private Integer page; + + @Range(min = 1, max = 50, message = "每页条数1-50") + private Integer size; + + /** 当前选中的箱子id,传入后仅返回该箱子的开箱记录 */ + private Integer boxId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/BoxRecordPageResult.java b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/BoxRecordPageResult.java new file mode 100644 index 0000000..e69de29 diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TenTopQuery.java b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TenTopQuery.java new file mode 100644 index 0000000..7eb7382 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TenTopQuery.java @@ -0,0 +1,30 @@ +package com.ruoyi.domain.dto.boxRecords; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.Min; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class TenTopQuery { + + // 来源 + private List source; + + // 状态 + private List status; + + @Min(value = 1,message = "最小1") + private Integer page; + @Range(min = 1,max = 20) + private Integer size; + + private Integer limit; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TtBoxRecordsNum.java b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TtBoxRecordsNum.java new file mode 100644 index 0000000..485d256 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TtBoxRecordsNum.java @@ -0,0 +1,38 @@ +package com.ruoyi.domain.dto.boxRecords; + +import lombok.Data; + +@Data +public class TtBoxRecordsNum { + + private Integer boxId; + private Long ornamentId; + /** + * 数量(全部非白号开箱记录总数) + */ + private Integer num; + /** + * 非主播非机器人(普通玩家/招商/卡商/超管等,含历史删号)开箱数,对应真实爆率池 + */ + private Integer realNum; + /** + * 主播(user_type=01)开箱数,对应主播爆率池 + */ + private Integer anchorNum; + /** + * 机器人(user_type=03)开箱数,对应机器人普通爆率池(非欧皇/非酋对战的机器人记录) + */ + private Integer robotNum; + /** + * 机器人欧皇对战(tt_fight.model=0)开箱数,对应机器人爆率(欧皇)池 + */ + private Integer robotLuckyNum; + /** + * 机器人非酋对战(tt_fight.model=1)开箱数,对应机器人爆率 (非酋)池 + */ + private Integer robotUnluckyNum; + /** + * 白号(is_blank_account=1)开箱数,对应白号爆率池 + */ + private Integer whiteNum; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TtBoxRecordsPoolStat.java b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TtBoxRecordsPoolStat.java new file mode 100644 index 0000000..98c0533 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TtBoxRecordsPoolStat.java @@ -0,0 +1,50 @@ +package com.ruoyi.domain.dto.boxRecords; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 某宝箱某天的开箱记录按开箱人类型(奖池)汇总 + * 口径与 TtBoxRecordsMapper.selectBoxRecordsByOrnamentIds 的分池规则保持一致 + */ +@Data +public class TtBoxRecordsPoolStat { + + /** + * 主播(user_type=01),对应主播池 + */ + private Integer anchorNum; + private BigDecimal anchorPrice; + + /** + * 非主播非机器人非白号(普通/招商/卡商/超管等),对应真实池 + */ + private Integer realNum; + private BigDecimal realPrice; + + /** + * 机器人(user_type=03)且对战模式非欧皇/非酋,对应机器人池 + */ + private Integer robotNum; + private BigDecimal robotPrice; + + /** + * 机器人欧皇对战(tt_fight.model=0),对应机器人欧皇池 + */ + private Integer robotLuckyNum; + private BigDecimal robotLuckyPrice; + + /** + * 机器人非酋对战(tt_fight.model=1),对应机器人非酋池 + */ + private Integer robotUnluckyNum; + private BigDecimal robotUnluckyPrice; + + /** + * 白号(is_blank_account=1),对应白号池 + */ + private Integer whiteNum; + private BigDecimal whitePrice; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TtBoxRecordsRemark.java b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TtBoxRecordsRemark.java new file mode 100644 index 0000000..0e0ae3f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/TtBoxRecordsRemark.java @@ -0,0 +1,9 @@ +package com.ruoyi.domain.dto.boxRecords; + +import lombok.Data; + +@Data +public class TtBoxRecordsRemark { + + private String realOddsChange; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/queryCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/queryCondition.java new file mode 100644 index 0000000..501dde6 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/boxRecords/queryCondition.java @@ -0,0 +1,56 @@ +package com.ruoyi.domain.dto.boxRecords; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.Min; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class queryCondition { + + private Long boxRecordId; + + // 用户ID + private Integer userId; + + // 宝箱id + private Integer boxId; + + // 用户类型 + private String userType; + + // 来源 + private List source; + + // 物品价格区间 + @Min(value = 0,message = "最小0") + private BigDecimal ornamentPriceMin; + @Min(value = 0,message = "最小0") + private BigDecimal ornamentPriceMax; + + // 物品等级 + private List ornamentLevelIds; + + // 状态 + private List status; + + // 0时间升序 1时间降序 + private Integer orderByFie; + + @Min(value = 1,message = "最小1") + private Integer page; + @Range(min = 1,max = 20) + private Integer size; + + private Integer limit; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/deliver/TradeBuyParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/deliver/TradeBuyParam.java new file mode 100644 index 0000000..259431b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/deliver/TradeBuyParam.java @@ -0,0 +1,25 @@ +package com.ruoyi.domain.dto.deliver; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class TradeBuyParam { + + // 提货记录id + private Integer deliveryRecordId; + + // 提货平台 + private Integer partyType; + + // 平台的物品在售记录id + private Long productId; + + /** 物品价格 */ + private BigDecimal price; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/deliver/TradeManualConfirmParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/deliver/TradeManualConfirmParam.java new file mode 100644 index 0000000..6f401e5 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/deliver/TradeManualConfirmParam.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.dto.deliver; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class TradeManualConfirmParam { + + // 提货记录id + private Integer deliveryRecordId; + + /** 物品价格 */ + private BigDecimal price; + /** + * 交易订单号 + */ + private String orderNo; + /** + * 发货状态 + */ + private int status; + /** + * 发货原因 + */ + private String message; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentBoxVo.java b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentBoxVo.java new file mode 100644 index 0000000..c91bd46 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentBoxVo.java @@ -0,0 +1,18 @@ +package com.ruoyi.domain.dto.exponent; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ExponentBoxVo { + private Integer boxId; + private Integer boxTypeId; + private String boxName; + private BigDecimal price; + private String boxImg01; + private String boxImg02; + private String boxType; + private int buy_forward_up; + private int buy_forward_down; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentInfoVo.java b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentInfoVo.java new file mode 100644 index 0000000..23e924c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentInfoVo.java @@ -0,0 +1,79 @@ +package com.ruoyi.domain.dto.exponent; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * { + * "id": 361971, + * "exponent": 0, + * "period": "202412292560", + * "end_time": 1735481055, + * "points": "", + * "state": 0, + * "number": 2560, + * "forward": 0, + * "createtime": 1735481025, + * "updatetime": 1735481025, + * "timer": 17, + * "last": { + * "id": 361970, + * "exponent": 30003467, + * "period": "202412292559", + * "end_time": 1735481024, + * "points": "30000833,29994529,30007775,30006661,30002185,30005613,30003447,29991295,29990895,29995306,30003467", + * "state": 2, + * "number": 2559, + * "forward": 1, + * "createtime": 1735480994, + * "updatetime": 1735480994 + * }, + * "buy": 0, + * "buy_forward_up": 0, + * "buy_forward_down": 0, + * "jieguo": { + * "skinarr": [ + * { + * "id": 297, + * "image": "https:\/\/guangdun.oss-cn-beijing.aliyuncs.com\/uploads\/20241030\/9839b86148a39d2c69ea882dc40cccc3.webp", + * "count": 1 + * } + * ], + * "period": "202412292559", + * "exponent": 30003467, + * "forward": 1, + * "buy_forward_down": 0, + * "buy_forward_up": 10, + * "buy": 10, + * "buy_money": 10 + * } + * } + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class ExponentInfoVo { + private Integer id; + private Integer exponent; + private String period; + private Integer end_time; + private String points; + private Integer state; + private Integer number; + private Integer forward; + private Integer createtime; + private Integer updatetime; + private Integer timer; + private ExponentInfoVo last; + private Integer buy = 0; + private BigDecimal buy_forward_up = BigDecimal.ZERO; + private BigDecimal buy_forward_down = BigDecimal.ZERO; + private ExponentJieguoVo jieguo; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentJieguoVo.java b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentJieguoVo.java new file mode 100644 index 0000000..460f65b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentJieguoVo.java @@ -0,0 +1,72 @@ +package com.ruoyi.domain.dto.exponent; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.List; + +/** + * { + * "id": 361971, + * "exponent": 0, + * "period": "202412292560", + * "end_time": 1735481055, + * "points": "", + * "state": 0, + * "number": 2560, + * "forward": 0, + * "createtime": 1735481025, + * "updatetime": 1735481025, + * "timer": 17, + * "last": { + * "id": 361970, + * "exponent": 30003467, + * "period": "202412292559", + * "end_time": 1735481024, + * "points": "30000833,29994529,30007775,30006661,30002185,30005613,30003447,29991295,29990895,29995306,30003467", + * "state": 2, + * "number": 2559, + * "forward": 1, + * "createtime": 1735480994, + * "updatetime": 1735480994 + * }, + * "buy": 0, + * "buy_forward_up": 0, + * "buy_forward_down": 0, + * "jieguo": { + * "skinarr": [ + * { + * "id": 297, + * "image": "https:\/\/guangdun.oss-cn-beijing.aliyuncs.com\/uploads\/20241030\/9839b86148a39d2c69ea882dc40cccc3.webp", + * "count": 1 + * } + * ], + * "period": "202412292559", + * "exponent": 30003467, + * "forward": 1, + * "buy_forward_down": 0, + * "buy_forward_up": 10, + * "buy": 10, + * "buy_money": 10 + * } + * } + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class ExponentJieguoVo { + private List skinarr; + private Integer exponent; + private String period; + private Integer forward; + private Integer buy = 0; + private BigDecimal buy_money = BigDecimal.ZERO; + private BigDecimal buy_forward_up = BigDecimal.ZERO; + private BigDecimal buy_forward_down = BigDecimal.ZERO; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentJoinParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentJoinParam.java new file mode 100644 index 0000000..8965bdf --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentJoinParam.java @@ -0,0 +1,25 @@ +package com.ruoyi.domain.dto.exponent; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class ExponentJoinParam { + /** + * 指数ID + */ + private Integer id; + /** + * 盒子ID + */ + private Integer boxId; + /** + * 1上升2下降 + */ + private Integer forward; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentOpenBoxVo.java b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentOpenBoxVo.java new file mode 100644 index 0000000..5833b79 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentOpenBoxVo.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.dto.exponent; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +@Data +public class ExponentOpenBoxVo { + private Integer id; + private Integer forward; + private Integer boxId; + private Integer success; + private Integer failed; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + private String image; + private String image2; + // 1成功 2失败 + private int state; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentSkinarrVo.java b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentSkinarrVo.java new file mode 100644 index 0000000..bd40118 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/exponent/ExponentSkinarrVo.java @@ -0,0 +1,65 @@ +package com.ruoyi.domain.dto.exponent; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * { + * "id": 361971, + * "exponent": 0, + * "period": "202412292560", + * "end_time": 1735481055, + * "points": "", + * "state": 0, + * "number": 2560, + * "forward": 0, + * "createtime": 1735481025, + * "updatetime": 1735481025, + * "timer": 17, + * "last": { + * "id": 361970, + * "exponent": 30003467, + * "period": "202412292559", + * "end_time": 1735481024, + * "points": "30000833,29994529,30007775,30006661,30002185,30005613,30003447,29991295,29990895,29995306,30003467", + * "state": 2, + * "number": 2559, + * "forward": 1, + * "createtime": 1735480994, + * "updatetime": 1735480994 + * }, + * "buy": 0, + * "buy_forward_up": 0, + * "buy_forward_down": 0, + * "jieguo": { + * "skinarr": [ + * { + * "id": 297, + * "image": "https:\/\/guangdun.oss-cn-beijing.aliyuncs.com\/uploads\/20241030\/9839b86148a39d2c69ea882dc40cccc3.webp", + * "count": 1 + * } + * ], + * "period": "202412292559", + * "exponent": 30003467, + * "forward": 1, + * "buy_forward_down": 0, + * "buy_forward_up": 10, + * "buy": 10, + * "buy_money": 10 + * } + * } + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class ExponentSkinarrVo { + private Integer id; + private String image; + private String image2; + private Integer count; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/fight/BaiJiaFightParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/fight/BaiJiaFightParam.java new file mode 100644 index 0000000..cc0f2a2 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/fight/BaiJiaFightParam.java @@ -0,0 +1,26 @@ +package com.ruoyi.domain.dto.fight; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class BaiJiaFightParam { + + // 0:超神(全历史总产出最高),1:最大金额(今日总产出最高),2:最大增长(今日净赚最多),3:最大倍数(今日回报率最高) + private Integer maxType; + + // 模式: 0-欧皇, 1-非酋 + private String model; + + // 价格区间下限(含) + private Integer minPrice; + + // 价格区间上限(含) + private Integer maxPrice; + + // 房主昵称关键词(模糊搜索tt_user.nick_name) + private String keyword; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/fight/FightDetailParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/fight/FightDetailParam.java new file mode 100644 index 0000000..4348802 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/fight/FightDetailParam.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.dto.fight; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class FightDetailParam { + + @NotNull(message = "fightId不能为空") + private Integer fightId; + + /** 指定入座的座位号(可选;null=自动坐第一个空位),与 FightSeat.code 对应 */ + private Integer seatCode; + + @Min(value = 1,message = "最小1") + private Integer page; + + @Min(value = 1,message = "最小1") + @Max(value = 20,message = "最大20") + private Integer size; + + private Integer limit; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/fight/FightOnMyOwnParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/fight/FightOnMyOwnParam.java new file mode 100644 index 0000000..0a09a8a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/fight/FightOnMyOwnParam.java @@ -0,0 +1,36 @@ +package com.ruoyi.domain.dto.fight; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class FightOnMyOwnParam { + + private Integer fightId; + + private Integer playerId; + + // 模式 + private String model; + + // 状态列表 + private List statusList; + + // 价格区间下限(含) + private Integer minPrice; + + // 价格区间上限(含) + private Integer maxPrice; + + private Integer page; + + private Integer size; + + // 房主昵称关键词(模糊搜索tt_user.nick_name) + private String keyword; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/fight/QuickJoinFightParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/fight/QuickJoinFightParam.java new file mode 100644 index 0000000..6ee8e5a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/fight/QuickJoinFightParam.java @@ -0,0 +1,13 @@ +package com.ruoyi.domain.dto.fight; + +import lombok.Data; + +/** + * 快速加入对战房间请求 + */ +@Data +public class QuickJoinFightParam { + + /** 房间模式(大厅阵营):0-欧皇 1-非酋 */ + private String model; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/mayi/ApiPayAddOrderParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/mayi/ApiPayAddOrderParam.java new file mode 100644 index 0000000..98b822e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/mayi/ApiPayAddOrderParam.java @@ -0,0 +1,61 @@ +package com.ruoyi.domain.dto.mayi; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ApiPayAddOrderParam { + + //平台分配商户号 + private String pay_memberid; + + //上送订单号唯一 + private String pay_orderid; + + //提交时间 2016-12-26 18:18:18 + private String pay_applydate; + + //银行编码 + private String pay_bankcode; + + //服务端通知 + private String pay_notifyurl; + + //页面跳转通知 + private String pay_callbackurl; + + //订单金额 + private String pay_amount; + + //MD5 签名 + private String pay_md5sign; + + //附加字段 + private String pay_attach; + + //商品名称 + private String pay_productname; + + //商户品数量 + private String pay_productnum; + + //商品描述 + private String pay_productdesc; + + //商户链接地址 + private String pay_producturl; + + //分账绑卡 id + private String pay_bindid; + + //客户端 ip + private String pay_clientip; + + //返回数据格式 + private String type; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/mayi/ApiPayAddOrderResponse.java b/skins-model/src/main/java/com/ruoyi/domain/dto/mayi/ApiPayAddOrderResponse.java new file mode 100644 index 0000000..c792c24 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/mayi/ApiPayAddOrderResponse.java @@ -0,0 +1,28 @@ +package com.ruoyi.domain.dto.mayi; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ApiPayAddOrderResponse { + + //状态 + private String status; + + //消息 + private String msg; + + //金额 + private String pay_amount; + + //订单号 + private String pay_orderid; + + //支付地址 + private String payUrl; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/mayi/PayNotifyRequest.java b/skins-model/src/main/java/com/ruoyi/domain/dto/mayi/PayNotifyRequest.java new file mode 100644 index 0000000..8c6ee71 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/mayi/PayNotifyRequest.java @@ -0,0 +1,41 @@ +package com.ruoyi.domain.dto.mayi; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +// 回调参数 +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PayNotifyRequest { + + //商户编号 + private String memberid; + + //订单号 + private String orderid; + + //订单金额 + private String amount; + + //实付金额 + private String true_amount; + + //交易流水号 + private String transaction_id; + + //交易时间 + private String datetime; + + //交易状态 + private String returncode; + + //扩展返回 + private String attach; + + //签名 + private String sign; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/DecomposeLogCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/DecomposeLogCondition.java new file mode 100644 index 0000000..1732eec --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/DecomposeLogCondition.java @@ -0,0 +1,27 @@ +package com.ruoyi.domain.dto.packSack; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class DecomposeLogCondition { + + @Min(value = 1,message = "最小值1") + private Integer page; + + @Min(value = 1,message = "最小值1") + @Max(value = 20,message = "最大值20") + private Integer size; + + private Integer boxRecordStatus; + private Integer limit; + + private Integer userId; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/DecomposeParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/DecomposeParam.java new file mode 100644 index 0000000..b0d8b65 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/DecomposeParam.java @@ -0,0 +1,23 @@ +package com.ruoyi.domain.dto.packSack; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class DecomposeParam { + private List packSackIds; + @NotNull(message = "是否全选为必填项") + private Boolean isAll; + /** + * 分解场景:backpack=从背包分解(默认),game=游戏结果页立即分解。 + * 游戏内分解时,根据饰品来源写入对应的游戏奖励流水; + * 背包分解时,统一写入 DECOMPOSE_ORNAMENT(背包分解饰品收入)。 + */ + private String decomposeType; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/DeliveryParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/DeliveryParam.java new file mode 100644 index 0000000..d9a6b73 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/DeliveryParam.java @@ -0,0 +1,26 @@ +package com.ruoyi.domain.dto.packSack; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class DeliveryParam { + private List packSackIds; + @NotNull(message = "是否全选为必填项") + private Boolean isAll; + + /** + * 安全密码(6位),用户通过 /api/user/setSafePassword 单独设置,与登录密码无关 + */ + @NotBlank(message = "请输入安全密码") + @Pattern(regexp = "^\\d{6}$", message = "安全密码必须为6位数字") + private String twoFactorAuth; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/PackSackCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/PackSackCondition.java new file mode 100644 index 0000000..dccf180 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/packSack/PackSackCondition.java @@ -0,0 +1,35 @@ +package com.ruoyi.domain.dto.packSack; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PackSackCondition { + + @ApiModelProperty("背包类型(1我的库存 2提货背包 3福利箱子)") + private Integer packType; + + private List uidList; + + private List statusList; + + private List sourceList; + + private String name; + + private String beginTime; + + private String endTime; + + @ApiModelProperty("价格排序方向(1升序 2降序,null不排序)") + private Integer priceSort; + + @ApiModelProperty("时间排序方向(1升序 2降序,null不排序)") + private Integer timeSort; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/queryCondition/OrnamentCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/queryCondition/OrnamentCondition.java new file mode 100644 index 0000000..0b9f277 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/queryCondition/OrnamentCondition.java @@ -0,0 +1,71 @@ +package com.ruoyi.domain.dto.queryCondition; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.Objects; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class OrnamentCondition implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableField(value = "id", fill = FieldFill.INSERT) + private Long id; + + @Excel(name = "长名称") + @TableField("name") + private String name; + + private BigDecimal minPrice; + + private BigDecimal maxPrice; + + @Excel(name = "类别") + private String type; + + @Excel(name = "品质") + private String quality; + + @Excel(name = "稀有度") + private String rarity; + + @Excel(name = "外观") + private String exterior; + + // 1上架 0下架 + private String isPutaway; + + // 0是本网站自定义道具 1否 + private Boolean isProprietaryProperty; + + @Min(value = 1,message = "最小1") + private Integer page; + + @Max(value = 20,message = "最大20") + private Integer size; + + @Min(value = 1,message = "最小1") + private Integer pageNum; + + @Max(value = 20,message = "最大20") + private Integer pageSize; + + private Integer limit; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/queryCondition/RollPrizesCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/queryCondition/RollPrizesCondition.java new file mode 100644 index 0000000..163f922 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/queryCondition/RollPrizesCondition.java @@ -0,0 +1,29 @@ +package com.ruoyi.domain.dto.queryCondition; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class RollPrizesCondition { + + @NotNull(message = "rollId不能为空。") + private Integer rollId; + + @Min(value = 1,message = "最小1") + @NotNull(message = "page不能为空") + private Integer page; + + @Max(value = 20,message = "最大20") + @NotNull(message = "page不能为空") + private Integer size; + + private Integer limit; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/recharge/RechargeConfigOrderVo.java b/skins-model/src/main/java/com/ruoyi/domain/dto/recharge/RechargeConfigOrderVo.java new file mode 100644 index 0000000..1183461 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/recharge/RechargeConfigOrderVo.java @@ -0,0 +1,24 @@ +package com.ruoyi.domain.dto.recharge; + +import com.ruoyi.common.annotation.Excel; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class RechargeConfigOrderVo { + @Excel(name = "商品ID") + private Integer goodsId; + + @Excel(name = "商品价格") + private BigDecimal goodsPrice; + + @Excel(name = "商品数量") + private Integer goodsNum; + + @Excel(name = "商品总价") + private BigDecimal totalAmount; + + @Excel(name = "支付图片") + private String picture; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/roll/GetRollOpenPrizeParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/roll/GetRollOpenPrizeParam.java new file mode 100644 index 0000000..ec0612d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/roll/GetRollOpenPrizeParam.java @@ -0,0 +1,27 @@ +package com.ruoyi.domain.dto.roll; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GetRollOpenPrizeParam { + + @NotNull(message = "不能为空") + private Integer rollId; + + @Min(value = 1,message = "最小1") + private Integer page; + + @Min(value = 1,message = "最小1") + @Max(value = 20,message = "最大20") + private Integer size; + + private Integer limit; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/roll/GetRollPlayersParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/roll/GetRollPlayersParam.java new file mode 100644 index 0000000..5d2ba12 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/roll/GetRollPlayersParam.java @@ -0,0 +1,27 @@ +package com.ruoyi.domain.dto.roll; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GetRollPlayersParam { + + @NotNull(message = "不能为空") + private Integer rollId; + + @Min(value = 1, message = "页码最小为1") + private Integer page; + + @Min(value = 1, message = "长度最小为1") + @Max(value = 20, message = "长度最大为20") + private Integer size; + + private Integer limit; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/roll/GetRollPrizePool.java b/skins-model/src/main/java/com/ruoyi/domain/dto/roll/GetRollPrizePool.java new file mode 100644 index 0000000..2d9f116 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/roll/GetRollPrizePool.java @@ -0,0 +1,29 @@ +package com.ruoyi.domain.dto.roll; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GetRollPrizePool { + + @NotNull(message = "不能为空") + private Integer rollId; + + private Integer jackpotId; + + @Min(value = 1,message = "最小1") + private Integer page; + + @Min(value = 1,message = "最小1") + @Max(value = 20,message = "最大20") + private Integer size; + + private Integer limit; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/roll/InviteRollUser.java b/skins-model/src/main/java/com/ruoyi/domain/dto/roll/InviteRollUser.java new file mode 100644 index 0000000..6ee57f9 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/roll/InviteRollUser.java @@ -0,0 +1,24 @@ +package com.ruoyi.domain.dto.roll; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class InviteRollUser { + + @NotNull(message = "rollId不能为空") + private Integer rollId; + + @NotEmpty + private List userIds; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/rollJackpotOrnament/RollJOEdit.java b/skins-model/src/main/java/com/ruoyi/domain/dto/rollJackpotOrnament/RollJOEdit.java new file mode 100644 index 0000000..d8fbe3d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/rollJackpotOrnament/RollJOEdit.java @@ -0,0 +1,60 @@ +package com.ruoyi.domain.dto.rollJackpotOrnament; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class RollJOEdit { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "Roll房奖池ID") + private Integer jackpotId; + + @Excel(name = "Roll房奖池饰品ID") + private Long ornamentsId; + + @Excel(name = "饰品价格") + private BigDecimal price; + + @Excel(name = "饰品图片id") + private String ornamentImgId; + + @Excel(name = "饰品图片") + private String imgUrl; + + @Excel(name = "饰品名称") + private String ornamentName; + + @Excel(name = "饰品级别ID") + private Integer ornamentsLevelId; + + @Excel(name = "饰品数量") + private Integer ornamentsNum; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/share/ShareRecordQueryDTO.java b/skins-model/src/main/java/com/ruoyi/domain/dto/share/ShareRecordQueryDTO.java new file mode 100644 index 0000000..1df40f0 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/share/ShareRecordQueryDTO.java @@ -0,0 +1,25 @@ +package com.ruoyi.domain.dto.share; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 分享记录列表查询参数 + */ +@Data +@ApiModel("分享记录列表查询参数") +public class ShareRecordQueryDTO { + + /** 可选;按被邀请人用户ID过滤 */ + @ApiModelProperty("下级用户ID(可选)") + private Long subUserId; + + /** 页码(由前端传入) */ + @ApiModelProperty(value = "页码", required = true, example = "1") + private Integer pageNum; + + /** 每页大小(由前端传入) */ + @ApiModelProperty(value = "每页大小", required = true, example = "10") + private Integer pageSize; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/sys/OrderQueryCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/sys/OrderQueryCondition.java new file mode 100644 index 0000000..fb9e90a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/sys/OrderQueryCondition.java @@ -0,0 +1,47 @@ +package com.ruoyi.domain.dto.sys; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class OrderQueryCondition { + + private List uidList; + + private List statusList; + + // 时间区间 + private String beginTime; + + private String endTime; + + // 订单号 + private String orderId; + + // 外部订单号 + private String outTradeNo; + + // 排序字段: time、totalAmount + private String orderByFie; + + // 升1 降2 + private Integer orderByType; + + @Min(value = 1,message = "最小1") + private Integer page; + + @Min(value = 1,message = "最小1") + @Max(value = 20,message = "最大20") + private Integer size; + + private Integer limit; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/sys/TeamUsersParam.java b/skins-model/src/main/java/com/ruoyi/domain/dto/sys/TeamUsersParam.java new file mode 100644 index 0000000..9a23d44 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/sys/TeamUsersParam.java @@ -0,0 +1,34 @@ +package com.ruoyi.domain.dto.sys; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.List; +@AllArgsConstructor +@NoArgsConstructor +@Data +public class TeamUsersParam { + + private String userName; + private List bossIds; + private List employeeIds; + + // 排序字段 1、充值金额 2、综合消费 3、金币消费 4、积分消费 + private Integer orderByFie; + // 1升 2降 + private Integer orderType; + + // 时间区间 格式:yyyy-MM-dd HH:mm:ss + private String beginTime; + private String endTime; + + @Min(value = 1,message = "最小值1") + private Integer page; + @Min(value = 1,message = "最小值1") + @Max(value = 20,message = "最大值20") + private Integer size; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/upgrade/MyUpgradeRecordRequest.java b/skins-model/src/main/java/com/ruoyi/domain/dto/upgrade/MyUpgradeRecordRequest.java new file mode 100644 index 0000000..97c346e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/upgrade/MyUpgradeRecordRequest.java @@ -0,0 +1,26 @@ +package com.ruoyi.domain.dto.upgrade; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.Min; + +/** + * 个人幸运升级历史记录查询请求 + * 不区分成功/失败,全部记录都返回,按 open_time 倒序。 + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +public class MyUpgradeRecordRequest { + + /** 页码,从1开始 */ + @Min(value = 1, message = "页码最小1") + private Integer page = 1; + + /** 每页条数,最大20 */ + @Range(min = 1, max = 20, message = "每页条数范围1-20") + private Integer size = 10; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/upgrade/UpgradeCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/upgrade/UpgradeCondition.java new file mode 100644 index 0000000..a619f64 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/upgrade/UpgradeCondition.java @@ -0,0 +1,29 @@ +package com.ruoyi.domain.dto.upgrade; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UpgradeCondition { + + private Long upgradeRecordId; + + //@NotNull(message = "升级饰品id不能为空。") + private Long ornamentId; + private String userType; + private Integer userId; + + @Min(value = 1,message = "页码最小1") + private Integer page; + @Range(min = 1,max = 20) + private Integer size; + + private Integer limit; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/upgrade/UpgradeRecordPageRequest.java b/skins-model/src/main/java/com/ruoyi/domain/dto/upgrade/UpgradeRecordPageRequest.java new file mode 100644 index 0000000..afddb69 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/upgrade/UpgradeRecordPageRequest.java @@ -0,0 +1,35 @@ +package com.ruoyi.domain.dto.upgrade; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.Min; + +/** + * 幸运升级记录分页查询请求 + *

通过 scope(我的/全服) 与 itemScope(当前饰品/全部饰品) 两个维度组合,按 open_time 倒序分页。

+ */ +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UpgradeRecordPageRequest { + + /** 范围:0=全服(所有用户) 1=我的(当前登录用户) */ + private Integer scope = 0; + + /** 饰品:0=全部饰品 1=当前饰品 */ + private Integer itemScope = 0; + + /** 当前可升级饰品ID(tt_upgrade_ornaments.id),itemScope=1(当前饰品) 时必填 */ + private Long upgradeId; + + /** 页码,从1开始 */ + @Min(value = 1, message = "页码最小1") + private Integer page = 1; + + /** 每页条数,最大20 */ + @Range(min = 1, max = 20, message = "每页条数范围1-20") + private Integer size = 10; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/AmountRecordsDetailCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/AmountRecordsDetailCondition.java new file mode 100644 index 0000000..8702fc1 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/AmountRecordsDetailCondition.java @@ -0,0 +1,40 @@ +package com.ruoyi.domain.dto.userRecord; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.Date; + +@Data +public class AmountRecordsDetailCondition { + + private Integer userId; + + private String nickName; + + private String phoneNumber; + + // 收入1 支出0 + private Integer type; + + private String source; + + // 1金币 2积分 + @NotNull(message = "moneyType不能为空") + private Integer moneyType; + + @Min(value = 1,message = "最小值1") + private Integer page; + + @Min(value = 1,message = "最小值1") + private Integer size; + private Integer limit; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/BlendErcashCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/BlendErcashCondition.java new file mode 100644 index 0000000..2251ebf --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/BlendErcashCondition.java @@ -0,0 +1,45 @@ +package com.ruoyi.domain.dto.userRecord; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class BlendErcashCondition { + + private Integer userId; + + private Integer source; + + private Integer type; + + private String userName; + + private Integer moneyType; + + private String phoneNumber; + + /** 查询开始时间,为空时默认最近 15 天 */ + private String startTime; + + /** 查询结束时间,为空时默认当前时间 */ + private String endTime; + + @NotNull(message = "页码不能为空") + @Min(value = 1,message = "最小1") + private Integer page; + + @NotNull(message = "分页长度不能为空") + @Min(value = 1,message = "最小1") + @Max(value = 20,message = "最大20") + private Integer size; + + private Integer limit; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/DeliveryRecordsConfition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/DeliveryRecordsConfition.java new file mode 100644 index 0000000..7d2aba6 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/DeliveryRecordsConfition.java @@ -0,0 +1,32 @@ +package com.ruoyi.domain.dto.userRecord; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +public class DeliveryRecordsConfition { + + private List statusList; + + private List uIdList; + + @Min(value = 1,message = "最小1") + private Integer page; + + @Min(value = 1,message = "最小1") + @Max(value = 20,message = "最大20") + private Integer size; + + private Integer limit; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/OrderCondition.java b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/OrderCondition.java new file mode 100644 index 0000000..ff9aa8e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/OrderCondition.java @@ -0,0 +1,32 @@ +package com.ruoyi.domain.dto.userRecord; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class OrderCondition { + + private List userIdList; + + @Min(value = 1,message = "最小1") + private Integer page; + + @Min(value = 1,message = "最小1") + private Integer size; + + private Integer limit; + + // 1时间升序 2 时间降序 + private Integer orderBy; + + @NotNull(message = "订单状态不能为空") + private Integer status; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/ProfileRecordsRequest.java b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/ProfileRecordsRequest.java new file mode 100644 index 0000000..3871684 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/userRecord/ProfileRecordsRequest.java @@ -0,0 +1,52 @@ +package com.ruoyi.domain.dto.userRecord; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 个人资产详情 - 我的记录 统一请求参数 + * tab 1=余额记录 2=充值记录 3=提货记录 4=赠送记录 + * timeRange 1=今日 2=昨日 3=近7日(可选,默认 3;接口只返回近 7 天数据,三种取值均不超过 7 天) + * subFilter 二级筛选(语义随 tab 变化),不传或传 0 默认全部: + * - tab=1 余额:0=全部 1=消费 2=充值 4=结算 5=奖励 + * 过滤条件:amount 与 credits 不同时为 0;金额取值优先 amount,为 0 时走 credits(分解饰品等结算收入仅写 credits 列) + * 按 source 集合过滤: + * 充值={1} + * 奖励={3,5,7,9,11,13,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,39,41,43,44}(金额取正) + * 消费={2,4,6,8,12,33,34,38,40}(金额取负) + * 结算={10,16,17,37,42} + * - tab=4 赠送:0=全部 1=赠送(我送出) 2=受赠(我收到) + * - tab=2/3 忽略该参数 + * pageNum/pageSize 分页(默认 1/20,pageSize 上限 100),total 返回当前筛选条件下的真实总条数 + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ProfileRecordsRequest { + + @NotNull(message = "tab不能为空") + @Min(value = 1, message = "tab最小1") + @Max(value = 4, message = "tab最大4") + private Integer tab; + + /** 时间范围,可选;不传默认 3=近7日 */ + @Min(value = 1, message = "timeRange最小1") + @Max(value = 3, message = "timeRange最大3") + private Integer timeRange = 3; + + /** 二级筛选,可选;不传默认 0=全部 */ + private Integer subFilter; + + /** 页码,从 1 开始;不传默认 1 */ + @Min(value = 1, message = "pageNum最小1") + private Integer pageNum = 1; + + /** 每页条数;不传默认 20,最大 100 */ + @Min(value = 1, message = "pageSize最小1") + @Max(value = 100, message = "pageSize最大100") + private Integer pageSize = 20; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/yyyouping/OnSaleCommodityDTO.java b/skins-model/src/main/java/com/ruoyi/domain/dto/yyyouping/OnSaleCommodityDTO.java new file mode 100644 index 0000000..ba82a0a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/yyyouping/OnSaleCommodityDTO.java @@ -0,0 +1,9 @@ +package com.ruoyi.domain.dto.yyyouping; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +public class OnSaleCommodityDTO { + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/yyyouping/OrnamentYY.java b/skins-model/src/main/java/com/ruoyi/domain/dto/yyyouping/OrnamentYY.java new file mode 100644 index 0000000..6d85379 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/yyyouping/OrnamentYY.java @@ -0,0 +1,67 @@ +package com.ruoyi.domain.dto.yyyouping; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +// yy平台下载的饰品信息 +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class OrnamentYY implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(value = "id",type = IdType.AUTO) + private Long id; + + @Excel(name = "名称") + @TableField("name") + private String name; + + @Excel(name = "哈希名称") + @TableField("hash_name") + private String hashName; + + @Excel(name = "类型id") + @TableField("type_id") + private String typeId; + + @Excel(name = "type_name") + @TableField("type_name") + private String typeName; + + @Excel(name = "type_hash_name") + @TableField("type_hash_name") + private String typeHashName; + + @Excel(name = "weapon_id") + @TableField("weapon_id") + private Integer weaponId; + + @Excel(name = "weapon_name") + @TableField("weapon_name") + private String weaponName; + + @Excel(name = "weapon_hash_name") + @TableField("weapon_hash_name") + private String weaponHashName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @TableField("update_time") + private Timestamp updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/dto/zbt/OrnamentZBT.java b/skins-model/src/main/java/com/ruoyi/domain/dto/zbt/OrnamentZBT.java new file mode 100644 index 0000000..09a729f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/dto/zbt/OrnamentZBT.java @@ -0,0 +1,128 @@ +package com.ruoyi.domain.dto.zbt; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.Objects; + +// zbt平台下载的饰品信息 +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class OrnamentZBT implements Serializable { + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class PriceInfo{ + private Long userId; + private BigDecimal price; + private Integer quantity; + private BigDecimal autoDeliverPrice; + private Integer autoDeliverQuantity; + private BigDecimal manualDeliverPrice; + private Integer manualQuantity; + } + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + private String appId; + + @Excel(name = "ZBT官网饰品ID") + private Long itemId; + + @Excel(name = "价格和在售信息") + private PriceInfo priceInfo; + + @Excel(name = "长名称") + @TableField("name") + private String itemName; + + @Excel(name = "饰品唯一名称英文") + private String marketHashName; + + @Excel(name = "短名称") + private String shortName; + + @Excel(name = "图片") + private String imageUrl; + + @Excel(name = "类别") + private String type; + + @Excel(name = "类别中文名") + private String typeName; + + @Excel(name = "品质") + private String quality; + + @Excel(name = "品质名称") + private String qualityName; + + @Excel(name = "品质颜色") + private String qualityColor; + + @Excel(name = "稀有度") + private String rarity; + + @Excel(name = "稀有度名称") + private String rarityName; + + @Excel(name = "稀有度颜色") + private String rarityColor; + + @Excel(name = "外观") + private String exterior; + + @Excel(name = "外观名称") + private String exteriorName; + + // @Excel(name = "本网站使用价格") + // private BigDecimal usePrice; + + // @Excel(name = "在售最低价") + // private BigDecimal price; + + // @Excel(name = "在售数量") + // private Integer quantity; + + // @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + // private Date createTime; + // + // @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + // private Date updateTime; + // + // private String remark; + // + // private String isPutaway; + // + // private String isProprietaryProperty; + + // @Override + // public boolean equals(Object o) { + // if (this == o) return true; + // if (o == null || getClass() != o.getClass()) return false; + // OrnamentZBT that = (OrnamentZBT) o; + // return Objects.equals(marketHashName, that.marketHashName); + // } + // + // @Override + // public int hashCode() { + // return Objects.hash(marketHashName); + // } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/BoxTransferRecord.java b/skins-model/src/main/java/com/ruoyi/domain/entity/BoxTransferRecord.java new file mode 100644 index 0000000..a38634a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/BoxTransferRecord.java @@ -0,0 +1,76 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@TableName("tt_box_transfer_record") +public class BoxTransferRecord implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 转增人uid + */ + private Long srcUid; + + /** + * 授予人uid + */ + private Long dstUid; + + /** + * 宝箱记录ID + */ + private Long boxRecordId; + + /** + * 宝箱中饰品ID + */ + private Long ornamentId; + + private String marketHashName; + + private String ornamentsZbtId; + + private String ornamentsYyId; + + private String ornamentName; + + private BigDecimal ornamentsPrice; + + private String imageUrl; + + private Integer ornamentsLevelId; + + private String ornamentLevelImg; + + /** + * 转赠状态:0-审核中,1-成功,2-失败 + */ + private Integer status; + + /** + * 记录创建时间 + */ + private LocalDateTime createTime; + + /** + * 记录更新时间 + */ + private LocalDateTime updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarSpin.java b/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarSpin.java new file mode 100644 index 0000000..8eccc29 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarSpin.java @@ -0,0 +1,28 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "game_sugar_spin") +public class GameSugarSpin implements Serializable { + private Long gid; + private BigDecimal score; + private Integer count; + private String feature; + private Integer extraFree; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarStepInfo.java b/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarStepInfo.java new file mode 100644 index 0000000..7b12426 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarStepInfo.java @@ -0,0 +1,44 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "game_sugar_step_info") +public class GameSugarStepInfo implements Serializable { + private Long id; + private Long gid; + private Integer freeSpinId; + private Integer aes; + private String multipler; + private List multipler2; + private String grid; + private String symbolLinks; + private List symbolLinks2; + private BigDecimal score; + private LocalDateTime createTime; + private LocalDateTime updateTime; + + @Data + public static class SymbolLink implements Serializable { + private String symbol; + private List loc; + private List multipler; + private Integer totalMulti; + private Double baseScore; + private Double score; + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarUser.java b/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarUser.java new file mode 100644 index 0000000..eaf93c2 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarUser.java @@ -0,0 +1,30 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "game_sugar_user") +public class GameSugarUser implements Serializable { + private Long id; + private Integer userId; + private BigDecimal totalBet; + private BigDecimal totalWin; + private Integer count; + private Integer freeCount; + private Integer superFreeCount; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarWin.java b/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarWin.java new file mode 100644 index 0000000..828814e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/GameSugarWin.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "game_sugar_win") +public class GameSugarWin implements Serializable { + private Long id; + private LocalDateTime date; + private BigDecimal initBet; + private BigDecimal totalBet; + private BigDecimal totalWin; + private Integer count; + private Integer freeCount; + private Integer superFreeCount; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/PromoCdkeyRedeem.java b/skins-model/src/main/java/com/ruoyi/domain/entity/PromoCdkeyRedeem.java new file mode 100644 index 0000000..a584523 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/PromoCdkeyRedeem.java @@ -0,0 +1,64 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.ibatis.type.Alias; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 推广CDKEY兑换记录表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Alias("AdminPromoCdkeyRedeem") +@TableName("promo_cdkey_redeem") +public class PromoCdkeyRedeem implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** 关联promo_cdkey.id */ + private Long cdkeyId; + + /** CDKEY码 */ + private String cdkeyCode; + + /** 兑换者ID */ + private Long redeemerId; + + /** 兑换者名称 */ + private String redeemerName; + + /** 单个价值(金币) */ + private BigDecimal unitValue; + + /** 兑换时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date redeemTime; + + /** 生成者名称(来自promo_cdkey表) */ + @TableField(exist = false) + private String creatorName; + + /** 生成者用户ID(查询条件,来自promo_cdkey表) */ + @TableField(exist = false) + private Long creatorId; + + /** 兑换者昵称(来自tt_user表) */ + @TableField(exist = false) + private String redeemerNickName; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtBlankCardKey.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtBlankCardKey.java new file mode 100644 index 0000000..3e3af11 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtBlankCardKey.java @@ -0,0 +1,55 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 空白卡密(管理后台生成,独立于卡商卡密表) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("tt_blank_card_key") +public class TtBlankCardKey implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** 卡密编码(唯一) */ + private String cardCode; + + /** 面值金额 */ + private BigDecimal amount; + + /** 使用者ID */ + private Long usedById; + + /** 使用者名称 */ + private String usedByName; + + /** 使用时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date usedTime; + + /** 状态: 0=未使用, 1=已使用 */ + private Integer status; + + /** 创建者名称 */ + private String creatorName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtBoxRecords.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtBoxRecords.java new file mode 100644 index 0000000..bad6b49 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtBoxRecords.java @@ -0,0 +1,112 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_box_records") +public class TtBoxRecords implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Long id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "宝箱ID") + private Integer boxId; + + @Excel(name = "宝箱名称") + private String boxName; + + @Excel(name = "宝箱价格") + private BigDecimal boxPrice; + + @Excel(name = "转盘期号") + @TableField("period_no") + private Long periodNo; + + @Excel(name = "饰品ID") + @TableField("ornament_id") + private Long ornamentId; + + @Excel(name = "市场唯一hash名称") + @TableField("market_hash_name") + private String marketHashName; + + @Excel(name = "zbt ID") + @TableField("ornaments_zbt_id") + private String ornamentsZbtId; + + @Excel(name = "yy ID") + @TableField("ornaments_yy_id") + private String ornamentsYyId; + + @Excel(name = "饰品名称") + private String ornamentName; + + @Excel(name = "饰品价格") + private BigDecimal ornamentsPrice; + + @Excel(name = "饰品图片") + private String imageUrl; + + @Excel(name = "饰品级别ID") + private Integer ornamentsLevelId; + + @Excel(name = "饰品级别图片") + private String ornamentLevelImg; + + // 0、在背包显示 1、已分解 + @Excel(name = "状态") + private Integer status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + // 0、对战 + @Excel(name = "饰品来源") + private Integer source; + + @Excel(name = "对战ID") + private Integer fightId; + + @Excel(name = "对战模式:回合数") + private Integer fightRoundNumber; + + @Excel(name = "Roll房ID") + private Integer rollId; + + @Excel(name = "持有者_用户ID") + private Integer holderUserId; + + // 用于对战模式,游戏完全结束才显示 + @TableField("is_show") + private Integer isShow; + + @Excel(name = "外观名称") + @TableField(exist = false) + private String exteriorName; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtCommissionRecord.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtCommissionRecord.java new file mode 100644 index 0000000..e0e8913 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtCommissionRecord.java @@ -0,0 +1,65 @@ +package com.ruoyi.domain.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class TtCommissionRecord { + + /** + * ID + */ + private Integer id; + + /** + * 推广人员ID + */ + private Integer userId; + + /** + * 佣金 + */ + private BigDecimal commission; + + /** + * 汇总时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date summaryTime; + + /** + * 结算周期开始时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date settlementStartTime; + + /** + * 结算周期结束时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date settlementEndTime; + + /** + * 结算周期内参与计算的流水总额 + */ + private BigDecimal totalFlow; + + /** + * 结算时使用的费率快照(百分比数值,如 0.5 表示 0.5%) + */ + private BigDecimal rate; + + /** + * 领取状态(0未领取 1已领取) + */ + private String claimStatus; + + /** + * 领取时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date claimTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtLossRebate.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtLossRebate.java new file mode 100644 index 0000000..74ba73c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtLossRebate.java @@ -0,0 +1,92 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 亏损返水结算明细 tt_loss_rebate + *

+ * 每个自然半月每个亏损玩家一条记录,是后台「CDK管理 - 亏损返水」页面的数据源。 + * 定时任务结算时写入;status=0(超过单期上限)的记录等运营在该页面点「发放CDK」人工发放。 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName("tt_loss_rebate") +public class TtLossRebate implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** 期次,格式 yyyy-MM-dd~yyyy-MM-dd(区分上下半月) */ + private String period; + + @TableField("user_id") + private Integer userId; + + @TableField("user_name") + private String userName; + + @TableField("nick_name") + private String nickName; + + /** 本期游戏投入(bet 类 source 绝对值之和) */ + @TableField("bet_amount") + private BigDecimal betAmount; + + /** 本期游戏产出(reward 类 source 绝对值之和) */ + @TableField("win_amount") + private BigDecimal winAmount; + + /** 净亏 = 投入 - 产出,>0 才落记录 */ + @TableField("loss_amount") + private BigDecimal lossAmount; + + /** 应返水 = 净亏 × lossRebateRate */ + @TableField("rebate_amount") + private BigDecimal rebateAmount; + + /** 0=待人工发放(超单期上限) 1=已自动发放 2=已人工发放 */ + private Integer status; + + /** 关联 tt_red_packet.id */ + @TableField("red_packet_id") + private Integer redPacketId; + + /** 发放的CDK口令,后台可直接复制给玩家 */ + private String cdkey; + + @TableField("grant_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date grantTime; + + @TableField("grant_by") + private String grantBy; + + private String remark; + + @TableField("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @TableField("update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtOrder.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtOrder.java new file mode 100644 index 0000000..fb20786 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtOrder.java @@ -0,0 +1,80 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_order") +public class TtOrder implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + // 见PartyType + @Excel(name = "第三方") + private String thirdParty; + /** + * 0卡密 1支付宝 2微信 见PayType + */ + @Excel(name = "订单类型") + private String type; + + @Excel(name = "商品ID") + private Integer goodsId; + + @Excel(name = "商品价格") + private BigDecimal goodsPrice; + + @Excel(name = "商品数量") + private Integer goodsNum; + + @Excel(name = "商品总价") + private BigDecimal totalAmount; + + @Excel(name = "订单号") + private String orderId; + + @Excel(name = "外部订单号") + private String outTradeNo; + + @Excel(name = "sign验签密钥") + private String sign; + + @Excel(name = "支付状态") + private String status; + + @Excel(name = "支付跳转链接") + private String payUrl; + + @Excel(name = "百度推广bdVid") + private String bdVid; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtOrnament.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtOrnament.java new file mode 100644 index 0000000..90a6c63 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtOrnament.java @@ -0,0 +1,126 @@ +package com.ruoyi.domain.entity; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.Objects; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_ornament") +public class TtOrnament implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "饰品唯一名称英文") + @TableId(value = "market_hash_name", type = IdType.INPUT) + private String marketHashName; + + @TableField(value = "id", fill = FieldFill.INSERT) + private Long id; + + @Excel(name = "zbt_id") + @TableField("zbt_id") + private Long zbtId; + + @Excel(name = "yyyouping_id") + @TableField("yyyouping_id") + private Long yyyoupingId; + + @Excel(name = "长名称") + @TableField("name") + private String name; + + @Excel(name = "本网站使用价格(rmb)") + private BigDecimal usePrice; + + @Excel(name = "图片") + private String imageUrl; + + @Override + public int hashCode() { + return Objects.hash(this.marketHashName); + } + + @Excel(name = "在售最低价") + private BigDecimal price; + + @Excel(name = "商城价格") + private BigDecimal storePrice; + + @Excel(name = "在售数量") + private Integer quantity; + + @Excel(name = "短名称") + private String shortName; + + @Excel(name = "类别") + private String type; + + @Excel(name = "类别中文名") + private String typeName; + + @Excel(name = "类别hash名") + @TableField("type_hash_name") + private String typeHashName; + + @Excel(name = "品质") + private String quality; + + @Excel(name = "品质hash") + private String qualityHashName; + + @Excel(name = "品质名称") + private String qualityName; + + @Excel(name = "品质颜色") + private String qualityColor; + + @Excel(name = "稀有度") + private String rarity; + + private String rarityHashName; + + @Excel(name = "稀有度名称") + private String rarityName; + + @Excel(name = "稀有度颜色") + private String rarityColor; + + @Excel(name = "外观") + private String exterior; + + @Excel(name = "外观") + private String exteriorHashName; + + @Excel(name = "外观名称") + private String exteriorName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private String remark; + + // 0上架 1下架 + private String isPutaway; + + // 0是本网站自定义道具 1否 + private String isProprietaryProperty; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtOrnamentsYY.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtOrnamentsYY.java new file mode 100644 index 0000000..c17a8de --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtOrnamentsYY.java @@ -0,0 +1,72 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_ornament_yy") +public class TtOrnamentsYY implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(value = "id",type = IdType.AUTO) + private Integer id; + + @Excel(name = "哈希名称") + @TableField("hash_name") + private String hashName; + + @Excel(name = "名称") + @TableField("name") + private String name; + + @Excel(name = "类型id") + @TableField("type_id") + private Integer typeId; + + @Excel(name = "type_name") + @TableField("type_name") + private String typeName; + + @Excel(name = "type_hash_name") + @TableField("type_hash_name") + private String typeHashName; + + @Excel(name = "weapon_id") + @TableField("weapon_id") + private Integer weaponId; + + @Excel(name = "weapon_name") + @TableField("weapon_name") + private String weaponName; + + @Excel(name = "weapon_hash_name") + @TableField("weapon_hash_name") + private String weaponHashName; + + // @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + // private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @TableField("update_time") + private Timestamp updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtPromoCardKey.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtPromoCardKey.java new file mode 100644 index 0000000..9337ca0 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtPromoCardKey.java @@ -0,0 +1,61 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 卡商生成的卡密 promo_card_key + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("promo_card_key") +public class TtPromoCardKey implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** 卡密编码(唯一) */ + private String cardCode; + + /** 面值金额 */ + private BigDecimal amount; + + /** 充值列表ID */ + private Integer rechargeListId; + + /** 创建者ID(卡商userId) */ + private Long creatorId; + + /** 创建者名称 */ + private String creatorName; + + /** 使用者ID */ + private Long usedById; + + /** 使用者名称 */ + private String usedByName; + + /** 使用时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date usedTime; + + /** 状态: 0=未使用, 1=已使用 */ + private Integer status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtPromotionLevel.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtPromotionLevel.java new file mode 100644 index 0000000..9ce96c0 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtPromotionLevel.java @@ -0,0 +1,57 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_promotion_level") +public class TtPromotionLevel implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "推广等级名称") + private String name; + + @Excel(name = "推广等级图标") + private String icon; + + @Excel(name = "推广等级达标金额") + private BigDecimal rechargeThreshold; + + @Excel(name = "推广返佣比例") + private BigDecimal commissions; + + @Excel(name = "等级达标奖励红包金额") + private BigDecimal addedBonus; + + @Excel(name = "充值赠送比例") + private BigDecimal rechargeGiftRatio; + + @Excel(name = "描述") + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtPromotionRecord.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtPromotionRecord.java new file mode 100644 index 0000000..96c8d52 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtPromotionRecord.java @@ -0,0 +1,60 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_promotion_record") +public class TtPromotionRecord implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "下级用户ID") + private Integer subordinateUserId; + + @Excel(name = "下级用户充值金额") + private BigDecimal rechargePrice; + + /** + * 下级用户总消费 + */ + @TableField(exist = false) + private BigDecimal totalConsumption; + + @Excel(name = "返佣金额") + private BigDecimal rebate; + + @Excel(name = "充值记录ID") + private Integer rechargeRecordId; + + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtRechargeConfig.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtRechargeConfig.java new file mode 100644 index 0000000..31eb600 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtRechargeConfig.java @@ -0,0 +1,48 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_recharge_config") +public class TtRechargeConfig implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + // 名称 + private String name; + + @Excel(name = "充值金额") + private BigDecimal price; + + // 0启用,1禁用 + @Excel(name = "状态") + private String status; + + // 产品:金币 + @TableField("product_a") + private BigDecimal productA; + + // 图片信息 + @TableField("picture") + private String picture; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtRechargeProd.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtRechargeProd.java new file mode 100644 index 0000000..0380d7c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtRechargeProd.java @@ -0,0 +1,57 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_recharge_prod") +public class TtRechargeProd implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + // 名称 + private String name; + + @Excel(name = "充值金额") + private BigDecimal price; + + // 0启用,1禁用 + @Excel(name = "状态") + private String status; + + @Excel(name = "卡密链接") + private String cardLink; + + // 产品:金币 + @TableField("product_a") + private BigDecimal productA; + + // 产品:积分 + @TableField("product_c") + private BigDecimal productC; + + // 图片信息 + @TableField("picture") + private String picture; + + @Excel(name = "交易链接") + private String transactionLink; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserBlendErcash.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserBlendErcash.java new file mode 100644 index 0000000..4dd4beb --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserBlendErcash.java @@ -0,0 +1,102 @@ +package com.ruoyi.domain.entity; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; +import java.util.Objects; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_user_blend_ercash") +public class TtUserBlendErcash implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "id") + @TableId(value = "id",type = IdType.AUTO) + private Long id; + + @Excel(name = "user_id") + @TableField("user_id") + private Integer userId; + + /** + * 上级主播 ID(消费时的直属上级主播) + * 用于防借流水风控 - 记录消费时的归属关系快照 + */ + @Excel(name = "上级主播 ID") + @TableField("anchor_id") + private Integer anchorId; + + /** + * 最终招商 ID(上级主播所属的招商) + * 用于防借流水风控 - 记录消费时的归属关系快照 + */ + @Excel(name = "招商 ID") + @TableField("business_id") + private Integer businessId; + + @Excel(name = "金币") + @TableField("amount") + private BigDecimal amount; + + @Excel(name = "final_amount") + @TableField("final_amount") + private BigDecimal finalAmount; + + @Excel(name = "积分") + @TableField("credits") + private BigDecimal credits; + + @Excel(name = "final_credits") + @TableField("final_credits") + private BigDecimal finalCredits; + + @Excel(name = "合计") + @TableField("total") + private BigDecimal total; + + /** + * @see com.ruoyi.domain.common.constant.TtAccountRecordSource + */ + @Excel(name = "来源") + @TableField("source") + private Integer source; + + /** + * @see com.ruoyi.domain.common.constant.TtAccountRecordType + */ + @Excel(name = "收支类型") + @TableField("type") + private Integer type; + + @Excel(name = "create_time") + @TableField("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Timestamp createTime; + + @Excel(name = "update_time") + @TableField("update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Timestamp updateTime; + + @Excel(name = "笔记") + @TableField("remark") + private String remark; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserBlendErcashArchive.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserBlendErcashArchive.java new file mode 100644 index 0000000..515eb9e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserBlendErcashArchive.java @@ -0,0 +1,71 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 流水归档表 —— 存储 7 天前的历史流水数据。 + * 结构与 tt_user_blend_ercash 保持一致,额外增加 archive_time 标记归档时间。 + * id 不使用 AUTO_INCREMENT,直接搬运原表 id。 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("tt_user_blend_ercash_archive") +public class TtUserBlendErcashArchive implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 与原表 id 一致(迁移后原表删除) */ + @TableId(value = "id", type = IdType.INPUT) + private Long id; + + @TableField("user_id") + private Integer userId; + + @TableField("amount") + private BigDecimal amount; + + @TableField("final_amount") + private BigDecimal finalAmount; + + @TableField("credits") + private BigDecimal credits; + + @TableField("final_credits") + private BigDecimal finalCredits; + + @TableField("total") + private BigDecimal total; + + @TableField("source") + private Integer source; + + @TableField("type") + private Integer type; + + @TableField("remark") + private String remark; + + @TableField("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @TableField("update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 归档时间 */ + @TableField("archive_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date archiveTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserSourceReport.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserSourceReport.java new file mode 100644 index 0000000..870c964 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserSourceReport.java @@ -0,0 +1,58 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 用户来源日报表 —— 按 user_id + source + report_date 维度统计累计流水。 + * 凌晨 3 点由定时任务增量生成:今日 total = 昨日 total + 今日 delta。 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("tt_user_source_report") +public class TtUserSourceReport implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** 用户ID */ + @TableField("user_id") + private Integer userId; + + /** 邀请人ID(来自 tt_user.parent_id) */ + @TableField("parent_id") + private Integer parentId; + + /** 来源 source 码(对应 TtAccountRecordSource.code) */ + @TableField("source") + private Integer source; + + /** 截至当日的累计金额绝对值总和(与 tt_user_blend_ercash.total 语义对齐) */ + @TableField("total") + private BigDecimal total; + + /** 报表日期 */ + @TableField("report_date") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date reportDate; + + @TableField("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @TableField("update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserSubAccount.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserSubAccount.java new file mode 100644 index 0000000..97987e7 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtUserSubAccount.java @@ -0,0 +1,47 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 主号-副号关联表(一个主号最多绑定5个副号) + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("tt_user_sub_account") +public class TtUserSubAccount implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 主号用户ID + */ + private Long mainUid; + + /** + * 副号用户ID + */ + private Long subUid; + + /** + * 绑定时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/TtWelfareBoxInventory.java b/skins-model/src/main/java/com/ruoyi/domain/entity/TtWelfareBoxInventory.java new file mode 100644 index 0000000..7c75111 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/TtWelfareBoxInventory.java @@ -0,0 +1,48 @@ +package com.ruoyi.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("tt_welfare_box_inventory") +public class TtWelfareBoxInventory implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId + private Long id; + + /** 用户ID */ + private Long userId; + + /** 福利ID */ + private Integer welfareId; + + /** 箱子ID */ + private Integer boxId; + + /** 状态:0=未开启 1=已开启 */ + private Integer status; + + /** 领取日期 */ + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai") + private Date claimDate; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss" , timezone = "Asia/Shanghai") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/box/TtBoxThirdExplosiveUser.java b/skins-model/src/main/java/com/ruoyi/domain/entity/box/TtBoxThirdExplosiveUser.java new file mode 100644 index 0000000..f254964 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/box/TtBoxThirdExplosiveUser.java @@ -0,0 +1,41 @@ +package com.ruoyi.domain.entity.box; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_box_third_explosive_user") +public class TtBoxThirdExplosiveUser implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "宝箱ID") + private Integer boxId; + + @Excel(name = "参与者_用户ID") + private Integer userId; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/delivery/TtDeliveryRecord.java b/skins-model/src/main/java/com/ruoyi/domain/entity/delivery/TtDeliveryRecord.java new file mode 100644 index 0000000..3c11506 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/delivery/TtDeliveryRecord.java @@ -0,0 +1,102 @@ +package com.ruoyi.domain.entity.delivery; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_delivery_record") +public class TtDeliveryRecord implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "用户背包记录ID") + private Long boxRecordsId; + + @Excel(name = "饰品ID") + @TableField("ornament_id") + private Long ornamentId; + + @Excel(name = "市场唯一hashName") + @TableField("market_hash_name") + private String marketHashName; + + @Excel(name = "饰品价格") + private BigDecimal ornamentsPrice; + + @Excel(name = "网站订单号") + private String outTradeNo; + + /** + * 批次号,同一申请的多饰品共用(用于批量处理次数) + */ + @Excel(name = "批次号") + private String batchId; + + /** + * 申请提货时的 VIP 等级(锁定值,不随用户升级而变化) + */ + @Excel(name = "申请时VIP等级") + private Integer applyVipLevelId; + + /** + * 网站发货模式(1人工发货 2自动发货 3主播号提取) + */ + @Excel(name = "网站发货模式") + private Integer delivery; + + @Excel(name = "实际购买价格") + private BigDecimal buyPrice; + + @Excel(name = "第三方平台订单号") + private String orderId; + + @Excel(name = "第三方发货模式") + private Integer thirdpartyDelivery; + + /** + * 发货订单状态(1待发货 3待收货 10订单完成 11订单取消) + */ + @Excel(name = "发货订单状态") + private Integer status; + + /** + * 该批次次数是否已扣:0=未扣,1=已扣 + */ + @Excel(name = "次数已扣") + private Integer timesDeducted; + + private String message; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/exponent/TtExponent.java b/skins-model/src/main/java/com/ruoyi/domain/entity/exponent/TtExponent.java new file mode 100644 index 0000000..d013456 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/exponent/TtExponent.java @@ -0,0 +1,48 @@ +package com.ruoyi.domain.entity.exponent; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_exponent") +public class TtExponent implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "指数结果") + private Integer exponent; + + @Excel(name = "指数生成点") + private String points; + + // 1上升 2下降 + @Excel(name = "") + private Integer forward; + + @Excel(name = "当天第几期") + private Integer number; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/exponent/TtExponentUser.java b/skins-model/src/main/java/com/ruoyi/domain/entity/exponent/TtExponentUser.java new file mode 100644 index 0000000..b464d3b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/exponent/TtExponentUser.java @@ -0,0 +1,49 @@ +package com.ruoyi.domain.entity.exponent; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_exponent_user") +public class TtExponentUser implements Serializable { + @TableField(exist = false) + private static final long serialVersionUID = 1L; + @TableId(type = IdType.AUTO) + private Integer id; + @Excel(name = "指数ID") + private Integer exponentId; + @Excel(name = "盒子ID") + private Integer boxId; + @Excel(name = "用户ID") + private Integer userId; + @Excel(name = "盒子ID") + private BigDecimal price; + // 1上升 2下降 + @Excel(name = "1上升 2下降") + private Integer forward; + @Excel(name = "0默认,1成功,2失败") + private Integer status; + @Excel(name = "成功开箱结果ID") + private Long boxRecordId; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/exponent/TtExponentUserBox.java b/skins-model/src/main/java/com/ruoyi/domain/entity/exponent/TtExponentUserBox.java new file mode 100644 index 0000000..cf10177 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/exponent/TtExponentUserBox.java @@ -0,0 +1,39 @@ +package com.ruoyi.domain.entity.exponent; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_exponent_user_box") +public class TtExponentUserBox implements Serializable { + @TableField(exist = false) + private static final long serialVersionUID = 1L; + @TableId(type = IdType.AUTO) + private Integer id; + @Excel(name = "盒子ID") + private Integer boxId; + @Excel(name = "用户ID") + private Integer userId; + @Excel(name = "宝箱数量") + private Integer number; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/fight/FightSeat.java b/skins-model/src/main/java/com/ruoyi/domain/entity/fight/FightSeat.java new file mode 100644 index 0000000..954e63d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/fight/FightSeat.java @@ -0,0 +1,84 @@ +package com.ruoyi.domain.entity.fight; + +import cn.hutool.core.util.ObjectUtil; +import lombok.*; + +import java.math.BigDecimal; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class FightSeat { + + private Integer fightId; + + // 座位号 + private Integer code; + + private Integer playerId; + + // 玩家状态(0空位 1入座 2就绪) + private Integer status; + + // 头像 + private String avatar; + + private String nickName; + + private Integer vipLevel; + + private BigDecimal awardTotalPrices; + + public FightSeat(Integer fightId, Integer code) { + this.fightId = fightId; + this.code = code; + this.status = 0; + } + + public FightSeat(Integer code) { + this.code = code; + this.status = 0; + } + + // 入座 + public FightSeat sitDown(Integer playerId) { + if (ObjectUtil.isEmpty(code)) return null; + if (!status.equals(0)) return null; + this.playerId = playerId; + status = 1; + return this; + } + + // 离开座位 + public Boolean sitUp() { + if (ObjectUtil.isEmpty(this.playerId) || ObjectUtil.isEmpty(this.code)) { + return false; + } + if (!this.status.equals(1)) return false; + this.playerId = null; + this.status = 0; + // 与新建空位保持一致,使用 null 而非空串,避免前端把 "" 当作有效 URL 渲染破损头像 + this.nickName = null; + this.avatar = null; + this.vipLevel = null; + return true; + } + + // 准备 + public FightSeat ready() { + if (ObjectUtil.isEmpty(code) || ObjectUtil.isEmpty(playerId)) return null; + if (status.equals(0)) return null; + this.status = 2; + return this; + } + + // 取消准备 + public FightSeat readyCancel() { + if (ObjectUtil.isEmpty(code) || ObjectUtil.isEmpty(playerId)) return null; + if (status.equals(0)) return null; + if (status.equals(1)) return this; + this.status = 1; + return this; + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/fight/TtFight.java b/skins-model/src/main/java/com/ruoyi/domain/entity/fight/TtFight.java new file mode 100644 index 0000000..fb29950 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/fight/TtFight.java @@ -0,0 +1,100 @@ +package com.ruoyi.domain.entity.fight; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.domain.entity.fight.FightSeat; +import com.ruoyi.domain.vo.fight.FightBoxVO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_fight",autoResultMap = true) +public class TtFight implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "创建者ID") + private Integer userId; + + @Excel(name = "座位") + @TableField(value = "seats",typeHandler = JacksonTypeHandler.class) + private List seats; + + // 1、欧皇 2、非酋 + @Excel(name = "对战模式") + private String model; + + // 组队模式(0或null=个人对战 1=2v2组队,座位0/1为一队、座位2/3为二队) + @Excel(name = "组队模式") + private Integer teamMode; + + @Excel(name = "对战人数") + private Integer playerNum; + + /** + * 观战人数 + */ + @TableField(exist = false) + private Integer spectatorNum; + + @Excel(name = "回合数") + private Integer roundNumber; + + @TableField(value = "winner_ids",typeHandler = JacksonTypeHandler.class) + private List winnerIds; + + @Excel(name = "选择宝箱数据") + @TableField(value = "box_data",typeHandler = JacksonTypeHandler.class) + private Map boxData; + + @Excel(name = "创建宝箱价格总数") + private BigDecimal boxPriceTotal; + + // 对战状态(0准备 1进行中 2结束 3超时强制结束) + @Excel(name = "对战状态") + private Integer status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + // 开始时间 + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Timestamp beginTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @Excel(name = "备注") + private String remark; + + /** + * 房主昵称(入库,用户被物理删除后仍可展示) + */ + @Excel(name = "房主昵称") + private String ownerNickName; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLottery.java b/skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLottery.java new file mode 100644 index 0000000..38050b6 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLottery.java @@ -0,0 +1,72 @@ +package com.ruoyi.domain.entity.lottery; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 流水抽奖活动表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_turnover_lottery") +public class TtTurnoverLottery implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "抽奖 ID") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @Excel(name = "抽奖名称") + private String lotteryName; + + @Excel(name = "状态:0 关闭 1 开启") + private Integer status; + + @Excel(name = "类型:0 普通宝箱 1 流水抽奖") + private Integer lotteryType; + + @Excel(name = "该宝箱所需的流水门槛") + private Integer turnoverThreshold; + + @Excel(name = "宝箱封面图") + private String imageUrl; + + @Excel(name = "排序") + private Integer sort; + + @Excel(name = "奖品总价值") + private BigDecimal totalValue; + + @Excel(name = "每轮利润") + private Integer profitPerRound; + + @Excel(name = "利润率") + private BigDecimal profitRate; + + @Excel(name = "创建者") + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @Excel(name = "更新者") + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} \ No newline at end of file diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryPrize.java b/skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryPrize.java new file mode 100644 index 0000000..0cefb26 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryPrize.java @@ -0,0 +1,96 @@ +package com.ruoyi.domain.entity.lottery; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 流水抽奖奖品表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_turnover_lottery_prize") +public class TtTurnoverLotteryPrize implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "奖品 ID") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @Excel(name = "关联抽奖活动 ID") + private Long lotteryId; + + @Excel(name = "饰品 ID") + private Long ornamentId; + + @Excel(name = "饰品名称") + private String ornamentName; + + @Excel(name = "图片 URL") + private String imageUrl; + + @Excel(name = "饰品价格") + private BigDecimal price; + + @Excel(name = "饰品等级") + private Integer ornamentLevel; + + @Excel(name = "磨损度") + private String exteriorName; + + @Excel(name = "展示数量") + private Integer displayCount; + + @Excel(name = "真实数量") + private Integer realCount; + + @Excel(name = "初始真实数量") + private Integer initialRealCount; + + @Excel(name = "排序") + private Integer sort; + + @Excel(name = "已发放次数") + private Integer totalCount; + + // 计算字段 (不映射数据库,仅用于展示) + @TableField(exist = false) + @Excel(name = "显示爆率 (%)") + private Double theoreticalRate; + + @TableField(exist = false) + @Excel(name = "真实爆率 (%)") + private Double realRate; + + @TableField(exist = false) + private String displayPercent; + + @TableField(exist = false) + private String realPercent; + + @TableField(exist = false) + private Integer realRemaining; + + @TableField(exist = false) + private String levelImg; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryRecord.java b/skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryRecord.java new file mode 100644 index 0000000..e799ff4 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryRecord.java @@ -0,0 +1,60 @@ +package com.ruoyi.domain.entity.lottery; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 流水抽奖记录表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_turnover_lottery_record") +public class TtTurnoverLotteryRecord implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "记录 ID") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @Excel(name = "用户 ID") + private Integer userId; + + @Excel(name = "抽奖活动 ID") + private Long lotteryId; + + @Excel(name = "奖品 ID") + private Long prizeId; + + @Excel(name = "饰品 ID") + private Long ornamentId; + + @Excel(name = "饰品名称") + private String ornamentName; + + @Excel(name = "图片 URL") + private String imageUrl; + + @Excel(name = "奖品价值") + private BigDecimal ornamentsPrice; + + @Excel(name = "消耗的流水值") + private Integer costTurnover; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/recorde/TtUserAmountRecords.java b/skins-model/src/main/java/com/ruoyi/domain/entity/recorde/TtUserAmountRecords.java new file mode 100644 index 0000000..0b68387 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/recorde/TtUserAmountRecords.java @@ -0,0 +1,58 @@ +package com.ruoyi.domain.entity.recorde; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_user_amount_records") +public class TtUserAmountRecords implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + // 1、收入 2、支出 + private Integer type; + + // 来源 + private Integer source; + + @Excel(name = "变动金额") + private BigDecimal amount; + + @Excel(name = "最终金额") + private BigDecimal finalAmount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String remark; + + private Integer pwChildId; + private String pwChildName; + private BigDecimal pwChildAccount; + + @TableField("task_id") + private Integer taskId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/recorde/TtUserCreditsRecords.java b/skins-model/src/main/java/com/ruoyi/domain/entity/recorde/TtUserCreditsRecords.java new file mode 100644 index 0000000..ff7c5ef --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/recorde/TtUserCreditsRecords.java @@ -0,0 +1,58 @@ +package com.ruoyi.domain.entity.recorde; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_user_credits_records") +public class TtUserCreditsRecords implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + private Integer type; + + private Integer source; + + @Excel(name = "变动积分") + private BigDecimal credits; + + @Excel(name = "最终积分") + private BigDecimal finalCredits; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String remark; + + private Integer pwChildId; + + private String pwChildName; + + private BigDecimal pwChildAccount; + + @TableField("task_id") + private Integer taskId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtOfficialRollConfig.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtOfficialRollConfig.java new file mode 100644 index 0000000..a312b28 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtOfficialRollConfig.java @@ -0,0 +1,88 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 官方周期Roll房配置模板。 + * 每条配置指定奖池、周期(日/周)、充值门槛、人数等,由 OfficialRollTask 定时按周期自动创建 tt_roll 房间。 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_official_roll_config") +public class TtOfficialRollConfig implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + @Excel(name = "配置标识") + private String configName; + + @Excel(name = "奖池ID") + private Integer jackpotId; + + /** 周期类型: DAILY=每日, WEEKLY=每周 */ + @Excel(name = "周期类型") + private String periodType; + + @Excel(name = "房间名称") + private String roomName; + + @Excel(name = "房间描述") + private String description; + + private String rollPassword; + + @Excel(name = "充值门槛") + private BigDecimal minRecharge; + + @Excel(name = "VIP等级门槛") + private Integer vipLevel; + + @Excel(name = "人数上限") + private Integer peopleNum; + + @Excel(name = "排序依据") + private Integer sortBy; + + /** 状态: 0=启用, 1=停用 */ + @Excel(name = "状态") + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date lastGenerateTime; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date updateTime; + + @TableLogic + @TableField(select = false) + private String delFlag; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRoll.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRoll.java new file mode 100644 index 0000000..d628c31 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRoll.java @@ -0,0 +1,92 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_roll") +public class TtRoll implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "ROLL房ID") + @TableId(value = "id",type = IdType.AUTO) + private Integer id; + + @Excel(name = "奖池ID") + private Integer jackpotId; + + // 创建者 + private Integer userId; + + // 0官方 1主播 + private String rollType; + + @Excel(name = "Roll房名称") + private String rollName; + + @Excel(name = "Roll房描述") + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date endTime; + + @Excel(name = "人数限制") + private Integer peopleNum; + + @Excel(name = "Roll房参与密码") + private String rollPassword; + + @Excel(name = "充值门槛") + private BigDecimal minRecharge; + + @Excel(name = "VIP等级门槛") + private Integer vipLevel; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date rechargeStartTime; + + @Excel(name = "排序依据") + private Integer sortBy; + + // 0未开奖 1已开奖 + @Excel(name = "状态") + private String rollStatus; + + @Excel(name = "创建者") + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @TableField(select = false) + private String delFlag; + + // 主播房创建时实际扣除的积分(=min(奖池总价值,当时积分)),用于开奖/删除按比例退还,避免积分虚增 + private BigDecimal deductCredits; + + // 官方周期Roll配置ID(tt_official_roll_config.id),标记该房由哪条配置生成,手动创建的房间为NULL + private Integer officialConfigId; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollBody.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollBody.java new file mode 100644 index 0000000..3ff457a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollBody.java @@ -0,0 +1,13 @@ +package com.ruoyi.domain.entity.roll; + +import lombok.Data; + +@Data +public class TtRollBody { + + private Integer id; + + private String rollName; + + private String rollStatus; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollJackpot.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollJackpot.java new file mode 100644 index 0000000..eff9aac --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollJackpot.java @@ -0,0 +1,48 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_roll_jackpot") +public class TtRollJackpot implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer jackpotId; + + private String jackpotName; + + @TableField("total_price") + private BigDecimal totalPrice; + + private String description; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollJackpotOrnaments.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollJackpotOrnaments.java new file mode 100644 index 0000000..cc89bc5 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollJackpotOrnaments.java @@ -0,0 +1,63 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_roll_jackpot_ornaments") +public class TtRollJackpotOrnaments implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "Roll房奖池ID") + private Integer jackpotId; + + @Excel(name = "Roll房奖池饰品ID") + private Long ornamentsId; + + @Excel(name = "饰品价格") + private BigDecimal price; + + @Excel(name = "饰品图片") + private String imgUrl; + + @Excel(name = "饰品名称") + private String ornamentName; + + @Excel(name = "饰品级别ID") + @TableField("ornament_level_id") + private Integer ornamentLevelId; + + @Excel(name = "饰品数量") + private Integer ornamentsNum; + + @Excel(name = "指定获奖用户ID,多个用户用英文逗号分隔") + private String designatedUserId; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollJackpotOrnamentsBody.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollJackpotOrnamentsBody.java new file mode 100644 index 0000000..ca76ffe --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollJackpotOrnamentsBody.java @@ -0,0 +1,12 @@ +package com.ruoyi.domain.entity.roll; + +import lombok.Data; + +@Data +public class TtRollJackpotOrnamentsBody { + + private Integer jackpotId; + + private Integer id; + private String itemName; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollPrizeTemplate.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollPrizeTemplate.java new file mode 100644 index 0000000..55d4f8c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollPrizeTemplate.java @@ -0,0 +1,41 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +/** + * Roll房奖项模板 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_roll_prize_template") +public class TtRollPrizeTemplate implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + /** 模板名称 */ + private String templateName; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollPrizeTemplateDetail.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollPrizeTemplateDetail.java new file mode 100644 index 0000000..c7f9c8d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollPrizeTemplateDetail.java @@ -0,0 +1,52 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * Roll房奖项模板明细(模板内每个饰品) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_roll_prize_template_detail") +public class TtRollPrizeTemplateDetail implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + /** 模板ID */ + private Integer templateId; + + /** 饰品ID */ + private Long ornamentId; + + /** 饰品名称 */ + private String ornamentName; + + /** 饰品图片 */ + private String imgUrl; + + /** 饰品价格 */ + private BigDecimal price; + + /** 饰品数量 */ + private Integer ornamentsNum; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollUser.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollUser.java new file mode 100644 index 0000000..d93b0fe --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollUser.java @@ -0,0 +1,70 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_roll_user") +public class TtRollUser implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "用户名") + private String userName; + + @Excel(name = "昵称") + private String nickName; + + @Excel(name = "头像") + private String avatar; + + @Excel(name = "Roll房ID") + private Integer rollId; + + @Excel(name = "奖池饰品列表ID") + private Integer jackpotOrnamentsId; + + @Excel(name = "奖池饰品ID") + private Long ornamentsId; + + @Excel(name = "宝箱记录ID") + private Long boxRecordId; + + // 2系统指定获奖者 + private String status; + + @Excel(name = "指定者") + private String designatedBy; + + @TableField("get_prize_way") + private Integer getPrizeWay; + + @JsonFormat(pattern = "yyyy-MM-dd") + private Date joinTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollUserPrize.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollUserPrize.java new file mode 100644 index 0000000..2000f11 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtRollUserPrize.java @@ -0,0 +1,67 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_roll_user_prize") +public class TtRollUserPrize implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "roll用户ID") + @TableField("roll_user_id") + private Integer rollUserId; + + @Excel(name = "Roll房奖池id") + @TableField("roll_jackpot_id") + private Integer rollJackpotId; + + @Excel(name = "奖池饰品ID") + @TableField("roll_jackpot_ornament_id") + private Integer rollJackpotOrnamentId; + + @Excel(name = "饰品ID") + @TableField("ornament_id") + private Long ornamentId; + + @Excel(name = "饰品名称") + @TableField("ornament_name") + private String ornamentName; + + @Excel(name = "饰品图片") + @TableField("img_url") + private String imgUrl; + + @Excel(name = "饰品价格") + @TableField("price") + private BigDecimal price; + + @Excel(name = "奖品数量") + @TableField("number") + private Integer number; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtTimeRoll.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtTimeRoll.java new file mode 100644 index 0000000..ccc3256 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtTimeRoll.java @@ -0,0 +1,66 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_time_roll") +public class TtTimeRoll implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "奖池ID") + private Integer jackpotId; + + @Excel(name = "房间名称") + private String name; + + @Excel(name = "房间描述") + private String description; + + private String rechargeCondition; + + @Excel(name = "充值门槛") + private BigDecimal minRecharge; + + @Excel(name = "排序依据") + private Integer sortBy; + + private String status; + + @Excel(name = "创建者") + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @TableField(select = false) + private String delFlag; + + private Integer jobId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtTimeRollUser.java b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtTimeRollUser.java new file mode 100644 index 0000000..82f19d4 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/roll/TtTimeRollUser.java @@ -0,0 +1,59 @@ +package com.ruoyi.domain.entity.roll; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_time_roll_user") +public class TtTimeRollUser implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "时间Roll房ID") + private Integer timeRollId; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "奖池饰品列表ID") + private Integer jackpotOrnamentsId; + + @Excel(name = "奖池饰品ID") + private Long ornamentsId; + + @Excel(name = "宝箱记录ID") + private Long boxRecordId; + + private String status; + + @Excel(name = "指定者") + private String designatedBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date joinTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @Excel(name = "结束状态", readConverterExp = "0=未开始,1=已结束") + private String endStatus; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/sys/PromoAdmin.java b/skins-model/src/main/java/com/ruoyi/domain/entity/sys/PromoAdmin.java new file mode 100644 index 0000000..ff7ad80 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/sys/PromoAdmin.java @@ -0,0 +1,65 @@ +package com.ruoyi.domain.entity.sys; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Date; + +/** + * 推广后台管理员表(独立于tt_user前端用户表) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("promo_admin") +public class PromoAdmin implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 登录账号 + */ + private String username; + + /** + * 密码(BCrypt加密) + */ + private String password; + + /** + * 昵称 + */ + private String nickName; + + /** + * 手机号 + */ + private String phoneNumber; + + /** + * 邮箱 + */ + private String email; + + /** + * 头像(base64 dataURL) + */ + private String avatar; + + /** + * 状态 0正常 1停用 + */ + private String status; + + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/sys/TtPromotionUpdate.java b/skins-model/src/main/java/com/ruoyi/domain/entity/sys/TtPromotionUpdate.java new file mode 100644 index 0000000..d56f396 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/sys/TtPromotionUpdate.java @@ -0,0 +1,39 @@ +package com.ruoyi.domain.entity.sys; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.sql.Timestamp; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_promotion_update") +public class TtPromotionUpdate { + + @TableId(value = "id",type = IdType.AUTO) + private Integer id; + + @TableField("employee_id") + private Integer employeeId; + + @TableField("boss_id") + private Integer bossId; + + @TableField("create_time") + private Timestamp createTime; + + @TableField("update_time") + private Timestamp updateTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/sys/TtUser.java b/skins-model/src/main/java/com/ruoyi/domain/entity/sys/TtUser.java new file mode 100644 index 0000000..b998eba --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/sys/TtUser.java @@ -0,0 +1,201 @@ +package com.ruoyi.domain.entity.sys; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_user") +public class TtUser implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "用户ID") + @TableId(value = "user_id", type = IdType.INPUT) + private Integer userId; + + @Excel(name = "用户账号") + private String userName; + + @Excel(name = "用户昵称") + private String nickName; + + /** + * @see com.ruoyi.domain.common.constant.UserType + */ + private String userType; + + @Excel(name = "用户邮箱") + private String email; + + @Excel(name = "手机号码") + private String phoneNumber; + + @Excel(name = "头像地址") + private String avatar; + + @Excel(name = "密码") + private String password; + + /** + * 安全密码(6位数字,提货时校验,与登录密码完全独立,BCrypt 加密存储) + */ + @TableField("safe_password") + private String safePassword; + + /** + * 安全密码明文(仅供后台详情展示/编辑回填,不参与校验;与 password 明文存 remark 同一约定) + */ + @TableField("safe_password_plain") + private String safePasswordPlain; + + @Excel(name = "账户金额") + private BigDecimal accountAmount; + + @Excel(name = "账户积分") + private BigDecimal accountCredits; + + /** + * 卡商专用余额(仅卡商用户使用,与 account_amount/account_credits 完全独立) + */ + private BigDecimal merchantBalance; + + @Excel(name = "邀请码") + private String invitationCode; + + @Excel(name = "上级ID") + private Integer parentId; + + @Excel(name = "VIP等级") + private Integer vipLevel; + + /** + * 累计充值金额(缓存字段,避免每次从流水表+归档表联合查询) + */ + private BigDecimal totalRechargeAmount; + + @Excel(name = "推广等级") + private Integer promotionLevel; + + private String status; + + private String deliveryStatus; + + /** + * 赠送状态:0=启用(可被赠送),1=禁用(不可被赠送) + */ + private String giftStatus; + + @Excel(name = "steam账号ID") + private Long steamId; + + @Excel(name = "steam交易链接") + @TableField("transaction_link") + private String transactionLink; + + @Excel(name = "真实姓名") + private String realName; + + @Excel(name = "身份证号码") + private String idNum; + + @Excel(name = "实名认证流程号") + private String certifyId; + + private String isRealCheck; + + /** + * 是否白号:0=否 1=是 + * 白号:可正常游戏、可出现在榜单,但提货虚假成功、充值不计入真实充值、 + * 流水不计入主播收益、不触发返佣 + */ + private Integer isBlankAccount; + + /** + * 邀请新人充值比例(主页分享返佣)。 + * 下级用户充值时,按本字段作为比例计算返利并打入本账户,取值区间 0.01~0.1。 + */ + private BigDecimal commissionRate; + + /** + * 荒野争锋新游戏消费返点比例 + */ + private BigDecimal newGameCommissionRate; + + /** + * 经典玩法游戏消费返点比例(经典开箱、对战、幸运升级、疯狂转盘) + */ + private BigDecimal classicGameCommissionRate; + + @Excel(name = "最后登录IP") + private String loginIp; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "Asia/Shanghai") + private Date loginDate; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "Asia/Shanghai") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "Asia/Shanghai") + private Date updateTime; + + @Excel(name = "备注") + private String remark; + + //0 正常 + private String delFlag; + + @TableField(select = false) + private String deliveryAddress; + + @TableField(exist = false) + private List children; + + @Override + public boolean equals(Object obj) { + System.out.println("调用equals方法,当前的hashCode为:" + hashCode()); + /** 对象是 null 直接返回 false **/ + if (obj == null) { + return false; + } + /** 对象是当前对象,直接返回 true **/ + if (this == obj) { + return true; + } + /** 判断对象类型是否是User **/ + if (obj instanceof TtUser) { + TtUser vo = (TtUser) obj; + if (vo.getUserId().equals(this.userId)) { + return true; + } + } + return false; + } + + @Override + public int hashCode() { + return this.getUserId().hashCode(); + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/transfer/TtTransferApply.java b/skins-model/src/main/java/com/ruoyi/domain/entity/transfer/TtTransferApply.java new file mode 100644 index 0000000..1eff36f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/transfer/TtTransferApply.java @@ -0,0 +1,98 @@ +package com.ruoyi.domain.entity.transfer; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_transfer_apply") +public class TtTransferApply implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + /** 转赠批次号(同一批次内多个饰品一起审核) */ + @Excel(name = "批次号") + @TableField("batch_id") + private String batchId; + + @Excel(name = "背包记录ID") + @TableField("box_record_id") + private Long boxRecordId; + + @Excel(name = "转赠人ID") + @TableField("from_user_id") + private Integer fromUserId; + + /** + * 提交转赠时的VIP等级 + */ + @Excel(name = "提交时VIP等级") + @TableField("vip_level") + private Integer vipLevel; + + @Excel(name = "受赠人ID") + @TableField("to_user_id") + private Integer toUserId; + + @Excel(name = "饰品ID") + @TableField("ornament_id") + private Long ornamentId; + + @Excel(name = "饰品名称") + @TableField("ornament_name") + private String ornamentName; + + @Excel(name = "饰品图片") + @TableField("ornament_image_url") + private String ornamentImageUrl; + + @Excel(name = "饰品价格") + @TableField("ornament_price") + private BigDecimal ornamentPrice; + + /** + * 状态:0-待审核,1-审核通过,2-审核驳回 + */ + @Excel(name = "状态") + private Integer status; + + /** + * 该批次次数是否已扣:0=未扣,1=已扣 + */ + @Excel(name = "次数已扣") + @TableField("times_deducted") + private Integer timesDeducted; + + @Excel(name = "驳回原因") + @TableField("reject_reason") + private String rejectReason; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/entity/transfer/TtUserTransferDaily.java b/skins-model/src/main/java/com/ruoyi/domain/entity/transfer/TtUserTransferDaily.java new file mode 100644 index 0000000..218c1e5 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/entity/transfer/TtUserTransferDaily.java @@ -0,0 +1,85 @@ +package com.ruoyi.domain.entity.transfer; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Date; + +/** + * 用户每日转赠次数记录 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_user_transfer_daily") +public class TtUserTransferDaily implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Integer userId; + + /** + * 提交/生效时的VIP等级(tt_vip_level.id) + */ + private Integer vipLevel; + + /** + * 统计日期 + */ + private LocalDate statDate; + + /** + * 该VIP等级当日转赠次数上限 + */ + private Integer totalCount; + + /** + * 该VIP等级当日已成功转赠次数 + */ + private Integer usedCount; + + /** + * 该VIP等级当日审核中预扣次数 + */ + private Integer pendingCount; + + /** + * 该VIP等级当日提货次数上限 + */ + private Integer deliveryTotalCount; + + /** + * 该VIP等级当日已成功提货次数 + */ + private Integer deliveryUsedCount; + + /** + * 该VIP等级当日审核中提货预扣次数 + */ + private Integer deliveryPendingCount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiCaptchaCheckRequest.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiCaptchaCheckRequest.java new file mode 100644 index 0000000..8f39bec --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiCaptchaCheckRequest.java @@ -0,0 +1,23 @@ +package com.ruoyi.domain.other; + +import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; +import lombok.Data; + +@Data +public class ApiCaptchaCheckRequest { + + /** + * 验证码ID + */ + private String id; + + /** + * 滑块轨迹数据 + */ + private ImageCaptchaTrack data; + + /** + * 手机号,用于绑定验证状态 + */ + private String phoneNumber; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiForgetPasswordBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiForgetPasswordBody.java new file mode 100644 index 0000000..5b2e56f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiForgetPasswordBody.java @@ -0,0 +1,15 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class ApiForgetPasswordBody { + + private String phoneNumber; + + private String code; + + private String password; + + private String confirmPassword; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiLoginBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiLoginBody.java new file mode 100644 index 0000000..1c3c247 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiLoginBody.java @@ -0,0 +1,15 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Data +public class ApiLoginBody { + + @NotNull(message = "用户名不能为空") + private String username; + + @NotNull(message = "密码不能为空") + private String password; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiLuckyUpgradeBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiLuckyUpgradeBody.java new file mode 100644 index 0000000..cb37b6e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiLuckyUpgradeBody.java @@ -0,0 +1,22 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ApiLuckyUpgradeBody { + + private Integer id; + + private String type; + + private String itemName; + + private BigDecimal priceMin = BigDecimal.ZERO; + + private BigDecimal priceMax; + + /** 当前查看用户类型(01主播 02普通),用于按类型返回对应幸运值;由后端解析注入 */ + private String userType; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiRegisterBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiRegisterBody.java new file mode 100644 index 0000000..f9c413a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiRegisterBody.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.other; + +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +@Data +public class ApiRegisterBody { + + @Length(min = 2,max = 12) + private String nickName; + + private String phoneNumber; + + private String password; + + private String parentInvitationCode; + + private String code; + + private String bdVid; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiSafePasswordBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiSafePasswordBody.java new file mode 100644 index 0000000..cb93784 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiSafePasswordBody.java @@ -0,0 +1,16 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +/** + * 设置安全密码(提货校验用,与登录密码独立) + */ +@Data +public class ApiSafePasswordBody { + + private String phoneNumber; + + private String code; + + private String safePassword; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiShoppingBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiShoppingBody.java new file mode 100644 index 0000000..ac389d4 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiShoppingBody.java @@ -0,0 +1,45 @@ +package com.ruoyi.domain.other; + +import io.swagger.annotations.ApiParam; +import lombok.Data; + +import jakarta.validation.constraints.Min; +import java.math.BigDecimal; + +@Data +public class ApiShoppingBody { + + @ApiParam("商品名称") + private String name; + + @ApiParam("类型(0全部饰品 1匕首 2手枪 3步枪 4微型冲锋枪 5重型武器 6手套 7印花 8其它)") + private String type; + + @ApiParam("品质(1普通 2StatTrak™ 3纪念品 4★ 5★ StatTrak™ 6闪耀 7金色 8全息 9闪亮)") + private String quality; + + @ApiParam("稀有度") + private String rarity; + + @ApiParam("外观(1崭新出厂 2略有磨损 3久经沙场 4残损不堪 5战痕累累 6无涂装)") + private String exterior; + + @ApiParam("最高价格") + private BigDecimal maxPrice; + + @ApiParam("最低价格") + private BigDecimal minPrice = BigDecimal.ZERO; + + @ApiParam("1价格升序 2价格降序 3更新时间升序 4更新时间降序") + private Integer sortBy; + + @Min(value = 1, message = "每页长度最小为1") + private Integer pageSize; + + @Min(value = 1, message = "页码最小为1") + private Integer pageNum; + + @ApiParam("是否可兑换") + private Boolean isExchange; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiUpdateUserDetailsBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiUpdateUserDetailsBody.java new file mode 100644 index 0000000..403a8de --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiUpdateUserDetailsBody.java @@ -0,0 +1,33 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Data +public class ApiUpdateUserDetailsBody { + + private String nickName; + + private String email; + + private String phoneNumber; + private String code; + + @NotNull + private String oldPassword; + + @NotNull + private String password; + + @NotNull + private String passwordAgain; + + private String avatar; + + private String parentInvitationCode; + + private String transactionLink; + + private String deliveryAddress; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiUserOnline.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiUserOnline.java new file mode 100644 index 0000000..719c1da --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiUserOnline.java @@ -0,0 +1,23 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class ApiUserOnline { + + private String token; + + private String nickName; + + private String userName; + + private String ipaddr; + + private String loginLocation; + + private String browser; + + private String os; + + private Long loginTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiVerificationCodeLoginBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiVerificationCodeLoginBody.java new file mode 100644 index 0000000..80546d8 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiVerificationCodeLoginBody.java @@ -0,0 +1,22 @@ +package com.ruoyi.domain.other; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Data +public class ApiVerificationCodeLoginBody { + + @JsonAlias("username") + @NotNull(message = "手机号不能为空") + private String phoneNumber; + + @NotNull(message = "验证码不能为空") + private String code; + + /** + * 上级邀请码(推广后台:手机号未注册时用它自动注册为主播,其它场景忽略) + */ + private String parentInvitationCode; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ApiVerificationCodeLoginOrRegisterBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ApiVerificationCodeLoginOrRegisterBody.java new file mode 100644 index 0000000..46a232a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ApiVerificationCodeLoginOrRegisterBody.java @@ -0,0 +1,16 @@ +package com.ruoyi.domain.other; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class ApiVerificationCodeLoginOrRegisterBody { + + @NotNull(message = "手机号不能为空") + private String phoneNumber; + + @NotNull(message = "验证码不能为空") + private String code; + + private String parentInvitationCode; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/BoxDataBodyA.java b/skins-model/src/main/java/com/ruoyi/domain/other/BoxDataBodyA.java new file mode 100644 index 0000000..242a52c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/BoxDataBodyA.java @@ -0,0 +1,13 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class BoxDataBodyA { + private Integer boxId; + private Integer boxNum; + private Integer number; + private String boxName; + private String boxImg01; + private String boxImg02; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ConfigData.java b/skins-model/src/main/java/com/ruoyi/domain/other/ConfigData.java new file mode 100644 index 0000000..ee17037 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ConfigData.java @@ -0,0 +1,49 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "sys_config") +public class ConfigData implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Long configId; + + private String configName; + + private String configKey; + + private String configValue; + + private String configType; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private String remark; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/CreateFightBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/CreateFightBody.java new file mode 100644 index 0000000..e037013 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/CreateFightBody.java @@ -0,0 +1,34 @@ +package com.ruoyi.domain.other; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +@Data +public class CreateFightBody { + + @ApiModelProperty("模式(0欧皇 1非酋)") + @NotBlank(message = "模式不能为空") + private String model; + + @ApiModelProperty("玩家人数") + @Max(value = 12, message = "最大值12") + private Integer playerNumber; + + @ApiModelProperty("组队模式(0或空=个人对战 1=2v2组队,座位0/1为一队、座位2/3为二队)") + private Integer teamMode; + + // 所有宝箱 + @NotNull(message = "宝箱数据不能为空") + private Map boxIdAndNumber; + + // private Integer rounds; + // 回合数 + // private Integer createNum = 1; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/CreateOrderParam.java b/skins-model/src/main/java/com/ruoyi/domain/other/CreateOrderParam.java new file mode 100644 index 0000000..86f2d69 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/CreateOrderParam.java @@ -0,0 +1,32 @@ +package com.ruoyi.domain.other; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.NonNull; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; + +@Data +public class CreateOrderParam { + + @NotNull(message = "商品id不能为空。") + private Integer goodsId; + + @NotNull(message = "商品价格不能为空。") + private BigDecimal goodsPrice; + + @NotNull(message = "商品数量不能为空。") + @Min(value = 1, message = "最小值1") + private Integer goodsNum; + + @ApiModelProperty("对接qs支付新增:支付方式(alipay/wxpay)") + private String payType; + + @ApiModelProperty("1:原有支付,2:图片支付") + private Integer payWay = 1; + + @ApiModelProperty("百度推广bdvid") + private String bdVid; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/FightBoutData.java b/skins-model/src/main/java/com/ruoyi/domain/other/FightBoutData.java new file mode 100644 index 0000000..ee5e237 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/FightBoutData.java @@ -0,0 +1,17 @@ +package com.ruoyi.domain.other; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class FightBoutData { + + @ApiModelProperty("对战ID") + private Integer fightId; + + @ApiModelProperty("对战回合号") + private Integer boutNum; + + @ApiModelProperty("过期时间") + private Integer expirationTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/JackpotData.java b/skins-model/src/main/java/com/ruoyi/domain/other/JackpotData.java new file mode 100644 index 0000000..2950554 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/JackpotData.java @@ -0,0 +1,34 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class JackpotData { + + private String itemName; + + private BigDecimal usePrice; + + private String imageUrl; + + private String shortName; + + private String typeName; + + private String exteriorName; + + private String level; + + private String levelImg; + + private Integer id; + + private Integer allocatedNum; + + private Integer unAllocatedNum; + + private Integer ornamentsNum; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/OperationalStatistics.java b/skins-model/src/main/java/com/ruoyi/domain/other/OperationalStatistics.java new file mode 100644 index 0000000..cac9640 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/OperationalStatistics.java @@ -0,0 +1,17 @@ +package com.ruoyi.domain.other; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class OperationalStatistics { + + @JsonFormat(pattern = "yyyy-MM-dd") + private Date date; + private BigDecimal rechargePriceTotal; + private BigDecimal deliveryPriceTotal; + private BigDecimal profit; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ParameterSettingBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ParameterSettingBody.java new file mode 100644 index 0000000..8de345c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ParameterSettingBody.java @@ -0,0 +1,59 @@ +package com.ruoyi.domain.other; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class ParameterSettingBody { + + private String usePricePremiumRate; + private String exchangePriceRatio; + private String registerRedPacket; + private String maxCompoundNum; + private String compoundMinPrice; + private String compoundMinPremiumRate; + private String compoundMaxPremiumRate; + private String ZBTParities; + private String buyPricePremiumRate; + private String autoDeliveryMinPrice; + private String websiteMaintenance; + private String shoppingMaintenance; + private String fightMaintenance; + private String bindBoxMaintenance; + private String rollMaintenance; + private String compoundMaintenance; + private String westernDuelMaintenance; + private String luckyUpgradeMaintenance; + private String wheelMaintenance; + + /** 玫瑰女郎维护开关:0正常 1维护(对应 sys_config 键 olympusMaintenance,产品已更名,key 保持不变) */ + private String olympusMaintenance; + + /** 充值渠道维护开关:0正常 1维护(仅控制第三方支付渠道 unifiedPay) */ + private String rechargeMaintenance; + + /** 卡密充值维护开关:0正常 1维护(仅控制卡密充值 cardPay) */ + private String cardRechargeMaintenance; + + /** 推广后台维护开关:0正常 1维护 */ + private String promoMaintenance; + + /** 支付公司:chengduhipay */ + private String payCompany; + + /** 支付公司权重配置,JSON格式: [{"name":"chengduhipay","weight":10}] */ + private String payCompanyWeights; + + /** 流水归档保留天数,超过该天数的流水将迁移至归档表,兜底 "7" */ + private String flowArchiveRetainDays; + + /** SnapPay 开关:1=启用(1000-50000 强制走 SnapPay),0=停用(回落权重路由),兜底 "1" */ + private String snappayEnabled; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/PayCompanyWeight.java b/skins-model/src/main/java/com/ruoyi/domain/other/PayCompanyWeight.java new file mode 100644 index 0000000..5592d10 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/PayCompanyWeight.java @@ -0,0 +1,22 @@ +package com.ruoyi.domain.other; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 支付公司权重配置项 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PayCompanyWeight { + /** 公司标识,如 chengduhipay */ + private String name; + /** 显示名称 */ + private String label; + /** 权重值 */ + private int weight; + /** 支持的支付渠道:1=支付宝,2=银联。为空表示支持所有渠道 */ + private Integer payType; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/RealNameAuthenticationBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/RealNameAuthenticationBody.java new file mode 100644 index 0000000..66a9dd3 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/RealNameAuthenticationBody.java @@ -0,0 +1,12 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class RealNameAuthenticationBody { + + private String realName; + private String idNum; + private String phoneNumber; + private String captcha; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/ShoppingBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/ShoppingBody.java new file mode 100644 index 0000000..3a13a7b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/ShoppingBody.java @@ -0,0 +1,18 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ShoppingBody { + + private Long id; + private String itemName; + private String type; + private String exterior; + private BigDecimal maxPrice; + private BigDecimal minPrice = BigDecimal.ZERO; + private Integer isPutaway; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TransferApplyBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TransferApplyBody.java new file mode 100644 index 0000000..e1e59f8 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TransferApplyBody.java @@ -0,0 +1,45 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import java.util.List; + +/** + * 转赠申请查询条件 + */ +@Data +public class TransferApplyBody { + + /** 批次号 */ + private String batchId; + + /** 转赠人用户ID */ + private Integer fromUserId; + + /** 转赠人昵称(模糊查询) */ + private String fromNickName; + + /** 受赠人用户ID */ + private Integer toUserId; + + /** 受赠人昵称(模糊查询) */ + private String toNickName; + + /** 饰品名称(模糊查询) */ + private String ornamentName; + + /** 状态:0-待审核,1-审核通过,2-审核驳回 */ + private Integer status; + + /** 状态列表:用于查询多个状态,如已处理页面查询 1,2 */ + private List statusList; + + /** 手机号 */ + private String phoneNumber; + + /** 开始时间 */ + private String startTime; + + /** 结束时间 */ + private String endTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtAdvertisement.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtAdvertisement.java new file mode 100644 index 0000000..1d9166d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtAdvertisement.java @@ -0,0 +1,44 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_advertisement") +public class TtAdvertisement implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "标题") + private String title; + + @Excel(name = "图片") + private String picture; + + @Excel(name = "跳转链接") + private String jumpLink; + + @Excel(name = "图片排序") + private Integer sort; + + // 0可用 1禁用 + @Excel(name = "状态") + private String status; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtAnnouncement.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtAnnouncement.java new file mode 100644 index 0000000..3113628 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtAnnouncement.java @@ -0,0 +1,39 @@ +package com.ruoyi.domain.other; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +/** + * 公告实体类 + */ +@Data +public class TtAnnouncement { + + private Integer AnnouncementId; + + private String title; + + private String content; + + /** + * 公告类型:0=用户公告(需登录、有已读状态),1=系统公告(免登录、无已读状态) + */ + private Integer type; + + /** + * 目标用户ID,NULL表示全部用户 + */ + private Long targetUserId; + + /** + * 消息分类:GAME_xxx 或 SYS_xxx + */ + private String category; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String read; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBanner.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBanner.java new file mode 100644 index 0000000..f4e8280 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBanner.java @@ -0,0 +1,43 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_banner") +public class TtBanner implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "标题") + private String title; + + @Excel(name = "图片") + private String picture; + + @Excel(name = "跳转链接") + private String jumpLink; + + @Excel(name = "图片排序") + private Integer sort; + + // 0可用 1禁用 + @Excel(name = "状态") + private String status; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBonus.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBonus.java new file mode 100644 index 0000000..03c16ac --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBonus.java @@ -0,0 +1,69 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_bonus") +public class TtBonus implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "福利名称") + private String name; + + @Excel(name = "福利描述") + private String description; + + @Excel(name = "封面图片") + private String coverPicture; + + @Excel(name = "福利类型", readConverterExp = "2=充值红包,3=福利宝箱") + private String type; + + @Excel(name = "条件类型") + private String conditionType; + + @Excel(name = "充值门槛") + private BigDecimal rechargeThreshold; + + @Excel(name = "奖励区间") + private String awardSection; + + @Excel(name = "启用状态", readConverterExp = "0=启用,1=禁用") + private String status; + + @Excel(name = "排序依据") + private Integer sortBy; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBonusReceiveRecord.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBonusReceiveRecord.java new file mode 100644 index 0000000..b658d1b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBonusReceiveRecord.java @@ -0,0 +1,68 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_bonus_receive_record") +public class TtBonusReceiveRecord implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "福利类型") + private String type; + + @Excel(name = "福利ID") + private Integer bonusId; + + @Excel(name = "VIP等级ID") + private Integer vipLevelId; + + @Excel(name = "推广等级ID") + private Integer promotionLevelId; + + @Excel(name = "领取人ID") + private Integer userId; + + @Excel(name = "奖励类型") + private String awardType; + + @Excel(name = "奖励ID", readConverterExp = "0=金币") + private Integer awardId; + + @Excel(name = "奖励价值") + private BigDecimal awardPrice; + + @Excel(name = "状态", readConverterExp = "0=未领取,1=已领取") + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "领取时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date receiveTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBox.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBox.java new file mode 100644 index 0000000..7118fcc --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBox.java @@ -0,0 +1,91 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_box") +public class TtBox implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "宝箱ID") + @TableId + private Integer boxId; + + @Excel(name = "宝箱名称") + private String boxName; + + @Excel(name = "宝箱所属分类ID") + private String boxTypeId; + + @Excel(name = "宝箱价格") + private BigDecimal price; + + @Excel(name = "宝箱图片01") + private String boxImg01; + + @Excel(name = "宝箱图片02") + private String boxImg02; + + @Excel(name = "宝箱排序") + private Integer sort; + + // 0:是对战宝箱 + @Excel(name = "是否对战宝箱") + private String isFight; + + /** 宝箱类型 + * 1 普通箱 + * 2 对战箱 + * 3 补偿箱 + * 4 九宫格 + * 5 指数盲盒 + */ + private String boxType; + + /** 补偿值 */ + private Integer compAmount; + + @Excel(name = "宝箱状态") + private String status; + + @Excel(name = "宝箱开启次数") + private Long openNum; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private String remark; + + @Excel(name = "是否首页推荐") + private String isHome; + + @TableField(select = false) + @Excel(name = "删除标志") + private String delFlag; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxA.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxA.java new file mode 100644 index 0000000..834e01a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxA.java @@ -0,0 +1,22 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Data +public class TtBoxA { + + private Integer boxId; + private Integer boxTypeId; + private String boxName; + private BigDecimal price; + private String boxImg01; + private String boxImg02; + private String boxType; + private Integer totalOpenOdds; + private List boxOrnamentsList; + private List probabilityDistribution; + private List openProbabilityDistribution; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxBody.java new file mode 100644 index 0000000..485f245 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxBody.java @@ -0,0 +1,18 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class TtBoxBody { + + private Integer boxId; + + private String boxName; + + private String isFight; + + private String status; + + /** 宝箱类型:1普通箱 2对战箱 3补偿箱 4九宫格 5指数盲盒 6福利箱 */ + private String boxType; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxLevelA.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxLevelA.java new file mode 100644 index 0000000..28bde19 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxLevelA.java @@ -0,0 +1,13 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class TtBoxLevelA { + + private Integer ornamentsLevelId; + + private String level; + + private String probability; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxOpenChance.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxOpenChance.java new file mode 100644 index 0000000..9d9e524 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxOpenChance.java @@ -0,0 +1,33 @@ +package com.ruoyi.admin.domain; + +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +import jakarta.validation.constraints.NotNull; + +/** + * 开箱机会对象 tt_box_open_chance + * + * @author ruoyi + * @date 2024-07-09 + */ +@Data +public class TtBoxOpenChance extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 道具ID */ + @NotNull(message = "道具ID不能为空") + private Long ornamentId; + + /** 宝箱ID */ + @NotNull(message = "宝箱ID不能为空") + private Integer boxId; + + /** 开启次数 */ + @NotNull(message = "开启次数不能为空") + private Integer chanceNum; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxOrnaments.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxOrnaments.java new file mode 100644 index 0000000..e5b702d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxOrnaments.java @@ -0,0 +1,100 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_box_ornaments") +public class TtBoxOrnaments implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "宝箱ID") + private Integer boxId; + + @Excel(name = "饰品ID") + @TableField("ornament_id") + private Long ornamentId; + + @Excel(name = "市场唯一hash名称") + @TableField("market_hash_name") + private String marketHashName; + + @Excel(name = "zbt ID") + @TableField("ornaments_zbt_id") + private String ornamentsZbtId; + + @Excel(name = "yy ID") + @TableField("ornaments_yy_id") + private String ornamentsYyId; + + @Excel(name = "饰品级别") + private Integer level; + + @Excel(name = "显示数量") + private Integer odds; + + @Excel(name = "显示爆率百分比") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String oddsPercentum; + + @Excel(name = "真实数量") + private Integer realOdds; + + @Excel(name = "主播数量") + private Integer anchorOdds; + + @Excel(name = "机器人数量") + private Integer robotOdds; + + @Excel(name = "机器人数量(欧皇)") + private Integer robotOddsLucky; + + @Excel(name = "机器人数量 (非酋)") + private Integer robotOddsUnlucky; + + @Excel(name = "白号数量") + private Integer whiteOdds; + + private Integer compOdds; + + private Integer realCompOdds; + + @Excel(name = "三级爆率") + private Integer thirdExplosiveOdds; + + @Excel(name = "封面图片") + private String coverImg; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private String remark; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxRecordsBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxRecordsBody.java new file mode 100644 index 0000000..3f597db --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxRecordsBody.java @@ -0,0 +1,17 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class TtBoxRecordsBody { + + private Long id; + private Integer userId; + private Integer holderUserId; + private Long ornamentsId; + private String itemName; + private String levelImg; + private String status; + private Integer boxId; + private Integer source; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxType.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxType.java new file mode 100644 index 0000000..9fe92b7 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxType.java @@ -0,0 +1,51 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_box_type") +public class TtBoxType implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + private String name; + + private String icon; + + private Integer sort; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @Excel(name = "是否对战分类") + private String isFightType; + + /** + * 是否前端显示:0-显示 1-隐藏 + */ + @Excel(name = "是否显示") + private String isShow; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxUser.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxUser.java new file mode 100644 index 0000000..4a484af --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxUser.java @@ -0,0 +1,28 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TtBoxUser { + + private Long id; + private Integer boxId; + private Integer userId; + private String userName; + private String nickName; + private String avatar; + private Long ornamentsId; + private String itemName; + private String shortName; + private BigDecimal usePrice; + private String imageUrl; + private String exteriorName; + private Integer ornamentsLevelId; + private String levelImg; + + // 来源 + private String source; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxUserIdPrice.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxUserIdPrice.java new file mode 100644 index 0000000..4b956da --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxUserIdPrice.java @@ -0,0 +1,19 @@ +package com.ruoyi.domain.other; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtBoxUserIdPrice { + private Integer userId; + private BigDecimal totalPrice; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxVO.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxVO.java new file mode 100644 index 0000000..ee60e02 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtBoxVO.java @@ -0,0 +1,23 @@ +package com.ruoyi.domain.other; + +import com.ruoyi.domain.vo.TtOrnamentVO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtBoxVO { + private Integer boxTypeId; + private String boxTypeName; + private String icon; + private Integer sort; + private List boxList; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtCompRecord.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtCompRecord.java new file mode 100644 index 0000000..702c8a3 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtCompRecord.java @@ -0,0 +1,13 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class TtCompRecord { + + private Integer userId; + + private Integer boxId; + + private Integer compAmount; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtContent.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtContent.java new file mode 100644 index 0000000..6686598 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtContent.java @@ -0,0 +1,54 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_content") +public class TtContent implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "文章类别ID") + private Integer typeId; + + @Excel(name = "文章标题") + private String title; + + @Excel(name = "文章内容") + private String content; + + @Excel(name = "文章绑定链接") + private String link; + + @Excel(name = "浏览量") + private Integer pageView; + + @Excel(name = "状态") + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtContentType.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtContentType.java new file mode 100644 index 0000000..9aa1420 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtContentType.java @@ -0,0 +1,48 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_content_type") +public class TtContentType implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "类别名称") + private String name; + + @Excel(name = "类别别名") + private String alias; + + @Excel(name = "备注") + private String remark; + + @Excel(name = "状态") + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtDeliveryApplyBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtDeliveryApplyBody.java new file mode 100644 index 0000000..102d0ce --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtDeliveryApplyBody.java @@ -0,0 +1,14 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class TtDeliveryApplyBody { + + private String userName; + + private String phoneNumber; + + private String outTradeNo; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtDeliveryRecordBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtDeliveryRecordBody.java new file mode 100644 index 0000000..792d9a8 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtDeliveryRecordBody.java @@ -0,0 +1,30 @@ +package com.ruoyi.domain.other; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +@Data +public class TtDeliveryRecordBody { + + private String userName; + + private String phoneNumber; + + private String outTradeNo; + + private String orderId; + + private Integer userId; + + private String status; + + private String itemName; + + private String levelImg; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private String startTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private String endTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtFightBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtFightBody.java new file mode 100644 index 0000000..73667d9 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtFightBody.java @@ -0,0 +1,11 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class TtFightBody { + + private Integer id; + private String status; + private Integer userId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtFightRankingReward.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtFightRankingReward.java new file mode 100644 index 0000000..6d1c31d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtFightRankingReward.java @@ -0,0 +1,27 @@ +package com.ruoyi.domain.other; + +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 对战奖励金额对象 tt_fight_ranking_reward + * + * @author ruoyi + * @date 2024-07-04 + */ +@Data +public class TtFightRankingReward extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Integer id; + + /** 排名名称 */ + private String name; + + /** 奖励金额 */ + private BigDecimal reward; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtFightResult.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtFightResult.java new file mode 100644 index 0000000..81fac96 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtFightResult.java @@ -0,0 +1,38 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_fight_result") +public class TtFightResult implements Serializable { + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "对战房间ID") + private Integer fightId; + + @Excel(name = "对战结果") + private String fightResult; + + @Excel(name = "结果时间") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtFightUser.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtFightUser.java new file mode 100644 index 0000000..c33fbe5 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtFightUser.java @@ -0,0 +1,54 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_fight_user") +public class TtFightUser implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "对战ID") + private Integer fightId; + + @Excel(name = "参与者_用户ID") + private Integer userId; + + @Excel(name = "加入金额") + private BigDecimal joinPrice; + + @Excel(name = "加入位置编号") + private Integer joinSeatNum; + + // 0,准备 1,进行中 2,结束 + @Excel(name = "对战状态") + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtFirstRecharge.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtFirstRecharge.java new file mode 100644 index 0000000..915f41f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtFirstRecharge.java @@ -0,0 +1,84 @@ +package com.ruoyi.domain.other; + +import java.math.BigDecimal; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 首充赠送对象 tt_first_recharge + * + * @author ruoyi + * @date 2024-06-21 + */ +public class TtFirstRecharge extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Integer id; + + /** 充值金额下限 */ + @Excel(name = "充值金额下限") + private BigDecimal minAmount; + + /** 赠送比例 */ + @Excel(name = "赠送比例") + private BigDecimal ratio; + + /** 规则描述 */ + @Excel(name = "规则描述") + private String description; + + public void setId(Integer id) + { + this.id = id; + } + + public Integer getId() + { + return id; + } + public void setMinAmount(BigDecimal minAmount) + { + this.minAmount = minAmount; + } + + public BigDecimal getMinAmount() + { + return minAmount; + } + public void setRatio(BigDecimal ratio) + { + this.ratio = ratio; + } + + public BigDecimal getRatio() + { + return ratio; + } + public void setDescription(String description) + { + this.description = description; + } + + public String getDescription() + { + return description; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("minAmount", getMinAmount()) + .append("ratio", getRatio()) + .append("description", getDescription()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtMessage.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtMessage.java new file mode 100644 index 0000000..48c488a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtMessage.java @@ -0,0 +1,39 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_message") +public class TtMessage implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "消息内容") + private String message; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtMessageSend.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtMessageSend.java new file mode 100644 index 0000000..fc9138a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtMessageSend.java @@ -0,0 +1,48 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_message_send") +public class TtMessageSend implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "接收者用户ID") + private Integer recId; + + @Excel(name = "消息内容ID") + private Integer messageId; + + /** + * 1:已读 + */ + @Excel(name = "查看状态") + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date sendTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date readingTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtNotice.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtNotice.java new file mode 100644 index 0000000..a096ab3 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtNotice.java @@ -0,0 +1,41 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +/** + * 通知实体类 + */ +@Data +@TableName("tt_notice") +public class TtNotice { + + @TableId(value = "notice_id", type = IdType.AUTO) + private Integer noticeId; + + private Long userId; + + private String title; + + private String content; + + /** + * "1": 已读 + */ + @TableField("`read`") + private String read; + + /** + * 消息分类:GAME_xxx 或 SYS_xxx + */ + private String category; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsA.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsA.java new file mode 100644 index 0000000..e2563c8 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsA.java @@ -0,0 +1,28 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TtOrnamentsA { + + // private Long id; + // // 平台 + // private Integer partyType; + // 在该平台的id + private Long ornamentId; + private String name; + private String shortName; + private BigDecimal usePrice; + private String imageUrl; + private String exteriorName; + private Integer ornamentsLevelId; + private String levelImg; + private Integer odds; + private BigDecimal oddsResult; + private Integer compOdds; + private BigDecimal compOddsResult; + private Integer openOdds; + private BigDecimal openOddsResult; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsBody.java new file mode 100644 index 0000000..2cb91e8 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsBody.java @@ -0,0 +1,54 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TtOrnamentsBody { + + private Long id; + + /** + * 名称 + */ + private String name; + + /** + * 饰品名称(前端搜索参数) + */ + private String itemName; + + /** + * 类型 + */ + private String type; + + private String typeName; + + /** + * 外观 + */ + private String exterior; + + /** + * 最高价格 + */ + private BigDecimal maxPrice; + + /** + * 最低价格 + */ + private BigDecimal minPrice; + + private Integer pageNum; + + private Integer pageSize; + + private Integer zbtId; + + private Integer yyyoupingId; + + /** 只返回有价格(use_price > 0)的饰品,用于批量填货弹窗过滤无价饰品 */ + private Boolean filterNoPrice; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsLevel.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsLevel.java new file mode 100644 index 0000000..7f91877 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsLevel.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_ornaments_level") +public class TtOrnamentsLevel implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + private String level; + + private String levelImg; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsPrice.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsPrice.java new file mode 100644 index 0000000..dfb9dd6 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtOrnamentsPrice.java @@ -0,0 +1,14 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TtOrnamentsPrice { + + private Long id; + + private BigDecimal price; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeCard.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeCard.java new file mode 100644 index 0000000..2cae75f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeCard.java @@ -0,0 +1,67 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_recharge_card") +public class TtRechargeCard implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "充值列表ID") + private Integer rechargeListId; + + @Excel(name = "金额") + private BigDecimal price; + + @Excel(name = "卡密") + private String password; + + @Excel(name = "状态") + private String status; + + @Excel(name = "使用者ID") + private Integer useUserId; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date useTime; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @TableField(select = false) + @Excel(name = "删除标志") + private String delFlag; + + /** 是否空白卡密: 0=否, 1=是(空白卡密不计入充值记录) */ + @Excel(name = "空白卡密") + private Integer isBlank; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRankingReward.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRankingReward.java new file mode 100644 index 0000000..fc32c6e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRankingReward.java @@ -0,0 +1,28 @@ +package com.ruoyi.domain.other; + +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 充值奖励金额对象 tt_recharge_ranking_reward + * + * @author ruoyi + * @date 2024-07-04 + */ +@Data +public class TtRechargeRankingReward extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Integer id; + + /** 排名名称 */ + private String name; + + /** 奖励金额 */ + private BigDecimal reward; + private String rewardStr; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRecord.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRecord.java new file mode 100644 index 0000000..95bd64a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRecord.java @@ -0,0 +1,72 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_recharge_record") +public class TtRechargeRecord implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "充值记录ID") + @TableId + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "上级用户ID") + private Integer parentId; + + @Excel(name = "到账金额") + private BigDecimal arrivalAmount; + + @Excel(name = "实际支付金额") + private BigDecimal amountActuallyPaid; + + @Excel(name = "最终金额") + private BigDecimal finallyPrice; + + @Excel(name = "订单号") + private String orderId; + + @Excel(name = "外部订单号") + private String outTradeNo; + + private String status; + + private String channelType; + + @Excel(name = "卡商ID") + private Integer cardMerchantId; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @TableField(select = false) + private String delFlag; + + /** 是否空白卡密产生的充值记录: 0=否, 1=是(空白卡密不计入充值记录展示) */ + private Integer isBlankCard; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRecordBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRecordBody.java new file mode 100644 index 0000000..17c747b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRecordBody.java @@ -0,0 +1,26 @@ +package com.ruoyi.domain.other; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +@Data +public class TtRechargeRecordBody { + + private Integer userId; + + private Integer parentId; + // 招商ID:充值用户的上级上级(直属上级[主播]的上级[招商]) + private Integer merchantId; + private Integer cardMerchantId; + private String orderId; + private String outTradeNo; + private String channelType; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRecordVo.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRecordVo.java new file mode 100644 index 0000000..bf8ca57 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRechargeRecordVo.java @@ -0,0 +1,73 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtRechargeRecordVo implements Serializable { + + @Excel(name = "充值记录ID") + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "上级用户ID") + private Integer parentId; + + @Excel(name = "招商ID") + private Integer merchantId; + + @Excel(name = "到账金额") + private BigDecimal arrivalAmount; + + @Excel(name = "实际支付金额") + private BigDecimal amountActuallyPaid; + + @Excel(name = "最终金额") + private BigDecimal finallyPrice; + + @Excel(name = "订单号") + private String orderId; + + @Excel(name = "外部订单号") + private String outTradeNo; + + private String status; + + private String channelType; + + @Excel(name = "卡商ID") + private Integer cardMerchantId; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @TableField(select = false) + private String delFlag; + + private String nickName; + + private String phoneNumber; + + private String deliveryAddress; + + private String picture; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPack.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPack.java new file mode 100644 index 0000000..069ccf2 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPack.java @@ -0,0 +1,110 @@ +package com.ruoyi.domain.other; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 【请填写功能名称】对象 tt_red_pack + * + * @author ruoyi + * @date 2023-06-29 + */ +@Setter +@Getter +public class TtRedPack extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Long id; + + /** + * 标题 + */ + @Excel(name = "标题") + private String title; + + /** + * 红包类型 1.充值红包 2.普通红包 + */ + @Excel(name = "红包类型 1.充值红包 2.普通红包") + private String packType; + + /** + * 充值金额 + */ + @Excel(name = "充值金额") + private BigDecimal rechargeLimit; + + /** + * 总数量 0,无限制 + */ + @Excel(name = "总数量 0,无限制") + private Long packCount; + + /** + * 剩余数量 + */ + @Excel(name = "剩余数量") + private Long validCount; + + /** + * 红包最小值 + */ + @Excel(name = "红包最小值") + private BigDecimal valueMin; + + /** + * 红包最大值 + */ + @Excel(name = "红包最大值") + private BigDecimal valueMax; + + /** + * 开始时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date beginTime; + + /** + * 结束时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + /** + * 状态0 已结束 1 进行中 + */ + @Excel(name = "状态0 已结束 1 进行中") + private Long status; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("title", getTitle()) + .append("packType", getPackType()) + .append("rechargeLimit", getRechargeLimit()) + .append("packCount", getPackCount()) + .append("validCount", getValidCount()) + .append("valueMin", getValueMin()) + .append("valueMax", getValueMax()) + .append("beginTime", getBeginTime()) + .append("endTime", getEndTime()) + .append("status", getStatus()) + .append("createTime", getCreateTime()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacket.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacket.java new file mode 100644 index 0000000..d9ee2e2 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacket.java @@ -0,0 +1,90 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_red_packet") +public class TtRedPacket implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "红包标题") + private String title; + + @Excel(name = "红包描述") + private String description; + + @Excel(name = "发放个数") + private Integer num; + + @Excel(name = "单个红包金额区间") + private String amount; + + @Excel(name = "用户ID") + private Integer userId; + + /** + * 指定领取人用户ID。 + *

+ * NULL = 不限用户(平台红包、主播红包的原有行为,谁拿到口令谁能领); + * 非 NULL = 只有该用户本人能领(亏损返利CDK 用这个字段绑定收款人)。 + *

+ * 不要拿 {@link #userId} 兼职这件事:它的语义是「主播ID」,规则是 + * 「只有主播下级能领、且主播本人不能领」,跟"指定收款人"正好相反。 + */ + @TableField("receiver_user_id") + private Integer receiverUserId; + + @Excel(name = "红包口令") + private String password; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date openingTime; + + // 有效期限 + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date validity; + + // 0,正在使用 1,结束 + private Integer status; + + @Excel(name = "创建者") + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + //0启用 1禁用 + @TableField("use_status") + private Integer useStatus; + + @TableField(select = false) + @TableLogic(value = "0",delval = "1") + private Integer delFlag; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacketBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacketBody.java new file mode 100644 index 0000000..abef70b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacketBody.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.other; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtRedPacketBody { + + private Integer id; + + private String title; + + private String status; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacketRecord.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacketRecord.java new file mode 100644 index 0000000..89f7539 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacketRecord.java @@ -0,0 +1,46 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_red_packet_record") +public class TtRedPacketRecord implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "红包ID") + private Integer redPacketId; + + @Excel(name = "领取人ID") + private Integer userId; + + @Excel(name = "领取口令") + private String receivePassword; + + @Excel(name = "领取金额") + private BigDecimal receiveAmount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date receiveTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacketRecordBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacketRecordBody.java new file mode 100644 index 0000000..2490fce --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRedPacketRecordBody.java @@ -0,0 +1,19 @@ +package com.ruoyi.domain.other; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtRedPacketRecordBody { + + private Integer redPacketId; + private Integer userId; + private String receivePassword; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtReplacementRecord.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtReplacementRecord.java new file mode 100644 index 0000000..1b6038e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtReplacementRecord.java @@ -0,0 +1,153 @@ +package com.ruoyi.domain.other; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 汰换记录对象 tt_replacement_record + * + * @author junhai + * @date 2023-09-10 + */ +public class TtReplacementRecord extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** id */ + private Long id; + + /** 用户id */ + @Excel(name = "用户id") + private Long uid; + + /** 用户名称 */ + @Excel(name = "用户名称") + private String uname; + + /** 饰品ids */ + @Excel(name = "饰品ids") + private String oids; + + /** 合成饰品的id */ + @Excel(name = "合成饰品的id") + private Long awardOid; + + /** 合成饰品的名称 */ + @Excel(name = "合成饰品的名称") + private String awardOname; + + /** 合成饰品的价格 */ + @Excel(name = "合成饰品的价格") + private BigDecimal awardOprice; + + /** + * 饰品的图片 + */ + @Excel(name = "饰品的图片") + private String awardOimg; + + /** 合成日期 */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "合成日期", width = 30, dateFormat = "yyyy-MM-dd") + private Date time; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setUid(Long uid) + { + this.uid = uid; + } + + public Long getUid() + { + return uid; + } + public void setUname(String uname) + { + this.uname = uname; + } + + public String getUname() + { + return uname; + } + public void setOids(String oids) + { + this.oids = oids; + } + + public String getOids() + { + return oids; + } + public void setAwardOid(Long awardOid) + { + this.awardOid = awardOid; + } + + public Long getAwardOid() + { + return awardOid; + } + public void setAwardOname(String awardOname) + { + this.awardOname = awardOname; + } + + public String getAwardOname() + { + return awardOname; + } + public void setAwardOprice(BigDecimal awardOprice) + { + this.awardOprice = awardOprice; + } + + public BigDecimal getAwardOprice() + { + return awardOprice; + } + public void setTime(Date time) + { + this.time = time; + } + + public Date getTime() + { + return time; + } + + public String getAwardOimg() { + return awardOimg; + } + + public void setAwardOimg(String awardOimg) { + this.awardOimg = awardOimg; + } + + @Override + public String toString() { + return "TtReplacementRecord{" + + "id=" + id + + ", uid=" + uid + + ", uname='" + uname + '\'' + + ", oids='" + oids + '\'' + + ", awardOid=" + awardOid + + ", awardOname='" + awardOname + '\'' + + ", awardOprice=" + awardOprice + + ", awardOimg='" + awardOimg + '\'' + + ", time=" + time + + '}'; + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightGroup.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightGroup.java new file mode 100644 index 0000000..9200d0e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightGroup.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.other; + +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; + +/** + * 机器人对战分组对象 tt_robot_fight_group + * + * @author ruoyi + * @date 2024-06-28 + */ +@Data +public class TtRobotFightGroup extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 分组ID */ + private Integer groupId; + + /** 分组名称 */ + private String groupName; + + /** 座位数量 */ + private Integer seatNum; + + /** 对战模式(0欧皇 1非酋) */ + private String model; + + /** 价格档位配置ID */ + private Integer priceConfigId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightGroupBox.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightGroupBox.java new file mode 100644 index 0000000..dc1e27d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightGroupBox.java @@ -0,0 +1,35 @@ +package com.ruoyi.domain.other; + +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +/** + * 机器人对战分组宝箱对象 tt_robot_fight_group_box + * + * @author ruoyi + * @date 2024-06-28 + */ +@Data +public class TtRobotFightGroupBox extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 分组ID */ + @NotNull(message = "分组ID不能为空") + private Integer groupId; + + /** 宝箱ID */ + @NotNull(message = "宝箱ID不能为空") + private Integer boxId; + + /** 修改前的宝箱ID(用于UPDATE定位旧记录) */ + private transient Integer oldBoxId; + + /** 宝箱数量 */ + private Integer boxNum; + + /** 宝箱名称(关联查询,非持久化字段) */ + private transient String boxName; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightJoinConfig.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightJoinConfig.java new file mode 100644 index 0000000..b7fe774 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightJoinConfig.java @@ -0,0 +1,41 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 机器人对战进房时间配置对象 tt_robot_fight_join_config + * + * @author ruoyi + * @date 2026-07-23 + */ +@Data +@TableName("tt_robot_fight_join_config") +public class TtRobotFightJoinConfig extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @TableId(type = IdType.AUTO) + private Integer id; + + /** 档位最小金额(不含) */ + private BigDecimal minAmount; + + /** 档位最大金额(含) */ + private BigDecimal maxAmount; + + /** 机器人进房基础秒数 */ + private Integer joinSeconds; + + /** 机器人进房随机偏移秒数(在基础秒数上额外随机增加 1~randomSeconds 秒) */ + private Integer randomSeconds; + + /** 状态(0启用 1停用) */ + private String status; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightPriceConfig.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightPriceConfig.java new file mode 100644 index 0000000..42d8d29 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtRobotFightPriceConfig.java @@ -0,0 +1,38 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 机器人对战价格档位配置对象 tt_robot_fight_price_config + * + * @author ruoyi + * @date 2026-08-13 + */ +@Data +@TableName("tt_robot_fight_price_config") +public class TtRobotFightPriceConfig extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @TableId(type = IdType.AUTO) + private Integer id; + + /** 价格上限(房间宝箱总金额 <= price_limit 命中该档位) */ + private BigDecimal priceLimit; + + /** 该档位目标房间数量 */ + private Integer targetCount; + + /** 排序序号(越小越靠前,金额档位建议按价格上限升序) */ + private Integer sortOrder; + + /** 状态(0启用 1停用) */ + private String status; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtTaskCenter.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtTaskCenter.java new file mode 100644 index 0000000..13ae38f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtTaskCenter.java @@ -0,0 +1,34 @@ +package com.ruoyi.domain.other; + +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 任务中心对象 tt_task_center + * + * @author ruoyi + * @date 2024-05-25 + */ +@Data +public class TtTaskCenter extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 任务ID */ + private Integer taskId; + + /** 任务名称 */ + private String taskName; + + /** 任务描述 */ + private String description; + + /** 任务标识 */ + private String identify; + + /** 任务状态(0停用 1启用) */ + private String status; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUidTotalRechargeRecord.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUidTotalRechargeRecord.java new file mode 100644 index 0000000..fbd705e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUidTotalRechargeRecord.java @@ -0,0 +1,19 @@ +package com.ruoyi.domain.other; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtUidTotalRechargeRecord { + private Integer userId; + private BigDecimal totalPrice; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeFailOrnaments.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeFailOrnaments.java new file mode 100644 index 0000000..15400e9 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeFailOrnaments.java @@ -0,0 +1,56 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_upgrade_fail_ornaments") +public class TtUpgradeFailOrnaments implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "升级失败奖励列表ID") + @TableId + private Integer id; + + @Excel(name = "升级饰品列表ID") + private Integer upgradeId; + + @Excel(name = "失败奖励饰品ID") + private Long ornamentId; + + @Excel(name = "失败奖励饰品名称") + private String ornamentName; + + @Excel(name = "失败奖励饰品数量") + private Integer ornamentNumber; + + @Excel(name = "饰品级别ID") + private Integer ornamentLevelId; + + @Excel(name = "饰品价格") + private BigDecimal ornamentPrice; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeOrnaments.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeOrnaments.java new file mode 100644 index 0000000..a6d0e08 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeOrnaments.java @@ -0,0 +1,106 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_upgrade_ornaments") +public class TtUpgradeOrnaments implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "升级饰品ID") + @TableId + private Integer id; + + @Excel(name = "饰品ID") + private Long ornamentsId; + + @Excel(name = "饰品原价") + @TableField("ornament_price") + private BigDecimal ornamentPrice; + + @Excel(name = "饰品级别ID") + private Integer ornamentsLevelId; + + @Excel(name = "通用幸运区间") + private String luckSection; + + @Excel(name = "本轮出货所需金额") + private BigDecimal amountRequired; + + @Excel(name = "本轮已投金额") + private BigDecimal amountInvested; + + /** 真实计算口径下的本轮已投金额(溢出会继承到下一轮) */ + @TableField("effective_amount_invested") + private BigDecimal effectiveAmountInvested; + + @Excel(name = "主播幸运区间") + private String anchorLuckSection; + + @Excel(name = "本轮出货所需金额") + private BigDecimal anchorAmountRequired; + + @Excel(name = "本轮已投金额") + private BigDecimal anchorAmountInvested; + + /** 主播真实计算口径下的本轮已投金额(溢出会继承到下一轮) */ + @TableField("anchor_effective_amount_invested") + private BigDecimal anchorEffectiveAmountInvested; + + /** 普通用户累计原始概率(幸运值),出货成功清 0 重置 */ + @TableField("probability_invested") + private Integer probabilityInvested; + + /** 主播累计原始概率(幸运值),出货成功清 0 重置 */ + @TableField("anchor_probability_invested") + private Integer anchorProbabilityInvested; + + /** 白号累计原始概率(幸运值),出货成功清 0 重置(白号之间共享这一个池) */ + @TableField("blank_probability_invested") + private Integer blankProbabilityInvested; + + /** 白号幸运区间 [min,max),为空时回退通用 luck_section */ + @TableField("blank_luck_section") + private String blankLuckSection; + + /** 白号本轮已投金额(实际扣款口径),出货成功清 0 */ + @TableField("blank_amount_invested") + private BigDecimal blankAmountInvested; + + /** 白号本轮已投金额(真实计算口径),出货成功清 0 */ + @TableField("blank_effective_amount_invested") + private BigDecimal blankEffectiveAmountInvested; + + // 总投入 + @TableField("total_input") + private BigDecimal totalInput; + + // 0可用 1禁用 + @Excel(name = "状态") + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeOrnamentsBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeOrnamentsBody.java new file mode 100644 index 0000000..92d513c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeOrnamentsBody.java @@ -0,0 +1,13 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class TtUpgradeOrnamentsBody { + + private String itemName; + + private String type; + + private String status; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeRecord.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeRecord.java new file mode 100644 index 0000000..487a0e3 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeRecord.java @@ -0,0 +1,101 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonRawValue; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.domain.common.constant.UserType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_upgrade_record") +public class TtUpgradeRecord implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "升级记录ID") + @TableId + private Long id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "昵称") + private String nickName; + + @Excel(name = "用户类型") + private String userType; + + @Excel(name = "消耗金额") + private BigDecimal amountConsumed; + + @Excel(name = "可升级饰品ID") + @TableField("target_upgrade_id") + private Integer targetUpgradeId; + + @Excel(name = "目标饰品ID") + @TableField("target_ornament_id") + private Long targetOrnamentId; + + @Excel(name = "目标饰品价格") + @TableField("target_ornament_price") + private BigDecimal targetOrnamentPrice; + + @Excel(name = "概率") + @TableField("probability") + private Integer probability; + + @Excel(name = "最终奖励饰品集合") + @TableField("gain_ornament_list") + @JsonRawValue + private String gainOrnamentList; + + @Excel(name = "最终奖励总价") + @TableField("gain_ornaments_price") + private BigDecimal gainOrnamentsPrice; + + @Excel(name = "成败") + @TableField("is_victory") + private Boolean isVictory; + + /** 本轮目标金额快照(区分用户类型:普通用户取 amount_required,主播取 anchor_amount_required) */ + @TableField("amount_required") + private BigDecimal amountRequired; + + /** 本轮已投金额快照(区分用户类型:普通用户取 amount_invested,主播取 anchor_amount_invested) */ + @TableField("amount_invested") + private BigDecimal amountInvested; + + /** 真实计算口径下的本轮已投金额快照(区分用户类型:普通用户取 effective_amount_invested,主播取 anchor_effective_amount_invested) */ + @TableField("effective_amount_invested") + private BigDecimal effectiveAmountInvested; + + /** 真实计算金额:超过 75% 按 75% 计算,小于等于 75% 按实际概率计算 */ + @TableField("effective_amount") + private BigDecimal effectiveAmount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date openTime; + + @TableField(exist = false) + private String userAvatar; + + /** 本次升级幸运值(累计原始概率,出货成功清 0;按用户类型区分)快照,持久化用于历史记录展示 */ + @TableField("lucky_value") + private Integer luckyValue; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeRecordBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeRecordBody.java new file mode 100644 index 0000000..84ca23d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUpgradeRecordBody.java @@ -0,0 +1,11 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class TtUpgradeRecordBody { + + private Integer userId; + + private String targetItemName; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUserAmountRecordsBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserAmountRecordsBody.java new file mode 100644 index 0000000..7393af2 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserAmountRecordsBody.java @@ -0,0 +1,41 @@ +package com.ruoyi.domain.other; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.Date; + +@Data +public class TtUserAmountRecordsBody { + + private Integer userId; + + private String nickName; + + private String phoneNumber; + + // 收入1 支出0 + private Integer type; + + private Integer source; + + // 1金币 2积分 + private Integer moneyType; + + @NotNull(message = "页码不能为空") + @Min(value = 1, message = "最小值1") + private Integer page; + + @NotNull(message = "分页长度不能为空") + @Min(value = 1, message = "最小值1") + private Integer size; + private Integer limit; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUserAvatar.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserAvatar.java new file mode 100644 index 0000000..81aca48 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserAvatar.java @@ -0,0 +1,40 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_user_avatar") +public class TtUserAvatar implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "用户头像") + private String avatar; + + @Excel(name = "是否默认", readConverterExp = "0=否,1=是") + private String isDefault; + + @Excel(name = "头像排序") + private Integer sort; + + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUserBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserBody.java new file mode 100644 index 0000000..0c89c5f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserBody.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.other; + +import lombok.Data; + +@Data +public class TtUserBody { + + private Integer userId; + + private String userName; + + private String nickName; + + private String userType; + + private String phoneNumber; + + private String status; + + private String startDate; + + private String endDate; + + private String invitationCode; + + private Integer parentId; + + private Integer isBlankAccount; + + private String delFlag; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUserCreditsRecordsBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserCreditsRecordsBody.java new file mode 100644 index 0000000..8fc3159 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserCreditsRecordsBody.java @@ -0,0 +1,25 @@ +package com.ruoyi.domain.other; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class TtUserCreditsRecordsBody { + + private Integer userId; + + private String nickName; + + private String phoneNumber; + + private Integer type; + + private String source; + + private Integer page; + + private Integer size; + + private Integer limit; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtUserPackSackBody.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserPackSackBody.java new file mode 100644 index 0000000..0e18a08 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtUserPackSackBody.java @@ -0,0 +1,22 @@ +package com.ruoyi.domain.other; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TtUserPackSackBody { + + private Integer userId; + private Integer holderUserId; + private String phoneNumber; + /** 状态筛选:0/2 按「展示状态」语义过滤(见 TtUserMapper.getPackSack),其余按原始 status */ + private Integer status; + private String source; + private Integer fightId; + private Integer rollId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtVipLevel.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtVipLevel.java new file mode 100644 index 0000000..72cadb4 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtVipLevel.java @@ -0,0 +1,64 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_vip_level") +public class TtVipLevel implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "VIP等级名称") + private String name; + + @Excel(name = "VIP等级图标") + private String icon; + + @Excel(name = "VIP等级达标金额") + private BigDecimal rechargeThreshold; + + @Excel(name = "充值加送比例") + private BigDecimal commissions; + + @Excel(name = "等级达标奖励红包金额") + private BigDecimal addedBonus; + + @Excel(name = "VIP流水返利百分比") + private BigDecimal flowRebatePercent; + + @Excel(name = "每日转赠次数") + private Integer transferCount; + + @Excel(name = "每日提货次数") + private Integer deliveryCount; + + @Excel(name = "描述") + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtWelfare.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtWelfare.java new file mode 100644 index 0000000..5d0ef89 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtWelfare.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.other; + +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; + +/** + * 福利列表对象 tt_welfare + * + * @author ruoyi + * @date 2024-05-11 + */ +@Data +public class TtWelfare extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 福利ID */ + private Integer welfareId; + + /** 福利名称 */ + private String welfareName; + + /** 类型 */ + private String type; + + /** VIP等级 */ + private Integer vipLevel; + + /** 箱子ID */ + private Integer boxId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtWelfareMonthlyRecharges.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtWelfareMonthlyRecharges.java new file mode 100644 index 0000000..d0bfc1c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtWelfareMonthlyRecharges.java @@ -0,0 +1,36 @@ +package com.ruoyi.domain.other; + +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 每月充值福利对象 tt_welfare_monthly_recharges + * + * @author ruoyi + * @date 2024-06-24 + */ +@Data +public class TtWelfareMonthlyRecharges extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 福利ID */ + private Integer welfareId; + + /** 福利名称 */ + private String welfareName; + + /** 福利类型 */ + private String type; + + /** VIP等级 */ + private Integer vipLevel; + + /** 充值金额 */ + private BigDecimal amount; + + /** 福利宝箱ID */ + private Long boxId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/TtYYOrnaments.java b/skins-model/src/main/java/com/ruoyi/domain/other/TtYYOrnaments.java new file mode 100644 index 0000000..ea4dcd8 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/TtYYOrnaments.java @@ -0,0 +1,53 @@ +package com.ruoyi.domain.other; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.sql.Timestamp; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "tt_yy_ornaments") +public class TtYYOrnaments { + + @TableId(value = "id",type = IdType.AUTO) + private String id; + + @TableField("name") + private String name; + + @TableField("hash_name") + private String hashName; + + @TableField("type_id") + private Integer typeId; + + @TableField("type_name") + private String typeName; + + @TableField("type_hash_name") + private String typeHashName; + + @TableField("weapon_id") + private Integer weaponId; + + @TableField("weapon_name") + private String weaponName; + + @TableField("weapon_hash_name") + private String weaponHashName; + + @TableField("update_time") + private Timestamp updateTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/other/UpgradeBodyA.java b/skins-model/src/main/java/com/ruoyi/domain/other/UpgradeBodyA.java new file mode 100644 index 0000000..61e29b7 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/other/UpgradeBodyA.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.other; + +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; + +@Data +public class UpgradeBodyA { + + // 饰品id + //@NotNull(message = "饰品id不能为空") + // private Long ornamentId; + + // 幸运升级饰品id + @NotNull(message = "幸运升级饰品id不能为空") + private Integer upgradeId; + + // 消费金额由后端根据 (升级饰品价格 × 概率 / 100) 计算得出,不再由前端传入, + // 防止前端传入 0 或异常价格导致 100% 成功率等风险。 +// @NotNull(message = "消费金额不能为空") +// private BigDecimal price; + + // 概率,增量 + @NotNull(message = "概率不能为空") + @Range(min = 1, max = 100) + private Integer probability; + + // private Integer id; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/param/RedPackRecordParam.java b/skins-model/src/main/java/com/ruoyi/domain/param/RedPackRecordParam.java new file mode 100644 index 0000000..ee80e6e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/param/RedPackRecordParam.java @@ -0,0 +1,87 @@ +package com.ruoyi.domain.param; + +public class RedPackRecordParam { + + private Integer uId; + /** 用户名 */ + private String uname; + + /** 关联道具id */ + private String email; + + /** 饰品封面 */ + private String phone; + /** 饰品封面 */ + private String key; + + /** 饰品封面 */ + private String name; + + private String origin; + + private Long rId; + + public Integer getuId() { + return uId; + } + + public void setuId(Integer uId) { + this.uId = uId; + } + + public String getUname() { + return uname; + } + + public void setUname(String uname) { + this.uname = uname; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public Long getrId() { + return rId; + } + + public void setrId(Long rId) { + this.rId = rId; + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/param/promotion/AnchorDailyTurnoverParam.java b/skins-model/src/main/java/com/ruoyi/domain/param/promotion/AnchorDailyTurnoverParam.java new file mode 100644 index 0000000..055edc3 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/param/promotion/AnchorDailyTurnoverParam.java @@ -0,0 +1,32 @@ +package com.ruoyi.domain.param.promotion; + +import lombok.Data; + +import java.util.List; + +/** + * 主播日流水 + */ +@Data +public class AnchorDailyTurnoverParam { + + /** + * 玩家上级ID + */ + private Integer parentId; + + /** + * 玩家上级昵称 + */ + private String parentNickName; + + private String beginTime; + + private String endTime; + + private Integer num; + + private Integer size; + + private List userIds; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/param/promotion/AnchorRechargeParam.java b/skins-model/src/main/java/com/ruoyi/domain/param/promotion/AnchorRechargeParam.java new file mode 100644 index 0000000..09d3b2d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/param/promotion/AnchorRechargeParam.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.param.promotion; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 主播日流水 + */ +@Data +public class AnchorRechargeParam { + + /** + * 主播ID + */ + private Integer parentId; + /** + * 充值金额 + */ + private BigDecimal recharge; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/DTO/pWelfareMQData.java b/skins-model/src/main/java/com/ruoyi/domain/task/DTO/pWelfareMQData.java new file mode 100644 index 0000000..6b4631b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/DTO/pWelfareMQData.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.task.DTO; + +import com.ruoyi.domain.entity.sys.TtUser; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class pWelfareMQData implements Serializable { + + // 消费者 + private Integer userId; + private TtUser user; + + // 金额 + private BigDecimal account; + + // 消费类型 + private Integer accountType; + + private Timestamp createTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/TtTask.java b/skins-model/src/main/java/com/ruoyi/domain/task/TtTask.java new file mode 100644 index 0000000..f569b5c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/TtTask.java @@ -0,0 +1,61 @@ +package com.ruoyi.domain.task; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +@TableName("tt_task") +public class TtTask { + + @TableId(value = "id",type = IdType.AUTO) + private Integer id; + + // 描述 + @TableField("task_describe") + private String taskDescribe; + + @TableField("name") + private String name; + + // 任务状态 + @TableField("state") + private Integer state; + + // 任务类型 + @TableField("type") + private Integer type; + + // 目标值类型 + @TableField("target_type") + private Integer targetType; + + // 目标值 + @TableField("target_value") + private Integer targetValue; + + // 奖励类型 + @TableField("award_type") + private Integer awardType; + + // 奖励数量 + @TableField("award_value") + private Integer awardValue; + + // 触发条件 + @TableField("task_condition") + private String taskCondition; + + // 海报地址 + @TableField("placard_url") + private String placardUrl; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/TtTaskDoing.java b/skins-model/src/main/java/com/ruoyi/domain/task/TtTaskDoing.java new file mode 100644 index 0000000..b56839b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/TtTaskDoing.java @@ -0,0 +1,48 @@ +package com.ruoyi.domain.task; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +@TableName("tt_task_doing") +public class TtTaskDoing { + + @TableId(value = "id",type = IdType.AUTO) + private Integer id; + + // 任务id + @TableField("task_id") + private Integer taskId; + + // 玩家id + @TableField("user_id") + private Integer userId; + + // 任务完成状态 + @TableField("completion_state") + private Integer completionState; + + // 任务进度值 + @TableField("progress") + private Integer progress; + + // 开始时间 + @TableField("begin_time") + private Timestamp beginTime; + + // 完成时间 + @TableField("compete_time") + private Timestamp competeTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/VO/TtTaskDoingVO.java b/skins-model/src/main/java/com/ruoyi/domain/task/VO/TtTaskDoingVO.java new file mode 100644 index 0000000..a3c1b19 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/VO/TtTaskDoingVO.java @@ -0,0 +1,79 @@ +package com.ruoyi.domain.task.VO; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class TtTaskDoingVO { + + // 任务id + // @TableField("task_id") + private Integer taskId; + + // 任务实例id + // @TableField("task_id") + private Integer taskDoingId; + + // 玩家id + // @TableField("user_id") + private Integer userId; + + // 描述 + // @TableField("describe") + private String taskDescribe; + + // @TableField("name") + private String name; + + // 任务类型 + // @TableField("type") + private Integer type; + + // 目标值类型 + // @TableField("target_type") + private Integer targetType; + + // 目标值 + // @TableField("target_value") + private Integer targetValue; + + // 奖励类型 + // @TableField("award_type") + private Integer awardType; + + // 奖励数量 + // @TableField("award_value") + private Integer awardValue; + + // 任务完成状态 + // @TableField("completion_state") + private Integer completionState; + + // 开始时间 + // @TableField("begin_time") + private Timestamp beginTime; + + // 完成时间 + // @TableField("compete_time") + private Timestamp competeTime; + + // 任务进度值 + // @TableField("progress") + private Integer progress; + + // 海报地址 + // @TableField("placard_url") + private String placardUrl; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskAwardType.java b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskAwardType.java new file mode 100644 index 0000000..69a8683 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskAwardType.java @@ -0,0 +1,22 @@ +package com.ruoyi.domain.task.constant; + +// 任务奖励类型 +public enum TaskAwardType { + + // 积分 + CREDITS(1), + + // 其他 + OTHER(2); + + private Integer code; + + TaskAwardType(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskCompletionState.java b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskCompletionState.java new file mode 100644 index 0000000..314a075 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskCompletionState.java @@ -0,0 +1,19 @@ +package com.ruoyi.domain.task.constant; + +public enum TaskCompletionState { + + COMPLETION(1), + DOING(2), + TIME_OUT(3), + COMPLETION_PRIZE(4); + + private Integer code; + + TaskCompletionState(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskState.java b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskState.java new file mode 100644 index 0000000..a25200a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskState.java @@ -0,0 +1,18 @@ +package com.ruoyi.domain.task.constant; + +public enum TaskState { + + UP(1), + DOWN(2); + + private Integer code; + + TaskState(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskTargetType.java b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskTargetType.java new file mode 100644 index 0000000..87bd9b8 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskTargetType.java @@ -0,0 +1,18 @@ +package com.ruoyi.domain.task.constant; + +public enum TaskTargetType { + + CREDITS(1), + DOWNLOAD(2); + + private Integer code; + + TaskTargetType(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskType.java b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskType.java new file mode 100644 index 0000000..0ab468f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/constant/TaskType.java @@ -0,0 +1,20 @@ +package com.ruoyi.domain.task.constant; + +public enum TaskType { + + COMMON(1), + DAY(2), + WEEK(3), + MONTH(4); + + private Integer code; + + TaskType(Integer code){ + this.code = code; + } + + public Integer getCode(){ + return this.code; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/task/constant/mq/MQMoudle.java b/skins-model/src/main/java/com/ruoyi/domain/task/constant/mq/MQMoudle.java new file mode 100644 index 0000000..22aa0e2 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/task/constant/mq/MQMoudle.java @@ -0,0 +1,19 @@ +package com.ruoyi.domain.task.constant.mq; + +public enum MQMoudle { + + PROMOTION_WELFARE_QUEUE("pWelfareQueue"), + PROMOTION_WELFARE_EXCHANGE("pWelfareExchange"), + PROMOTION_WELFARE_KEY1("pwKey1"); + + private String value; + + MQMoudle(String value){ + this.value = value; + } + + public String getValue(){ + return this.value; + } + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ApiContentDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiContentDataVO.java new file mode 100644 index 0000000..5668f6d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiContentDataVO.java @@ -0,0 +1,27 @@ +package com.ruoyi.domain.vo; + +import com.ruoyi.domain.other.TtContent; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class ApiContentDataVO { + + private Integer contentTypeId; + + private String contentTypeName; + + private String contentAlias; + + private List contentList; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ApiFightListDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiFightListDataVO.java new file mode 100644 index 0000000..45a1108 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiFightListDataVO.java @@ -0,0 +1,87 @@ +package com.ruoyi.domain.vo; + +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.fasterxml.jackson.annotation.JsonRawValue; +import com.ruoyi.domain.entity.fight.FightSeat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.commons.lang3.StringUtils; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class ApiFightListDataVO implements Serializable { + + private Integer id; + + private String model; + + /** 组队模式(0或null=个人 1=2v2组队) */ + private Integer teamMode; + + private List seats; + + private Integer roundNumber; + + private Integer playerNum; + + @JsonRawValue + private String boxData; + + private BigDecimal boxPriceTotal; + + private String status; + + @JsonRawValue + private String userData; + + private String createTime; + + private String updateTime; + + + + private List winnerIds; + + private String winnerIdList; + /** + * 开箱饰品总金额 + */ + private BigDecimal totalOrnamentsPrice; + + /** + * 赢家饰品总金额 + */ + private BigDecimal winnerTotalPrice; + + /** + * 失败方安慰奖总金额 + */ + private BigDecimal loserTotalPrice; + + private Integer userId; + + /** + * 房主昵称(不入库,动态填充) + */ + private String ownerNickName; + + public List getWinnerIds() { + if (StringUtils.isBlank(winnerIdList)) { + return new ArrayList<>(); + } + return JSONUtil.toList(winnerIdList, Integer.class); + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ApiLuckyOrnamentsDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiLuckyOrnamentsDataVO.java new file mode 100644 index 0000000..e9d65c7 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiLuckyOrnamentsDataVO.java @@ -0,0 +1,35 @@ +package com.ruoyi.domain.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ApiLuckyOrnamentsDataVO { + + private Integer id; + + private Long ornamentId; + + private String itemName; + + /** 饰品全名(tt_ornament.name),前端展示优先使用 */ + private String name; + + private BigDecimal usePrice; + + private String imageUrl; + + private String shortName; + + private String typeName; + + private String rarityName; + + private String exteriorName; + + private String levelImg; + + /** 本饰品当前幸运值(累计原始概率,出货成功清 0;按当前查看用户类型返回) */ + private Integer luckyValue; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ApiLuckyUpgradeRecentPullVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiLuckyUpgradeRecentPullVO.java new file mode 100644 index 0000000..1af3855 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiLuckyUpgradeRecentPullVO.java @@ -0,0 +1,47 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class ApiLuckyUpgradeRecentPullVO { + + /** 用户头像 */ + private String avatar; + + /** 用户昵称 */ + private String nickName; + + /** 用户VIP等级 */ + private Integer vipLevel; + + /** 本次拉取的概率(前端原始概率,如 90) */ + private Integer probability; + + /** 本次拉取时的幸运值快照(累计原始概率;中奖为 0) */ + private Integer luckyValue; + + /** 成败:true=升级成功,false=失败(前端胜负样式用) */ + private Boolean isVictory; + + /** 饰品全名 */ + private String name; + + /** 饰品图片 */ + private String imageUrl; + + /** 饰品价格 */ + private BigDecimal usePrice; + + /** 磨损度(外观名称) */ + private String exteriorName; + + /** 品质图片 */ + private String levelImg; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date openTime; +} \ No newline at end of file diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ApiLuckyUpgradeRecordDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiLuckyUpgradeRecordDataVO.java new file mode 100644 index 0000000..245eb3e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiLuckyUpgradeRecordDataVO.java @@ -0,0 +1,33 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class ApiLuckyUpgradeRecordDataVO { + + private String nickName; + + private String avatar; + + private String probability; + + private BigDecimal amountConsumed; + + private String gainItemName; + + // 获得饰品价值 + private BigDecimal gainOrnamentsPrice; + + private String imageUrl; + + private Boolean isVictory; + + // private String result; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date openTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ApiMessageDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiMessageDataVO.java new file mode 100644 index 0000000..1dfe5b2 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiMessageDataVO.java @@ -0,0 +1,22 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +@Data +public class ApiMessageDataVO { + + private Integer id; + + private String message; + + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date sendTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date readingTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ApiShoppingDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiShoppingDataVO.java new file mode 100644 index 0000000..797f57e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiShoppingDataVO.java @@ -0,0 +1,36 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotEmpty; +import java.math.BigDecimal; + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ApiShoppingDataVO { + + private Long id; + private String itemName; + private String name; // 新增字段,与 TtOrnament.name 对应 + private BigDecimal usePrice; + private BigDecimal storePrice; + private BigDecimal creditsPrice; + private String imageUrl; + private String itemId; + private String shortName; + private String type; + private String typeName; + private String quality; + private String qualityName; + private String rarity; + private String rarityName; + private String rarityColor; + private String exterior; + private String exteriorName; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ApiUpgradeRecordItemVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiUpgradeRecordItemVO.java new file mode 100644 index 0000000..a467286 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ApiUpgradeRecordItemVO.java @@ -0,0 +1,63 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 幸运升级记录列表项(全服/我的 × 当前饰品/全部饰品 通用) + */ +@Data +public class ApiUpgradeRecordItemVO { + + /** 用户ID(前端标记「我的」/头像点击) */ + private Integer userId; + + /** 用户头像 */ + private String avatar; + + /** 用户昵称(写库快照) */ + private String nickName; + + /** 用户VIP等级 */ + private Integer vipLevel; + + /** 成功率(前端原始概率,如 89) */ + private Integer probability; + + /** 幸运值(累计原始概率快照,如 899) */ + private Integer luckyValue; + + /** 消耗金额 */ + private BigDecimal amountConsumed; + + /** 获得饰品总价 */ + private BigDecimal gainOrnamentsPrice; + + /** 成败(true=升级成功) */ + private Boolean isVictory; + + /** 升级时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date openTime; + + /** 目标饰品ID */ + private Long targetOrnamentId; + + /** 饰品全名 */ + private String name; + + /** 饰品图片 */ + private String imageUrl; + + /** 饰品价格 */ + private BigDecimal usePrice; + + /** 磨损度(外观名称) */ + private String exteriorName; + + /** 品质图片 */ + private String levelImg; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/AvailableMarketOrnamentVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/AvailableMarketOrnamentVO.java new file mode 100644 index 0000000..001d96c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/AvailableMarketOrnamentVO.java @@ -0,0 +1,31 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class AvailableMarketOrnamentVO { + + // 平台 "yy" "zbt" + private Integer platform; + + // yy的 + private Integer templateId; + // zbt的 + private String itemId; + + private Integer delivery; + private String itemName; + private String imageUrl; + private BigDecimal cnyPrice; + private BigDecimal price; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/BoxPoolStatisticsVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/BoxPoolStatisticsVO.java new file mode 100644 index 0000000..9f4d772 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/BoxPoolStatisticsVO.java @@ -0,0 +1,62 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 宝箱统计(按奖池/开箱人类型拆分) + * 奖池编号见 BoxPoolType:01 主播池 / 02 真实池 / 05 机器人池 / 06 机器人欧皇池 / 07 机器人非酋池 / 08 白号池 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class BoxPoolStatisticsVO { + + /** + * 奖池编号 + */ + private String poolType; + + /** + * 奖池名称(tab 标题) + */ + private String poolName; + + /** + * 本轮剩余饰品数量 + */ + private Integer remaining; + + /** + * 本轮饰品总量(该奖池爆率总和) + */ + private Integer total; + + /** + * 本轮剩余/总量,前端直接展示 + */ + private String remainingNum; + + /** + * 今日开箱次数 + */ + private Integer todayOpenNum; + + /** + * 今日爆出饰品总价值 + */ + private BigDecimal todayArisePriceTotal; + + /** + * 今日利润(门票收入 - 爆出价值) + */ + private BigDecimal todayProfit; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ChildDetailByBossVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ChildDetailByBossVO.java new file mode 100644 index 0000000..911eb3f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ChildDetailByBossVO.java @@ -0,0 +1,36 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class ChildDetailByBossVO { + + // 粉丝id + private Integer userId; + + // 粉丝昵称 + private String nickName; + + // 消费金币 + private BigDecimal amountTotal; + + // 消费积分 + private BigDecimal creditsToTal; + + // 消费合计 + private BigDecimal total; + + // boss收益 + private BigDecimal earnings; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/CommonOrnamentVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/CommonOrnamentVO.java new file mode 100644 index 0000000..3c6e062 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/CommonOrnamentVO.java @@ -0,0 +1,124 @@ +package com.ruoyi.domain.vo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; + +/* +通用饰品信息vo + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class CommonOrnamentVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + private Integer yyId; + + // 是表里的itemId(ZBT官网饰品id) + private Integer zbtId; + + // 也是zbt的长名称 + @Excel(name = "名称") + @TableField("name") + private String name; + + @Excel(name = "饰品唯一名称英文") + private String marketHashName; + + @Excel(name = "类型") + private Integer type; + + @Excel(name = "type_name") + @TableField("type_name") + private String typeName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @TableField("update_time") + private Timestamp updateTime; + + @Excel(name = "本网站使用价格") + private BigDecimal usePrice; + + @Excel(name = "图片") + private String imageUrl; + + @Excel(name = "在售数量") + private Integer quantity; + + + + + // yy + @Excel(name = "type_hash_name") + @TableField("type_hash_name") + private String typeHashName; + + @Excel(name = "weapon_id") + @TableField("weapon_id") + private Integer weaponId; + + @Excel(name = "weapon_name") + @TableField("weapon_name") + private String weaponName; + + @Excel(name = "weapon_hash_name") + @TableField("weapon_hash_name") + private String weaponHashName; + + @Excel(name = "在售最低价") + private BigDecimal price; + + @Excel(name = "短名称") + private String shortName; + + @Excel(name = "品质") + private String quality; + + @Excel(name = "品质名称") + private String qualityName; + + @Excel(name = "品质颜色") + private String qualityColor; + + @Excel(name = "稀有度") + private String rarity; + + @Excel(name = "稀有度名称") + private String rarityName; + + @Excel(name = "稀有度颜色") + private String rarityColor; + + @Excel(name = "外观") + private String exterior; + + @Excel(name = "外观名称") + private String exteriorName; + + private String remark; + + private String isPutaway; + + private String isProprietaryProperty; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/DeliveryApplyVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/DeliveryApplyVO.java new file mode 100644 index 0000000..a8c5df4 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/DeliveryApplyVO.java @@ -0,0 +1,54 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +// 提货申请记录信息 +@Data +public class DeliveryApplyVO { + + private Integer id; + + private Integer userId; + + private String outTradeNo; + + /** + * 批次号,同一申请的多饰品共用 + */ + private String batchId; + + /** + * 当日剩余提货次数(delivery_total_count - delivery_used_count - delivery_pending_count) + */ + private Integer remainingCount; + + private Long ornamentId; + + private String ornamentName; + + private Integer boxRecordsId; + + private String nickName; + + /** 用户类型: 01=主播, 02=普通用户, 03=机器人, 04=招商, 05=卡商 */ + private String userType; + + // private Integer partyType; + + // 饰品hash + private String hashName; + + private BigDecimal ornamentsPrice; + + /** CS340三方实时价格(tt_ornament.use_price) */ + private BigDecimal cs340Price; + + private String name; + private String imageUrl; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/FightBoxDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/FightBoxDataVO.java new file mode 100644 index 0000000..ee0d1fa --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/FightBoxDataVO.java @@ -0,0 +1,24 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class FightBoxDataVO { + + private Integer boxId; + private String boxImg01; + private String boxImg02; + private String boxName; + private BigDecimal price; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/FightResultDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/FightResultDataVO.java new file mode 100644 index 0000000..8c6315d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/FightResultDataVO.java @@ -0,0 +1,17 @@ +package com.ruoyi.domain.vo; + +import com.ruoyi.domain.entity.TtOrnament; +import lombok.Data; + +import java.util.List; + +@Data +public class FightResultDataVO { + + private Integer fightId; + private Integer round; + private List playerGainsOrnamentsData; + private List winnerGainsOrnamentsData; + private List TtOrnament; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/FightRoomDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/FightRoomDataVO.java new file mode 100644 index 0000000..7e4ba33 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/FightRoomDataVO.java @@ -0,0 +1,21 @@ +package com.ruoyi.domain.vo; + +import com.ruoyi.domain.other.BoxDataBodyA; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Data +public class FightRoomDataVO { + + List boxDataList; + private Integer fightId; + private String fightStatus; + private Integer boxNumTotal; + private BigDecimal boxPriceTotal; + private Integer userId; + private String userName; + private String nickName; + private String avatar; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/JackpotOrnamentsDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/JackpotOrnamentsDataVO.java new file mode 100644 index 0000000..e09de85 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/JackpotOrnamentsDataVO.java @@ -0,0 +1,25 @@ +package com.ruoyi.domain.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class JackpotOrnamentsDataVO { + //产品名称 + private String itemName; + //成色 + private String exteriorName; + //图片地址 + private String imageUrl; + //等级 + private String level; + //等级图片 + private String levelImg; + //商品名称 + private String shortName; + //类型名称 + private String typeName; + //价格 + private BigDecimal usePrice; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/JoinFightUserDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/JoinFightUserDataVO.java new file mode 100644 index 0000000..811c680 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/JoinFightUserDataVO.java @@ -0,0 +1,15 @@ +package com.ruoyi.domain.vo; + +import lombok.Data; + +@Data +public class JoinFightUserDataVO { + + private Integer fightId; + private Integer userId; + private Integer joinSeatNum; + private String userName; + private String nickName; + private String avatar; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/OnSaleOrnamentVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/OnSaleOrnamentVO.java new file mode 100644 index 0000000..5b0659d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/OnSaleOrnamentVO.java @@ -0,0 +1,25 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class OnSaleOrnamentVO { + + private Integer partyType; + private String id; + private Integer delivery; + private String itemName; + private String imageUrl; + private BigDecimal cnyPrice; + private BigDecimal price; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/OpenBoxVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/OpenBoxVO.java new file mode 100644 index 0000000..a773470 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/OpenBoxVO.java @@ -0,0 +1,111 @@ +package com.ruoyi.domain.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class OpenBoxVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Long id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "宝箱ID") + private Integer boxId; + + @Excel(name = "宝箱名称") + private String boxName; + + @Excel(name = "宝箱价格") + private BigDecimal boxPrice; + + @Excel(name = "饰品ID") + @TableField("ornament_id") + private Long ornamentId; + + @Excel(name = "市场唯一hash名称") + @TableField("market_hash_name") + private String marketHashName; + + @Excel(name = "zbt ID") + @TableField("ornaments_zbt_id") + private String ornamentsZbtId; + + @Excel(name = "yy ID") + @TableField("ornaments_yy_id") + private String ornamentsYyId; + + @Excel(name = "饰品名称") + private String ornamenName; + + @Excel(name = "饰品价格") + private BigDecimal ornamentsPrice; + + @Excel(name = "外观名称") + private String exteriorName; + + @Excel(name = "短名称") + private String shortName; + + @Excel(name = "本平台使用价格") + private BigDecimal usePrice; + + @Excel(name = "饰品图片") + private String imageUrl; + + @Excel(name = "饰品级别ID") + private Integer ornamentsLevelId; + + private String levelImg; + + // 0、在背包显示 1、已分解 + @Excel(name = "状态") + private Integer status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + // 0、对战 + @Excel(name = "饰品来源") + private Integer source; + + @Excel(name = "对战ID") + private Integer fightId; + + @Excel(name = "对战模式:回合数") + private Integer fightRoundNumber; + + @Excel(name = "Roll房ID") + private Integer rollId; + + @Excel(name = "持有者_用户ID") + private Integer holderUserId; + + // 用于对战模式,游戏完全结束才显示 + @TableField("is_show") + private Integer isShow; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/PlayerGainsOrnamentsDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/PlayerGainsOrnamentsDataVO.java new file mode 100644 index 0000000..fc836c3 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/PlayerGainsOrnamentsDataVO.java @@ -0,0 +1,19 @@ +package com.ruoyi.domain.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class PlayerGainsOrnamentsDataVO { + + List ornamentsDataList; + private UserPackSackDataVO ornamentsData; + private Integer userId; + private String userName; + private String nickName; + private String avatar; + private Integer joinSeatNum; + + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/PromotionDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/PromotionDataVO.java new file mode 100644 index 0000000..cc7036d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/PromotionDataVO.java @@ -0,0 +1,20 @@ +package com.ruoyi.domain.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class PromotionDataVO { + + private BigDecimal rechargeTotal; + private BigDecimal rebateTotal; + private BigDecimal unbalancedPrice; + private BigDecimal balancedPrice; + private BigDecimal todayRechargeTotal; + private BigDecimal todayRebateTotal; + private BigDecimal thisWeekRechargeTotal; + private BigDecimal thisWeekRebateTotal; + private BigDecimal thisMonthRechargeTotal; + private BigDecimal thisMonthRebateTotal; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/RollDetailsDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/RollDetailsDataVO.java new file mode 100644 index 0000000..caa66dd --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/RollDetailsDataVO.java @@ -0,0 +1,60 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.domain.entity.roll.TtRollUser; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.domain.vo.roll.SimpleRollOrnamentVO; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Data +public class RollDetailsDataVO { + + private String rollName; + + private Boolean hasPW; + // private String pw; + + private String description; + + // 房间类型 + private String rollType; + + private String rollTypeName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date endTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; + + private Integer currentPeopleNum; + + private Integer peopleNum; + + private Integer ornamentsNum; + + private BigDecimal totalOrnamentsPrice; + + // 状态 + private String rollStatus; + + //充值门槛 + private BigDecimal minRecharge; + + // 当前登录用户是否已参与 + private Boolean joined; + + // 参与人员列表 + // private List playerList; + + // 奖池 + private List jackpotOrnamentsDataList; + + // 获奖名单 + // private List openPrizeList; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/RollListDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/RollListDataVO.java new file mode 100644 index 0000000..2e0ce50 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/RollListDataVO.java @@ -0,0 +1,89 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.domain.vo.roll.SimpleRollOrnamentVO; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Data +public class RollListDataVO { + + /** + * ROLL房ID + */ + private Integer id; + + /** + * ROLL房密码 + */ + private String rollPassword; + + /** + * 是否有密码 + */ + private Boolean hasPW; + + /** + * ROLL房类型 + */ + private String rollType; + + /** + * ROLL房类型名称 + */ + private String rollTypeName; + + private String nickName; + + private String avatar; + + private String rollName; + + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date endTime; + + private Integer currentPeopleNum; + + private Integer peopleNum; + + /** + * ROLL房状态 + */ + private String rollStatus; + + /** + * 奖池物品实例数量 + */ + private Integer ornamentsNum; + + /** + * 奖池饰品列表(对象数组,便于前端直接渲染) + */ + private List jackpotOrnamentsDataList; + + private BigDecimal totalOrnamentsPrice; + + /** + * 门槛 + */ + private BigDecimal minRecharge; + + /** + * VIP等级门槛(直接返回数字:0、2、13) + */ + private Integer vipLevel; + + /** + * 充值起始时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date rechargeStartTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/ShoppingDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/ShoppingDataVO.java new file mode 100644 index 0000000..094b680 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/ShoppingDataVO.java @@ -0,0 +1,46 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class ShoppingDataVO { + private Long id; + private String itemName; + private String name; // 新增字段,与 TtOrnament.name 对应 + private BigDecimal usePrice; + private BigDecimal useCredits; + private String imageUrl; + private String marketHashName; + private String itemId; + private BigDecimal price; + private BigDecimal storePrice; + private String shortName; + private String type; + private String typeName; + private String quality; + private String qualityName; + private String qualityColor; + private String rarity; + private String rarityName; + private String rarityColor; + private String exterior; + private String exteriorName; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + private String remark; + private String isPutaway; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/SimpleRollUserVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/SimpleRollUserVO.java new file mode 100644 index 0000000..d3cc0af --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/SimpleRollUserVO.java @@ -0,0 +1,52 @@ +package com.ruoyi.domain.vo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class SimpleRollUserVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "用户名") + private String userName; + + @Excel(name = "昵称") + private String nickName; + + @Excel(name = "头像") + private String avatar; + + @Excel(name = "Roll房ID") + private Integer rollId; + + @Excel(name = "宝箱记录ID") + private Long boxRecordId; + + @JsonFormat(pattern = "yyyy-MM-dd") + private Date joinTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/SimpleUserVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/SimpleUserVO.java new file mode 100644 index 0000000..0be7ebd --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/SimpleUserVO.java @@ -0,0 +1,22 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +public class SimpleUserVO { + + private Integer userId; + private Integer rollUserId; + private String nickName; + private String userName; + private Integer userType; + private Integer phoneNumber; + private Integer parentId; + private String avatar; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TeamDetailVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TeamDetailVO.java new file mode 100644 index 0000000..7a10281 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TeamDetailVO.java @@ -0,0 +1,16 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class TeamDetailVO { + private Integer employeeId; + private String employeeNickName; + private Timestamp beginTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtBoxDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtBoxDataVO.java new file mode 100644 index 0000000..0e347b7 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtBoxDataVO.java @@ -0,0 +1,48 @@ +package com.ruoyi.domain.vo; + +import lombok.Data; + +import java.math.BigDecimal; +import com.ruoyi.common.annotation.Excel; +@Data +public class TtBoxDataVO { + + @Excel(name = "宝箱ID") + private Integer boxId; + @Excel(name = "宝箱名称") + private String boxName; + @Excel(name = "宝箱所属分类ID") + private String boxTypeId; + @Excel(name = "宝箱所属分类") + private String boxTypeName; + @Excel(name = "价格") + private BigDecimal price; + private String boxImg01; + private String boxImg02; + @Excel(name = "宝箱顺序") + private Integer sort; + @Excel(name = "是否对战宝箱") + private String isFight; + @Excel(name = "宝箱类型") + private String boxType; + private Integer compAmount; + private String status; + private Long openNum; + private String isHome; + + // 宝箱完全开启所需金额 + @Excel(name = "每轮花费金额") + private BigDecimal amountConsumed; + + // 宝箱内饰品总价值 + @Excel(name = "饰品总价值") + private BigDecimal aggregateAmount; + + // 利润 系统的利润 + @Excel(name = "每轮利润") + private BigDecimal profit; + + // 利润率 系统的利润率 + @Excel(name = "每轮利润率") + private String profitMargin = "100%"; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtBoxOrnamentsDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtBoxOrnamentsDataVO.java new file mode 100644 index 0000000..f5cb975 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtBoxOrnamentsDataVO.java @@ -0,0 +1,53 @@ +package com.ruoyi.domain.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TtBoxOrnamentsDataVO { + + private Integer id; + private Integer boxId; + private Long ornamentId; + + private String imageUrl; + private String itemName; + private String name; + private BigDecimal usePrice; + private BigDecimal price; + private Integer ornamentsLevelId; + + // 等级图片 背景 + private String levelImg; + + private String level; + private Integer odds; + private String oddsPercentum; + private Integer oddsRemaining; + private Integer realOdds; + private String realOddsPercentum; + private Integer realRemainingOdds; + private Integer anchorOdds; + private String anchorOddsPercentum; + private Integer anchorRemainingOdds; + private Integer robotOdds; + private String robotOddsPercentum; + private Integer robotRemainingOdds; + private Integer robotOddsLucky; + private String robotOddsLuckyPercentum; + private Integer robotLuckyRemainingOdds; + private Integer robotOddsUnlucky; + private String robotOddsUnluckyPercentum; + private Integer robotUnluckyRemainingOdds; + private Integer whiteOdds; + private String whiteOddsPercentum; + private Integer whiteRemainingOdds; + private Integer compOdds; + private Integer realCompOdds; + private Integer luckRemainingOdds; + private String remark; + private Integer thirdExplosiveOdds; + private String thirdExplosiveOddsPercentum; + private String coverImg; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtBoxRecordsDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtBoxRecordsDataVO.java new file mode 100644 index 0000000..fb8541b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtBoxRecordsDataVO.java @@ -0,0 +1,32 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class TtBoxRecordsDataVO { + + private Long boxRecordId; + private Integer userId; + /** 开启者昵称 */ + private String nickName; + private Integer holderUserId; + private Integer boxId; + private String boxName; + private BigDecimal boxPrice; + private Long ornamentsId; + private String ornamentName; + private String ornamentImgUrl; + private String ornamentLevelImg; + private BigDecimal ornamentsPrice; + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtDeliveryRecordDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtDeliveryRecordDataVO.java new file mode 100644 index 0000000..521c6ef --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtDeliveryRecordDataVO.java @@ -0,0 +1,26 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TtDeliveryRecordDataVO { + + private Integer id; + private Integer userId; + private String nickName; + private String itemName; + private String imageUrl; + private String outTradeNo; + private BigDecimal buyPrice; + private BigDecimal ornamentsPrice; + private String levelImg; + private Integer thirdpartyDelivery; + private String orderId; + private String status; + private String message; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private String updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtOrnamentVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtOrnamentVO.java new file mode 100644 index 0000000..4851433 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtOrnamentVO.java @@ -0,0 +1,117 @@ +package com.ruoyi.domain.vo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.Objects; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtOrnamentVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(value = "id",type = IdType.AUTO) + private Long id; + + @Excel(name = "zbt_id") + @TableField("zbt_id") + private Long zbtId; + + @Excel(name = "yyyouping_id") + @TableField("yyyouping_id") + private Long yyyoupingId; + + @Excel(name = "长名称") + @TableField("name") + private String name; + + @Excel(name = "本网站使用价格") + private BigDecimal usePrice; + + @Excel(name = "图片") + private String imageUrl; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TtOrnamentVO that = (TtOrnamentVO) o; + return Objects.equals(marketHashName, that.marketHashName); + } + + @Override + public int hashCode() { + return Objects.hash(marketHashName); + } + + @Excel(name = "饰品唯一名称英文") + private String marketHashName; + + // @Excel(name = "在售最低价") + // private BigDecimal price; + + @Excel(name = "在售数量") + private Integer quantity; + + @Excel(name = "短名称") + private String shortName; + + @Excel(name = "类别") + private String type; + + @Excel(name = "类别中文名") + private String typeName; + + @Excel(name = "品质") + private String quality; + + @Excel(name = "品质名称") + private String qualityName; + + @Excel(name = "品质颜色") + private String qualityColor; + + @Excel(name = "稀有度") + private String rarity; + + @Excel(name = "稀有度名称") + private String rarityName; + + @Excel(name = "稀有度颜色") + private String rarityColor; + + @Excel(name = "外观") + private String exterior; + + @Excel(name = "外观名称") + private String exteriorName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private String remark; + + private String isPutaway; + + private String isProprietaryProperty; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtRedPacketDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtRedPacketDataVO.java new file mode 100644 index 0000000..8741f65 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtRedPacketDataVO.java @@ -0,0 +1,61 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtRedPacketDataVO { + + private Integer id; + + private String title; + + private String description; + + private Integer num; + + private String amount; + + private String remainingNum; + + private String amountRange; + + private Integer userId; + + /** 指定领取人用户ID:null=不限用户;非null=仅该用户可领(亏损返利CDK) */ + private Integer receiverUserId; + + private String password; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date openingTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date validity; + + private String status; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private String useStatus; + + private Integer createNum; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtRedPacketRecordDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtRedPacketRecordDataVO.java new file mode 100644 index 0000000..d891a57 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtRedPacketRecordDataVO.java @@ -0,0 +1,36 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtRedPacketRecordDataVO { + + private Integer id; + + private Integer redPacketId; + + private String title; + + private String receivePassword; + + private Integer userId; + + private String nickName; + + private BigDecimal receiveAmount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date receiveTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtRollJackpotOrnamentsDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtRollJackpotOrnamentsDataVO.java new file mode 100644 index 0000000..e5ff183 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtRollJackpotOrnamentsDataVO.java @@ -0,0 +1,39 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class TtRollJackpotOrnamentsDataVO { + + private Integer id; + + private Long ornamentsId; + + private Integer ornamentsNum; + + private String itemName; + + private String shortName; + + private String name; + + private BigDecimal usePrice; + + private String imageUrl; + + private String ornamentLevelId; + + private String level; + + private String levelImg; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtRollPrizeDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtRollPrizeDataVO.java new file mode 100644 index 0000000..7b5a322 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtRollPrizeDataVO.java @@ -0,0 +1,59 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtRollPrizeDataVO { + + // roll玩家id + @NotNull(message = "必须指定获奖人。") + private Integer rollUserId; + + /** + * 获奖用户ID + */ + private Integer userId; + + // 奖池物品id + @NotNull(message = "必须指定奖品id。") + private Integer rollJackpotOrnamentId; + + // 数量 + @Min(value = 1,message = "数量最少为1") + private Integer ornamentNum; + + // roll房id + private Integer rollId; + + /** + * 指定人 + */ + private String designatedBy; + + /** + * 获奖用户昵称 + */ + private String nickName; + private String imageUrl; + private Integer jackpotOrnamentsListId; + private Long ornamentsId; + private Integer ornamentsLevelId; + private String itemName; + private String shortName; + private BigDecimal usePrice; + private Integer rollUserPrizeId; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUpgradeFailOrnamentsDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUpgradeFailOrnamentsDataVO.java new file mode 100644 index 0000000..20d06e5 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUpgradeFailOrnamentsDataVO.java @@ -0,0 +1,40 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class TtUpgradeFailOrnamentsDataVO { + + private Integer id; + + private Integer upgradeId; + + private Long ornamentId; + + private BigDecimal usePrice; + + private String itemName; + + private Integer ornamentNumber; + + private String imageUrl; + + private String type; + + private String typeName; + + private Integer ornamentLevelId; + + private String level; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUpgradeOrnamentsDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUpgradeOrnamentsDataVO.java new file mode 100644 index 0000000..aa04bf6 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUpgradeOrnamentsDataVO.java @@ -0,0 +1,59 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class TtUpgradeOrnamentsDataVO { + + private Integer id; + + private Long ornamentsId; + + private BigDecimal usePrice; + + private String itemName; + + private String imageUrl; + + private String type; + + private String typeName; + + private Integer ornamentsLevelId; + + private String level; + + private String luckSection; + + private BigDecimal amountRequired; + + private BigDecimal amountInvested; + + private String anchorLuckSection; + + /** 白号幸运区间,为空时回退普通 luckSection */ + private String blankLuckSection; + + /** 白号本轮已投金额 */ + private BigDecimal blankAmountInvested; + + /** 白号当前幸运值(运行时累计,仅供后台展示,编辑时不回写) */ + private Integer blankProbabilityInvested; + + private BigDecimal anchorAmountRequired; + + private BigDecimal anchorAmountInvested; + + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAccountRecordsRankVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAccountRecordsRankVO.java new file mode 100644 index 0000000..2e6b2cb --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAccountRecordsRankVO.java @@ -0,0 +1,46 @@ +package com.ruoyi.domain.vo; + +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +//@Accessors(chain = true) +@Builder +public class TtUserAccountRecordsRankVO { + +// @TableId +// private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + private String userName; + + private String nickName; + + private String avatar; + +// private String type; +// +// private String source; + + @Excel(name = "变动金额") + private BigDecimal account; + +// @Excel(name = "最终积分") +// private BigDecimal finalCredits; + +// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") +// private Date createTime; + + private String remark; + + private Integer accountRank; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/PWelfareVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/PWelfareVO.java new file mode 100644 index 0000000..c6450cf --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/PWelfareVO.java @@ -0,0 +1,35 @@ +package com.ruoyi.domain.vo.TtUserAmountRecords; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.recorde.TtUserAmountRecords; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class PWelfareVO implements Serializable { + + private static final long serialVersionUID = 1L; + + // 明细 + private Page details; + + // 今日预计收益 + private BigDecimal todayPredict; + + // 历史总收益 + private BigDecimal historyTotal; + + // 时间区间收益 + private BigDecimal timeTotal; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/PersonBlendErcashVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/PersonBlendErcashVO.java new file mode 100644 index 0000000..2f89b34 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/PersonBlendErcashVO.java @@ -0,0 +1,27 @@ +package com.ruoyi.domain.vo.TtUserAmountRecords; + +// 个人综合收支统计 + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PersonBlendErcashVO { + + private Integer userId; + + private String userName; + + // 金币 + private BigDecimal amount; + // 积分 + private BigDecimal credits; + // 合计 + private BigDecimal total; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/TtUserBlendErcashVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/TtUserBlendErcashVO.java new file mode 100644 index 0000000..714caaa --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/TtUserBlendErcashVO.java @@ -0,0 +1,79 @@ +package com.ruoyi.domain.vo.TtUserAmountRecords; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtUserBlendErcashVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "id") + @TableId(value = "id",type = IdType.AUTO) + private Long id; + + @Excel(name = "user_id") + @TableField("user_id") + private Integer userId; + + @Excel(name = "金币") + @TableField("amount") + private BigDecimal amount; + + @Excel(name = "final_amount") + @TableField("final_amount") + private BigDecimal finalAmount; + + @Excel(name = "积分") + @TableField("credits") + private BigDecimal credits; + + @Excel(name = "final_credits") + @TableField("final_credits") + private BigDecimal finalCredits; + + @Excel(name = "合计") + @TableField("total") + private BigDecimal total; + + @Excel(name = "来源") + @TableField("source") + private Integer source; + + @Excel(name = "收支类型") + @TableField("type") + private Integer type; + + @Excel(name = "create_time") + @TableField("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Timestamp createTime; + + @Excel(name = "update_time") + @TableField("update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Timestamp updateTime; + + @Excel(name = "笔记") + @TableField("remark") + private String remark; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/UserAmountDetailVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/UserAmountDetailVO.java new file mode 100644 index 0000000..c582abb --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserAmountRecords/UserAmountDetailVO.java @@ -0,0 +1,59 @@ +package com.ruoyi.domain.vo.TtUserAmountRecords; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class UserAmountDetailVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "用户ID") + private Integer userId; + + // 1、收入 2、支出 + private Integer type; + + // 1、金币 2积分 + private Integer moneyType; + + // 来源 + private Integer source; + + @Excel(name = "变动金额") + private BigDecimal amount; + + private BigDecimal credits; + + @Excel(name = "最终金额") + private BigDecimal finalAmount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String remark; + + private Integer pwChildId; + private String pwChildName; + private BigDecimal pwChildAccount; + + @TableField("task_id") + private Integer taskId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserCreditsRecordsRankVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserCreditsRecordsRankVO.java new file mode 100644 index 0000000..646277c --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserCreditsRecordsRankVO.java @@ -0,0 +1,53 @@ +package com.ruoyi.domain.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +//@Accessors(chain = true) +@Builder +public class TtUserCreditsRecordsRankVO { + +// @TableId +// private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + private String userName; + + private String nickName; + + private String avatar; + +// private String type; +// +// private String source; + + @Excel(name = "变动积分") + private BigDecimal credits; + +// @Excel(name = "最终积分") +// private BigDecimal finalCredits; + +// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") +// private Date createTime; + + private String remark; + + private Integer creditsRank; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserPackSackDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserPackSackDataVO.java new file mode 100644 index 0000000..0e1d02d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserPackSackDataVO.java @@ -0,0 +1,58 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.Date; + +@Data +public class TtUserPackSackDataVO implements Comparator { + + private Long id; + + private Integer userId; + + private Integer holderUserId; + + private String nickName; + + private String avatar; + + private Long ornamentsId; + + private String ornamentName; + + private BigDecimal ornamentsPrice; + + private String imageUrl; + + private String status; + + /** + * 是否存在「未取消的提货记录」(tt_delivery_record.status != 11)。 + * status=2 有两种含义:1=用户已提交提货申请(后台有申请),0=只是待在提货背包(商城购买/提货被退回)。 + * 后台列表用它把「申请提货」与「在背包显示」区分开。 + */ + private Integer deliveryApply; + + private String source; + + private Integer fightId; + + private Integer rollId; + + private Integer priceRank; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @Override + public int compare(TtUserPackSackDataVO o1, TtUserPackSackDataVO o2) { + return o1.getOrnamentsPrice().compareTo(o2.getOrnamentsPrice()); + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserVipLevelVo.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserVipLevelVo.java new file mode 100644 index 0000000..639b170 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserVipLevelVo.java @@ -0,0 +1,28 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TtUserVipLevelVo { + private Integer userId; + /** + * vip等级 + */ + private Integer vipLevel; + /** + * 当前充值总金额 + */ + private BigDecimal rate; + /** + * 最高vip登录金额 + */ + private BigDecimal maxRate; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserVipProgressVo.java b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserVipProgressVo.java new file mode 100644 index 0000000..ce6c58f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/TtUserVipProgressVo.java @@ -0,0 +1,54 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * VIP等级进度(经验制) + *

+ * 经验规则:每充值1元=1经验;单笔充值不足1元按1点经验计算(向上取整)。 + * 经验仅供VIP等级成长使用,不能作为累计充值金额参考。 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TtUserVipProgressVo { + + /** + * 用户ID + */ + private Integer userId; + + /** + * 当前已激活的VIP等级(对应 tt_vip_level.id;0 表示尚未激活任何等级) + */ + private Integer vipLevel; + + /** + * 当前累计经验值 + */ + private Long currentExp; + + /** + * 下一级所需经验阈值(已是最高等级时返回最高等级阈值) + */ + private Long nextLevelExp; + + /** + * 距离下一级还需经验值(可为负,负值表示已超过阈值) + */ + private Long needExp; + + /** + * 是否已经是最高VIP等级 + */ + private Boolean maxLevel; + + /** + * 进度文本,形如 "100/30" + */ + private String progressText; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/UpgradeResultDataVOA.java b/skins-model/src/main/java/com/ruoyi/domain/vo/UpgradeResultDataVOA.java new file mode 100644 index 0000000..880986e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/UpgradeResultDataVOA.java @@ -0,0 +1,26 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class UpgradeResultDataVOA { + + private Long id; + + private BigDecimal ornamentsPrice; + private String itemName; + private String shortName; + private String imageUrl; + private String exteriorName; + private String levelImg; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/UserBERankVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/UserBERankVO.java new file mode 100644 index 0000000..82fa263 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/UserBERankVO.java @@ -0,0 +1,64 @@ +package com.ruoyi.domain.vo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class UserBERankVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "id") + @TableId(value = "id",type = IdType.AUTO) + private Long id; + + @Excel(name = "user_id") + @TableField("user_id") + private Integer userId; + + @TableField("nick_name") + private String nickName; + + // 头像 + private String avatar; + + @Excel(name = "amount") + @TableField("amount") + private BigDecimal amount; + + private String value; + + @Excel(name = "credits") + @TableField("credits") + private BigDecimal credits; + + @Excel(name = "total") + @TableField("total") + private BigDecimal total; + + @Excel(name = "create_time") + @TableField("create_time") + private Timestamp createTime; + + @TableField("be_rank") + private Integer beRank; + + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/UserPackSackDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/UserPackSackDataVO.java new file mode 100644 index 0000000..a7b9a48 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/UserPackSackDataVO.java @@ -0,0 +1,27 @@ +package com.ruoyi.domain.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class UserPackSackDataVO { + + private Long id; + + private Long ornamentId; + private BigDecimal ornamentsPrice; + /** 饰品完整名称(tt_ornament.name,回退 tt_box_records.ornament_name) */ + private String name; + private String itemName; + private String shortName; + private String imageUrl; + private String exteriorName; + + private Integer ornamentsLevelId; + private String levelImg; + + /** 饰品来源(6=商城兑换) */ + private Integer source; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/VipLevelCountVo.java b/skins-model/src/main/java/com/ruoyi/domain/vo/VipLevelCountVo.java new file mode 100644 index 0000000..498bcf6 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/VipLevelCountVo.java @@ -0,0 +1,35 @@ +package com.ruoyi.domain.vo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * VIP 等级次数配置(面向客户端展示) + * 仅包含每个 VIP 等级的每日转赠次数与每日提货次数限额 + */ +@Data +public class VipLevelCountVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * VIP 等级 ID + */ + private Integer id; + + /** + * VIP 等级名称 + */ + private String name; + + /** + * 每日转赠次数限额 + */ + private Integer transferCount; + + /** + * 每日提货次数限额 + */ + private Integer deliveryCount; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/WebsitePropertyDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/WebsitePropertyDataVO.java new file mode 100644 index 0000000..4976dfc --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/WebsitePropertyDataVO.java @@ -0,0 +1,40 @@ +package com.ruoyi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class WebsitePropertyDataVO { + + private Long id; + + private String name; + + private BigDecimal usePrice; + + private String imageUrl; + + private Integer quantity; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private String isPutaway; + + private String isProprietaryProperty; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/box/AdminBoxDetailVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/box/AdminBoxDetailVO.java new file mode 100644 index 0000000..cb793c7 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/box/AdminBoxDetailVO.java @@ -0,0 +1,32 @@ +package com.ruoyi.domain.vo.box; + +import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class AdminBoxDetailVO { + + // 统计信息 + // 宝箱完全开启所需金额 + private BigDecimal amountConsumed; + + // 宝箱内饰品总价值 + private BigDecimal aggregateAmount; + + // 利润 + private BigDecimal profit; + + // 利润率 + private BigDecimal profitMargin; + + // 宝箱内物品明细 + private List ornamentsDetail; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/box/BoxGlobalData.java b/skins-model/src/main/java/com/ruoyi/domain/vo/box/BoxGlobalData.java new file mode 100644 index 0000000..c542598 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/box/BoxGlobalData.java @@ -0,0 +1,43 @@ +package com.ruoyi.domain.vo.box; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class BoxGlobalData { + + private Integer boxId; + private String boxName; + + // 单价 + private BigDecimal price; + // private Integer openNumber; + + // 通用------------------------------------------------- + // 宝箱完全开启所需金额 + private BigDecimal commonAmountConsumed; + // 宝箱内饰品总价值 + private BigDecimal commonAggregateAmount; + // 利润 + private BigDecimal commonProfit; + // 利润率 + private BigDecimal commonProfitMargin; + + // 主播--------------------------------------------------- + // 宝箱完全开启所需金额 + private BigDecimal anchorAmountConsumed; + // 宝箱内饰品总价值 + private BigDecimal anchorAggregateAmount; + // 利润 + private BigDecimal anchorProfit; + // 利润率 + private BigDecimal anchorProfitMargin; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/box/TtBoxUserVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/box/TtBoxUserVO.java new file mode 100644 index 0000000..9bfe30a --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/box/TtBoxUserVO.java @@ -0,0 +1,12 @@ +package com.ruoyi.domain.vo.box; + +import com.ruoyi.domain.entity.sys.TtUser; +import lombok.Data; + +import java.util.List; + +@Data +public class TtBoxUserVO { + private Integer boxId; + private List users; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/boxRecords/TtBoxRecordsVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/boxRecords/TtBoxRecordsVO.java new file mode 100644 index 0000000..04b6a05 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/boxRecords/TtBoxRecordsVO.java @@ -0,0 +1,117 @@ +package com.ruoyi.domain.vo.boxRecords; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtBoxRecordsVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Long id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "持有者_用户ID") + private Integer holderUserId; + + // 持有者昵称 + private String holderUserNickName; + + // 持有者头像 + private String avatar; + + @Excel(name = "宝箱ID") + private Integer boxId; + + @Excel(name = "宝箱名称") + private String boxName; + + @Excel(name = "宝箱价格") + private BigDecimal boxPrice; + + @Excel(name = "宝箱图片") + private String boxImg; + + @Excel(name = "饰品ID") + @TableField("ornament_id") + private Long ornamentId; + + @Excel(name = "饰品名称") + private String ornamentName; + + @Excel(name = "饰品级别ID") + private Integer ornamentsLevelId; + + @Excel(name = "饰品级别图片") + private String ornamentLevelImg; + + @Excel(name = "市场唯一hash名称") + @TableField("market_hash_name") + private String marketHashName; + + @Excel(name = "zbt ID") + @TableField("ornaments_zbt_id") + private String ornamentsZbtId; + + @Excel(name = "yy ID") + @TableField("ornaments_yy_id") + private String ornamentsYyId; + + @Excel(name = "饰品价格") + private BigDecimal ornamentsPrice; + + @Excel(name = "饰品图片") + private String imageUrl; + + @Excel(name = "磨损度") + @TableField("exterior_name") + private String exteriorName; + + + // 0、在背包显示 1、已分解 + @Excel(name = "状态") + private Integer status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date updateTime; + + // 0、对战 + @Excel(name = "饰品来源") + private Integer source; + + @Excel(name = "对战ID") + private Integer fightId; + + @Excel(name = "对战模式:回合数") + private Integer fightRoundNumber; + + @Excel(name = "Roll房ID") + private Integer rollId; + + // 用于对战模式,游戏完全结束才显示 + @TableField("is_show") + private Integer isShow; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/client/PackSackGlobalData.java b/skins-model/src/main/java/com/ruoyi/domain/vo/client/PackSackGlobalData.java new file mode 100644 index 0000000..1736aa1 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/client/PackSackGlobalData.java @@ -0,0 +1,20 @@ +package com.ruoyi.domain.vo.client; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PackSackGlobalData { + + // private String userName; + + // 物品总数 + private Integer totalOrnamentNumber; + + // 物品总价值 + private Integer totalOrnamentPrice; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/client/WelfareBoxVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/client/WelfareBoxVO.java new file mode 100644 index 0000000..b39dea3 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/client/WelfareBoxVO.java @@ -0,0 +1,61 @@ +package com.ruoyi.domain.vo.client; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 福利箱子VO + */ +@Data +public class WelfareBoxVO { + + /** 库存记录ID */ + private Long id; + + /** 福利ID */ + private Integer welfareId; + + /** 福利名称 */ + private String welfareName; + + /** 福利类型 */ + private String type; + + /** VIP等级 */ + private Integer vipLevel; + + /** 箱子ID */ + private Integer boxId; + + /** 箱子名称 */ + private String boxName; + + /** 箱子图片 */ + private String boxImg01; + + /** 箱子图片2 */ + private String boxImg02; + + /** 箱子价格 */ + private BigDecimal price; + + /** 状态:0=未开启 1=已开启 */ + private Integer status; + + /** 领取日期 */ + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai") + private Date claimDate; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; + + /** 是否具备领取条件(0否 1是) */ + private String eligible; + + /** 领取状态(0未领取 1已领取) */ + private String claimStatus; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/delivery/DeliveryRecordVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/delivery/DeliveryRecordVO.java new file mode 100644 index 0000000..5cbe0db --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/delivery/DeliveryRecordVO.java @@ -0,0 +1,87 @@ +package com.ruoyi.domain.vo.delivery; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class DeliveryRecordVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "用户背包记录ID") + private Long boxRecordsId; + + @Excel(name = "饰品ID") + @TableField("ornament_id") + private Long ornamentId; + + @Excel(name = "饰品图片") + private String ornamentName; + + @Excel(name = "饰品图片") + private String ornamentImg; + + @Excel(name = "饰品图片") + private String ornamentLevelImg; + + @Excel(name = "市场唯一hashName") + @TableField("market_hash_name") + private String marketHashName; + + @Excel(name = "饰品价格") + private BigDecimal ornamentsPrice; + + @Excel(name = "网站订单号") + private String outTradeNo; + + //网站发货模式(1人工发货 2自动发货 3主播号提取) + @Excel(name = "网站发货模式") + private Integer delivery; + + @Excel(name = "实际购买价格") + private BigDecimal buyPrice; + + @Excel(name = "第三方平台订单号") + private String orderId; + + @Excel(name = "第三方发货模式") + private Integer thirdpartyDelivery; + + // DeliveryOrderStatus 发货订单状态(0发起提货 1待发货 3待收货 10订单完成 11订单取消) + @Excel(name = "发货订单状态") + private Integer status; + private String message; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/fight/AudienceVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/AudienceVO.java new file mode 100644 index 0000000..e777cc7 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/AudienceVO.java @@ -0,0 +1,34 @@ +package com.ruoyi.domain.vo.fight; + +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.fight.TtFight; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +@Builder +public class AudienceVO { + + private Integer currentRound; + + private List winnerIds; + + private TtFight fight; + + // 开箱结果(正常回合,fightRoundNumber > 0) + private List fightResult; + + // 胜利方获得的所有饰品(自己开的 + 失败方的,对战结束后胜利方弹框展示) + private List winnerPrize; + + // 失败方安慰奖(fightRoundNumber = -1,对战结束后失败方弹框展示) + private List endGamePrize; + + // 宝箱详情 + private List fightBoxVOList; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightBoxVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightBoxVO.java new file mode 100644 index 0000000..f76ed14 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightBoxVO.java @@ -0,0 +1,56 @@ +package com.ruoyi.domain.vo.fight; + +import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class FightBoxVO implements Serializable{ + + private static final long serialVersionUID = 1L; + + private Integer boxId; + private Integer number; + private String boxImg01; + private String boxImg02; + private BigDecimal price; + private String boxName; + + // 宝箱所有饰品信息 + private List ornaments; + + public FightBoxVO(Integer boxId,Integer number,String boxImg01,String boxImg02){ + this.boxId = boxId; + this.number = number; + this.boxImg01 = boxImg01; + this.boxImg02 = boxImg02; + } + // + // public FightBoxVO(String number,String boxImg01,String boxImg02){ + // this.number = Integer.valueOf(number); + // this.boxImg01 = boxImg01; + // this.boxImg02 = boxImg02; + // } + + // public FightBoxVO(String json){ + // + // ObjectMapper objectMapper = new ObjectMapper(); + // try { + // FightBoxVO fightBoxVO = objectMapper.readValue(json, FightBoxVO.class); + // this.number = fightBoxVO.getNumber(); + // this.boxImg01 = fightBoxVO.getBoxImg01(); + // this.boxImg02 = fightBoxVO.getBoxImg02(); + // } catch (JsonProcessingException e) { + // System.out.println("FightBoxVO【构造器异常】"); + // } + // } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightBrief.java b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightBrief.java new file mode 100644 index 0000000..dee3dcb --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightBrief.java @@ -0,0 +1,55 @@ +package com.ruoyi.domain.vo.fight; + +import com.ruoyi.domain.entity.fight.FightSeat; +import com.ruoyi.domain.entity.fight.TtFight; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FightBrief implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer userId; + private List seats; + private String model; + private Integer teamMode; + private Integer playerNum; + private Integer spectatorNum; + private Integer roundNumber; + private List winnerIds; + private BigDecimal boxPriceTotal; + private Integer status; + private Date endTime; + private Timestamp beginTime; + + public static FightBrief from(TtFight fight) { + if (fight == null) return null; + return FightBrief.builder() + .id(fight.getId()) + .userId(fight.getUserId()) + .seats(fight.getSeats()) + .model(fight.getModel()) + .teamMode(fight.getTeamMode()) + .playerNum(fight.getPlayerNum()) + .roundNumber(fight.getRoundNumber()) + .winnerIds(fight.getWinnerIds()) + .boxPriceTotal(fight.getBoxPriceTotal()) + .status(fight.getStatus()) + .beginTime(fight.getBeginTime()) + .endTime(fight.getEndTime()) + .build(); + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightResultVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightResultVO.java new file mode 100644 index 0000000..4c47432 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightResultVO.java @@ -0,0 +1,51 @@ +package com.ruoyi.domain.vo.fight; + +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.fight.TtFight; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class FightResultVO { + + private Long currentRound; + + private List winnerIds; + + private TtFight fight; + + // 开箱结果(正常回合,fightRoundNumber > 0) + private List fightResult; + + // 胜利方获得的所有饰品(自己开的 + 失败方的,对战结束后胜利方弹框展示) + private List winnerPrize; + + // 失败方安慰奖(fightRoundNumber = -1,对战结束后失败方弹框展示) + private List endGamePrize; + + /** + * 开箱饰品总金额 + */ + private BigDecimal totalOrnamentsPrice; + + /** + * 赢家饰品总金额(winnerPrize 中所有 ornamentsPrice 之和) + */ + private BigDecimal winnerTotalPrice; + + /** + * 失败方安慰奖总金额(endGamePrize 中所有 ornamentsPrice 之和,通常每项 0.01) + */ + private BigDecimal loserTotalPrice; + + // 宝箱详情 + private List fightBoxVOList; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightState.java b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightState.java new file mode 100644 index 0000000..272523f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/FightState.java @@ -0,0 +1,125 @@ +package com.ruoyi.domain.vo.fight; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.ruoyi.domain.entity.TtBoxRecords; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FightState implements Serializable { + + private static final long serialVersionUID = 1L; + + private String phase; + private Integer currentRound; + private Integer totalRounds; + private FightBrief fight; + private List revealedRounds; + private List winnerIds; + private List fightBoxVOList; + private Integer spectatorNum; + + private List roundResults; + private Settlement settlement; + private Boolean sync; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class RoundResult implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer round; + private List records; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class RoundRecord implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long recordId; + private Integer userId; + private Long ornamentId; + private Integer boxId; + private String ornamentName; + private BigDecimal ornamentsPrice; + private String imageUrl; + private Integer ornamentsLevelId; + private String ornamentLevelImg; + /** 饰品外观/磨损度名称,如:崭新出厂、略有磨损、久经沙场、破损不堪、战痕累累 */ + private String exteriorName; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class RoundAssignment implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long recordId; + private Integer userId; + private Long ornamentId; + private Integer boxId; + private Integer holderUserId; + private Integer ornamentsLevelId; + private String ornamentLevelImg; + /** 饰品外观/磨损度名称 */ + private String exteriorName; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class PrizeRecord implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long recordId; + private Integer userId; + private Long ornamentId; + private Integer holderUserId; + private String ornamentName; + private BigDecimal ornamentsPrice; + private String imageUrl; + private Integer ornamentsLevelId; + private String ornamentLevelImg; + /** 饰品外观/磨损度名称 */ + private String exteriorName; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class Settlement implements Serializable { + + private static final long serialVersionUID = 1L; + + private List roundAssignments; + private List prizeRecords; + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/fight/PersonResult.java b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/PersonResult.java new file mode 100644 index 0000000..1a39b89 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/PersonResult.java @@ -0,0 +1,10 @@ +package com.ruoyi.domain.vo.fight; + +import com.ruoyi.domain.entity.TtBoxRecords; + +public class PersonResult { + + private Integer userId; + + private TtBoxRecords openBoxRecord; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/fight/RoundResult.java b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/RoundResult.java new file mode 100644 index 0000000..8088229 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/RoundResult.java @@ -0,0 +1,15 @@ +package com.ruoyi.domain.vo.fight; + +import java.util.List; + +public class RoundResult { + + // 回合数 + private Integer roundNumber; + + // 宝箱id + private Integer boxId; + + // 本回合的开箱结果集合 + private List personResultList; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/fight/TtFightVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/TtFightVO.java new file mode 100644 index 0000000..6aef086 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/fight/TtFightVO.java @@ -0,0 +1,85 @@ +package com.ruoyi.domain.vo.fight; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.fight.FightSeat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtFightVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "创建者ID") + private Integer userId; + + @Excel(name = "座位") + @TableField(value = "seats",typeHandler = JacksonTypeHandler.class) + private List seats; + + // 1、欧皇 2、非酋 + @Excel(name = "对战模式") + private String model; + + @Excel(name = "对战人数") + private Integer playerNum; + + @Excel(name = "回合数") + private Integer roundNumber; + + @TableField(value = "winner_ids",typeHandler = JacksonTypeHandler.class) + private List winnerIds; + + private List fightResult; + + @Excel(name = "选择宝箱数据") + @TableField(value = "box_data",typeHandler = JacksonTypeHandler.class) + private Map boxData; + + @Excel(name = "创建宝箱价格总数") + private BigDecimal boxPriceTotal; + + // 0,准备 1,进行中 2,结束 3,超时强制结束 + @Excel(name = "对战状态") + private Integer status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + // 开始时间 + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Timestamp beginTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @Excel(name = "备注") + private String remark; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/lottery/TurnoverLotteryWinVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/lottery/TurnoverLotteryWinVO.java new file mode 100644 index 0000000..754a9af --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/lottery/TurnoverLotteryWinVO.java @@ -0,0 +1,61 @@ +package com.ruoyi.domain.vo.lottery; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 流水抽奖全服中奖记录(含用户昵称、头像) + * 已排除白号、机器人、封禁及已删除用户 + */ +@Data +public class TurnoverLotteryWinVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 记录 ID */ + private Long id; + + /** 中奖用户 ID */ + private Integer userId; + + /** 中奖用户昵称 */ + private String nickName; + + /** 中奖用户头像 */ + private String avatar; + + /** 抽奖活动 ID */ + private Long lotteryId; + + /** 抽奖活动名称 */ + private String lotteryName; + + /** 奖品 ID */ + private Long prizeId; + + /** 饰品名称 */ + private String ornamentName; + + /** 饰品图片 */ + private String imageUrl; + + /** 奖品价值 */ + private BigDecimal ornamentsPrice; + + /** 品质等级 ID */ + private Integer ornamentLevel; + + /** 品质底图 */ + private String levelImg; + + /** 磨损度 */ + private String exteriorName; + + /** 中奖时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/order/TtOrderVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/order/TtOrderVO.java new file mode 100644 index 0000000..22cdc09 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/order/TtOrderVO.java @@ -0,0 +1,75 @@ +package com.ruoyi.domain.vo.order; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtOrderVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "第三方") + private String thirdParty; + /** + * 0卡密 1支付宝 2微信 + */ + @Excel(name = "订单类型") + private String type; + + @Excel(name = "商品ID") + private Integer goodsId; + + @Excel(name = "商品价格") + private BigDecimal goodsPrice; + + @Excel(name = "商品数量") + private Integer goodsNum; + + @Excel(name = "商品总价") + private BigDecimal totalAmount; + + @Excel(name = "订单号") + private String orderId; + + @Excel(name = "外部订单号") + private String outTradeNo; + + @Excel(name = "sign验签密钥") + private String sign; + + @Excel(name = "支付状态") + private String status; + + @Excel(name = "支付跳转链接") + private String payUrl; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/promotion/AnchorDailyTurnoverVo.java b/skins-model/src/main/java/com/ruoyi/domain/vo/promotion/AnchorDailyTurnoverVo.java new file mode 100644 index 0000000..ee6afc6 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/promotion/AnchorDailyTurnoverVo.java @@ -0,0 +1,37 @@ +package com.ruoyi.domain.vo.promotion; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 主播日流水 + */ +@Data +public class AnchorDailyTurnoverVo { + + /** + * 上级ID + */ + private Integer parentId; + + /** + * 上级昵称 + */ + private String parentNickName; + + /** + * 主播下级日消费 + */ + private BigDecimal dailyTotal; + + /** + * 主播下级总消费 + */ + private BigDecimal totalByParent; + + @JsonFormat(pattern = "yyyy-MM-dd") + private Date date; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/promotion/TtPromotionRecordVo.java b/skins-model/src/main/java/com/ruoyi/domain/vo/promotion/TtPromotionRecordVo.java new file mode 100644 index 0000000..01547bb --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/promotion/TtPromotionRecordVo.java @@ -0,0 +1,47 @@ +package com.ruoyi.domain.vo.promotion; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtPromotionRecordVo { + private Integer userId; + + /** + * 下级用户ID + */ + private Integer subordinateUserId; + + /** + * 下级用户名 + */ + private String subordinateUserName; + + /** + * 下级用户充值金额 + */ + private BigDecimal rechargePrice; + + /** + * 返佣金额 + */ + private BigDecimal rebate; + /** + * 奖励比例 + */ + private BigDecimal commissions; + /** + * 充值时间 + */ + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/queryTemplateSaleByCategoryDataVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/queryTemplateSaleByCategoryDataVO.java new file mode 100644 index 0000000..90f6a8f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/queryTemplateSaleByCategoryDataVO.java @@ -0,0 +1,16 @@ +package com.ruoyi.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class queryTemplateSaleByCategoryDataVO { + private List saleTemplateByCategoryResponseList; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/queryTemplateSaleByCategoryVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/queryTemplateSaleByCategoryVO.java new file mode 100644 index 0000000..9f8dacc --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/queryTemplateSaleByCategoryVO.java @@ -0,0 +1,30 @@ +package com.ruoyi.domain.vo; + +//yy有品 批量查询在售商品详情vo + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class queryTemplateSaleByCategoryVO { + private Integer templateId; + private String templateHashName; + private String templateName; + private String iconUrl; + private String exteriorName; + private String rarityName; + private Integer typeId; + private String typeHashName; + private Integer weaponId; + private String weaponHashName; + private BigDecimal minSellPrice; + private BigDecimal referencePrice; + private Integer sellNum; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollJackpotOrnamentsByPageVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollJackpotOrnamentsByPageVO.java new file mode 100644 index 0000000..d219c07 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollJackpotOrnamentsByPageVO.java @@ -0,0 +1,30 @@ +package com.ruoyi.domain.vo.roll; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class RollJackpotOrnamentsByPageVO implements Serializable { + + private List list; + + private Integer total; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollJackpotOrnamentsVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollJackpotOrnamentsVO.java new file mode 100644 index 0000000..a0ed9f9 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollJackpotOrnamentsVO.java @@ -0,0 +1,60 @@ +package com.ruoyi.domain.vo.roll; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class RollJackpotOrnamentsVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "Roll房奖池ID") + private Integer jackpotId; + + @Excel(name = "Roll房奖池饰品ID") + private Long ornamentsId; + + @Excel(name = "饰品价格") + private BigDecimal price; + + @Excel(name = "饰品图片") + private String imgUrl; + + @Excel(name = "饰品名称") + private String ornamentName; + + @Excel(name = "饰品级别ID") + private Integer ornamentLevelId; + + private String ornamentLevelImg; + + @Excel(name = "饰品数量") + private Integer ornamentsNum; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollUserPrizeVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollUserPrizeVO.java new file mode 100644 index 0000000..6d2a7d0 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollUserPrizeVO.java @@ -0,0 +1,79 @@ +package com.ruoyi.domain.vo.roll; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class RollUserPrizeVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer rollUserPrizeId; + + @Excel(name = "roll用户ID") + @TableField("roll_user_id") + private Integer rollUserId; + + // 用户id + private Integer userId; + + private String nickName; + + private String designatedBy; + + // roll房id + private Integer rollId; + + @Excel(name = "Roll房奖池id") + @TableField("roll_jackpot_id") + private Integer rollJackpotId; + + @Excel(name = "奖池饰品ID") + @TableField("roll_jackpot_ornament_id") + private Integer rollJackpotOrnamentId; + + @Excel(name = "饰品ID") + @TableField("ornament_id") + private Long ornamentId; + + @Excel(name = "饰品名称") + @TableField("ornament_name") + private String ornamentName; + + // 饰品图片 + private String ornamentImg; + + @Excel(name = "饰品图片") + @TableField("img_url") + private String imgUrl; + + @Excel(name = "饰品价格") + @TableField("price") + private BigDecimal price; + + @Excel(name = "奖品数量") + @TableField("number") + private Integer number; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollUserVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollUserVO.java new file mode 100644 index 0000000..06d7b5f --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/RollUserVO.java @@ -0,0 +1,69 @@ +package com.ruoyi.domain.vo.roll; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class RollUserVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "用户名") + private String userName; + + @Excel(name = "昵称") + private String nickName; + + @Excel(name = "头像") + private String avatar; + + @Excel(name = "Roll房ID") + private Integer rollId; + + @Excel(name = "奖池饰品列表ID") + private Integer jackpotOrnamentsId; + + @Excel(name = "奖池饰品ID") + private Long ornamentsId; + + @Excel(name = "宝箱记录ID") + private Long boxRecordId; + + // 2系统指定获奖者 + private String status; + + @Excel(name = "指定者") + private String designatedBy; + + @TableField("get_prize_way") + private Integer getPrizeWay; + + @JsonFormat(pattern = "yyyy-MM-dd") + private Date joinTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/roll/SimpleRollOrnamentVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/SimpleRollOrnamentVO.java new file mode 100644 index 0000000..1504204 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/roll/SimpleRollOrnamentVO.java @@ -0,0 +1,58 @@ +package com.ruoyi.domain.vo.roll; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class SimpleRollOrnamentVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + @Excel(name = "Roll房奖池ID") + private Integer jackpotId; + + @Excel(name = "饰品ID") + private Long ornamentId; + + @Excel(name = "饰品价格") + private BigDecimal price; + + @Excel(name = "饰品图片") + private String imgUrl; + + // 类型 + private String typeName; + + // 外观 + private String exteriorName; + + @Excel(name = "饰品名称") + private String ornamentName; + + @Excel(name = "饰品级别ID") + private Integer ornamentsLevelId; + + @Excel(name = "饰品级别img") + private String ornamentsLevelImg; + + @Excel(name = "饰品数量") + private Integer ornamentsNum; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/share/ShareCenterVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/share/ShareCenterVO.java new file mode 100644 index 0000000..8d07c78 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/share/ShareCenterVO.java @@ -0,0 +1,30 @@ +package com.ruoyi.domain.vo.share; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 主页 - 游戏分享中心顶部数据 + */ +@Data +public class ShareCenterVO { + + /** 邀请码 */ + private String invitationCode; + + /** 今日收益(今日下级充值/玩法返佣合计,次日 00:00 失效仅文案体现) */ + private BigDecimal todayEarnings; + + /** 累计收益(全部返佣合计,与 总计 同一含义) */ + private BigDecimal totalEarnings; + + /** 邀请人数(绑定的下级总人数) */ + private Integer inviteCount; + + /** 分享链接(当前网站链接 + 邀请码) */ + private String shareLink; + + /** 邀请新人充值比例(展示格式,如 "2%") */ + private String commissionRate; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/share/ShareRecordPageVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/share/ShareRecordPageVO.java new file mode 100644 index 0000000..a52d428 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/share/ShareRecordPageVO.java @@ -0,0 +1,23 @@ +package com.ruoyi.domain.vo.share; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; + +/** + * 主页 - 分享列表分页响应 + */ +@Data +public class ShareRecordPageVO { + + /** 收益总计(顶部"总计"展示,等于累计收益) */ + private BigDecimal totalRebate = BigDecimal.ZERO; + + /** 总记录条数 */ + private long total = 0L; + + /** 当前页数据 */ + private List list = Collections.emptyList(); +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/share/ShareRecordVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/share/ShareRecordVO.java new file mode 100644 index 0000000..584b9ca --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/share/ShareRecordVO.java @@ -0,0 +1,34 @@ +package com.ruoyi.domain.vo.share; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 主页 - 分享列表行数据 + */ +@Data +public class ShareRecordVO { + + /** 被邀请人用户ID(账号列展示) */ + private Integer subUserId; + + /** + * 状态:"充值" / "邀请" + * - 充值:来自 tt_promotion_record(下级充值产生的返佣) + * - 邀请:来自 tt_promotion_update(下级注册绑定) + */ + private String status; + + /** 充值金额(仅"充值"行有值,"邀请"行为 "--") */ + private String rechargePrice; + + /** 收益(返佣金额;邀请行如未配置邀请奖励则为 0) */ + private BigDecimal rebate; + + /** 时间(充值行=充值返佣时间,邀请行=绑定时间) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/sys/SimpleTtUserVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/sys/SimpleTtUserVO.java new file mode 100644 index 0000000..5acf4a5 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/sys/SimpleTtUserVO.java @@ -0,0 +1,124 @@ +package com.ruoyi.domain.vo.sys; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class SimpleTtUserVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "用户ID") + @TableId(value = "user_id",type = IdType.AUTO) + private Integer userId; + + @Excel(name = "用户账号") + private String userName; + + @Excel(name = "用户昵称") + private String nickName; + + //01主播 02普通用户 + private String userType; + + @Excel(name = "用户邮箱") + private String email; + + @Excel(name = "手机号码") + private String phoneNumber; + + @Excel(name = "头像地址") + private String avatar; + + // 充值金额 + private BigDecimal recharge; + + // 金币消费 + private BigDecimal amountConsume; + + // 积分消费 + private BigDecimal creditsConsume; + + // 综合消费 + private BigDecimal beConsume; + + // @Excel(name = "账户金额") + // private BigDecimal accountAmount; + // + // @Excel(name = "账户积分") + // private BigDecimal accountCredits; + + // @Excel(name = "邀请码") + // private String invitationCode; + + @Excel(name = "上级ID") + private Integer parentId; + + @Excel(name = "VIP等级") + private Integer vipLevel; + + @Excel(name = "推广等级") + private Integer promotionLevel; + + private String status; + + private String deliveryStatus; + + // @Excel(name = "steam账号ID") + // private Long steamId; + // + // @Excel(name = "steam交易链接") + // @TableField("transaction_link") + // private String transactionLink; + + // @Excel(name = "真实姓名") + // private String realName; + // + // @Excel(name = "身份证号码") + // private String idNum; + // + // @Excel(name = "实名认证流程号") + // private String certifyId; + // + // private String isRealCheck; + + @Excel(name = "最后登录IP") + private String loginIp; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date loginDate; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @Excel(name = "备注") + private String remark; + + @TableField(select = false) + private String delFlag; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/task/SimpleAmountRecordVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/task/SimpleAmountRecordVO.java new file mode 100644 index 0000000..bd447e0 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/task/SimpleAmountRecordVO.java @@ -0,0 +1,60 @@ +package com.ruoyi.domain.vo.task; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class SimpleAmountRecordVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "上级ID") + private Integer parentId; + + // 1、收入 2、支出 + private Integer type; + + // 来源 + private Integer source; + + @Excel(name = "变动金额") + private BigDecimal amount; + + @Excel(name = "最终金额") + private BigDecimal finalAmount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String remark; + + private Integer pwChildId; + private String pwChildName; + private BigDecimal pwChildAccount; + + @TableField("task_id") + private Integer taskId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/task/SimpleBlendErcashRecordVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/task/SimpleBlendErcashRecordVO.java new file mode 100644 index 0000000..4b0cc03 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/task/SimpleBlendErcashRecordVO.java @@ -0,0 +1,59 @@ +package com.ruoyi.domain.vo.task; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class SimpleBlendErcashRecordVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "上级ID") + private Integer parentId; + + // 1、收入 2、支出 + private Integer type; + + // 来源 + private Integer source; + + @Excel(name = "变动金币") + private BigDecimal amount; + + @Excel(name = "变动积分") + private BigDecimal credits; + + // 合计 + private BigDecimal total; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private String remark; + +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/task/SimpleCreditsRecordVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/task/SimpleCreditsRecordVO.java new file mode 100644 index 0000000..3cc55ea --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/task/SimpleCreditsRecordVO.java @@ -0,0 +1,60 @@ +package com.ruoyi.domain.vo.task; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class SimpleCreditsRecordVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @TableId + private Integer id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "上级ID") + private Integer parentId; + + private Integer type; + + private Integer source; + + @Excel(name = "变动积分") + private BigDecimal credits; + + @Excel(name = "最终积分") + private BigDecimal finalCredits; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String remark; + + private Integer pwChildId; + + private String pwChildName; + + private BigDecimal pwChildAccount; + + @TableField("task_id") + private Integer taskId; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/transfer/TransferApplyBatchOrnamentVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/transfer/TransferApplyBatchOrnamentVO.java new file mode 100644 index 0000000..93a5bb4 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/transfer/TransferApplyBatchOrnamentVO.java @@ -0,0 +1,30 @@ +package com.ruoyi.domain.vo.transfer; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 转赠申请批次内的饰品明细 + */ +@Data +public class TransferApplyBatchOrnamentVO { + + /** 转赠申请单ID */ + private Integer id; + + /** 背包记录ID */ + private Long boxRecordId; + + /** 饰品ID */ + private Long ornamentId; + + /** 饰品名称 */ + private String ornamentName; + + /** 饰品图片 */ + private String ornamentImageUrl; + + /** 饰品价格 */ + private BigDecimal ornamentPrice; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/transfer/TransferApplyBatchVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/transfer/TransferApplyBatchVO.java new file mode 100644 index 0000000..387d4bc --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/transfer/TransferApplyBatchVO.java @@ -0,0 +1,67 @@ +package com.ruoyi.domain.vo.transfer; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; +import java.util.List; + +/** + * 转赠申请批次列表VO(同一批次内多个饰品聚合展示) + */ +@Data +public class TransferApplyBatchVO { + + /** 批次号 */ + private String batchId; + + /** 转赠人用户ID */ + private Integer fromUserId; + + /** 转赠人昵称 */ + private String fromNickName; + + /** 转赠人手机号 */ + private String fromPhoneNumber; + + /** 受赠人用户ID */ + private Integer toUserId; + + /** 受赠人昵称 */ + private String toNickName; + + /** 受赠人手机号 */ + private String toPhoneNumber; + + /** + * 状态:0-待审核,1-审核通过,2-审核驳回 + */ + private Integer status; + + /** 驳回原因 */ + private String rejectReason; + + /** 当前 VIP 等级 */ + private Integer vipLevel; + + /** 今日 VIP 转赠次数上限 */ + private Integer dailyTotalCount; + + /** 今日已成功转赠次数 */ + private Integer dailyUsedCount; + + /** 今日审核中预扣次数 */ + private Integer dailyPendingCount; + + /** 今日剩余可用转赠次数 */ + private Integer dailyAvailableCount; + + /** 该批次包含的饰品明细 */ + private List ornaments; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/transfer/TransferApplyVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/transfer/TransferApplyVO.java new file mode 100644 index 0000000..f3138cd --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/transfer/TransferApplyVO.java @@ -0,0 +1,98 @@ +package com.ruoyi.domain.vo.transfer; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 转赠申请列表VO + */ +@Data +public class TransferApplyVO { + + private Integer id; + + /** 背包记录ID */ + private Long boxRecordId; + + /** 转赠人用户ID */ + private Integer fromUserId; + + /** 转赠人昵称 */ + private String fromNickName; + + /** 转赠人手机号 */ + private String fromPhoneNumber; + + /** 受赠人用户ID */ + private Integer toUserId; + + /** 受赠人昵称 */ + private String toNickName; + + /** 受赠人手机号 */ + private String toPhoneNumber; + + /** 饰品ID */ + private Long ornamentId; + + /** 饰品名称 */ + private String ornamentName; + + /** 饰品图片 */ + private String ornamentImageUrl; + + /** 饰品价格 */ + private BigDecimal ornamentPrice; + + /** + * 状态:0-待审核,1-审核通过,2-审核驳回 + */ + private Integer status; + + /** 驳回原因 */ + private String rejectReason; + + /** + * 当前记录是该用户今日第几次转赠(递增:1, 2, 3...) + */ + private Integer transferCount; + + /** + * 该用户今日总转赠次数(用于判断是否标红,>=3次时该用户所有今日记录都标红) + */ + private Integer todayTotalCount; + + /** + * 转赠人当前 VIP 等级 + */ + private Integer vipLevel; + + /** + * 今日 VIP 转赠次数上限 + */ + private Integer dailyTotalCount; + + /** + * 今日已成功转赠次数 + */ + private Integer dailyUsedCount; + + /** + * 今日审核中预扣次数 + */ + private Integer dailyPendingCount; + + /** + * 今日剩余可用转赠次数 + */ + private Integer dailyAvailableCount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/upgrade/SimpleOrnamentVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/upgrade/SimpleOrnamentVO.java new file mode 100644 index 0000000..8b33f09 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/upgrade/SimpleOrnamentVO.java @@ -0,0 +1,33 @@ +package com.ruoyi.domain.vo.upgrade; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.Date; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class SimpleOrnamentVO { + /** 背包记录ID(tt_box_records.id),用于分解接口 /api/userPackSack/decompose 入参 packSackIds */ + private Long id; + private Long ornamentId; + private String ornamentName; + private Integer ornamentNumber; + private String ornamentHashName; + private BigDecimal ornamentPrice; + private String ornamentImgUrl; + private Integer ornamentLevelId; + private String ornamentLevel; + private String ornamentLevelImg; + private Integer quantity; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/upgrade/UpgradeRecordVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/upgrade/UpgradeRecordVO.java new file mode 100644 index 0000000..ea75a5e --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/upgrade/UpgradeRecordVO.java @@ -0,0 +1,108 @@ +package com.ruoyi.domain.vo.upgrade; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import jakarta.validation.constraints.NotNull; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class UpgradeRecordVO implements Serializable { + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @Excel(name = "升级记录ID") + @TableId + private Long id; + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "昵称") + private String nickName; + + private String avatar; + + @Excel(name = "用户类型") + private String userType; + + @Excel(name = "消耗金额") + private BigDecimal amountConsumed; + + private BigDecimal profit; + + @Excel(name = "本轮目标金额") + private BigDecimal amountRequired; + + @Excel(name = "本轮已投金额快照") + private BigDecimal amountInvested; + + @Excel(name = "起始金额") + private BigDecimal effectiveAmountInvested; + + @Excel(name = "真实计算金额") + private BigDecimal effectiveAmount; + + @Excel(name = "可升级饰品ID") + @TableField("target_upgrade_id") + private Integer targetUpgradeId; + + @Excel(name = "目标饰品ID") + @TableField("target_ornament_id") + private Long targetOrnamentId; + + private String targetOrnamentName; + + private String targetOrnamentImg; + + @Excel(name = "目标饰品价格") + @TableField("target_ornament_price") + private BigDecimal targetOrnamentPrice; + + @Excel(name = "概率") + @TableField("probability") + private Integer probability; + + @Excel(name = "最终奖励饰品集合") + @TableField("gain_ornament_list") + private String gainOrnamentList; + + /** 获得饰品全名(管理端列表展示用,从 gain_ornament_list 中提取) */ + @TableField(exist = false) + private String gainOrnamentName; + + @Excel(name = "最终奖励总价") + @TableField("gain_ornaments_price") + private BigDecimal gainOrnamentsPrice; + + @Excel(name = "成败") + @TableField("is_victory") + private Boolean isVictory; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date openTime; + + // 记录总条数 分页辅助 + private Integer total; + + @NotNull(message = "页码不能为空") + private Integer page; + + @NotNull(message = "分页长度不能为空") + private Integer size; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/user/TtUseChildInfoVo.java b/skins-model/src/main/java/com/ruoyi/domain/vo/user/TtUseChildInfoVo.java new file mode 100644 index 0000000..e30e9f1 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/user/TtUseChildInfoVo.java @@ -0,0 +1,27 @@ +package com.ruoyi.domain.vo.user; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TtUseChildInfoVo { + /** + * 用户ID + */ + private Integer userId; + + /** + * 用户账号 + */ + private String userName; + + /** + * 用户昵称 + */ + private String nickName; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/user/TtUserInfoVo.java b/skins-model/src/main/java/com/ruoyi/domain/vo/user/TtUserInfoVo.java new file mode 100644 index 0000000..f733f1b --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/user/TtUserInfoVo.java @@ -0,0 +1,40 @@ +package com.ruoyi.domain.vo.user; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TtUserInfoVo { + private Integer userId; + /** + * 父ID + */ + private Integer parentId; + /** + * 父名称 + */ + private String parentNickName; + /** + * 下级个数 + */ + private Integer childNum; + /** + * 下级总充值金额 + */ + private BigDecimal rechargePrice; + /** + * 充值收益 + */ + private BigDecimal commissionsRate; + /** + * 流水收益 + */ + private BigDecimal turnoverRate; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/user/TtUserVo.java b/skins-model/src/main/java/com/ruoyi/domain/vo/user/TtUserVo.java new file mode 100644 index 0000000..ed82988 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/user/TtUserVo.java @@ -0,0 +1,131 @@ +package com.ruoyi.domain.vo.user; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.domain.entity.sys.TtUser; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TtUserVo { + + @Excel(name = "用户ID") + private Integer userId; + + @Excel(name = "用户账号") + private String userName; + + @Excel(name = "用户昵称") + private String nickName; + + //01主播 02普通用户 + private String userType; + + @Excel(name = "用户邮箱") + private String email; + + @Excel(name = "手机号码") + private String phoneNumber; + + @Excel(name = "头像地址") + private String avatar; + + @Excel(name = "密码") + private String password; + + @Excel(name = "账户金额") + private BigDecimal accountAmount; + + @Excel(name = "用户总充值") + private BigDecimal totalPrice; + + @Excel(name = "用户总亏损") + private BigDecimal totalLoss; + + @Excel(name = "总提取") + private BigDecimal totalDelivery; + + @Excel(name = "账户积分") + private BigDecimal accountCredits; + + /** + * 卡商专用余额(仅卡商充值页面展示) + */ + private BigDecimal merchantBalance; + + @Excel(name = "邀请码") + private String invitationCode; + + @Excel(name = "上级ID") + private Integer parentId; + + @Excel(name = "VIP等级") + private Integer vipLevel; + + @Excel(name = "推广等级") + private Integer promotionLevel; + + private String status; + + private String deliveryStatus; + + /** + * 赠送状态:0=启用(可被赠送),1=禁用(不可被赠送) + */ + private String giftStatus; + + @Excel(name = "steam账号ID") + private Long steamId; + + @Excel(name = "steam交易链接") + private String transactionLink; + + @Excel(name = "真实姓名") + private String realName; + + @Excel(name = "身份证号码") + private String idNum; + + @Excel(name = "实名认证流程号") + private String certifyId; + + private String isRealCheck; + + /** + * 邀请新人充值比例(主页分享返佣) + */ + private BigDecimal commissionRate; + + @Excel(name = "最后登录IP") + private String loginIp; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date loginDate; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + @Excel(name = "备注") + private String remark; + + //0正常 + private String delFlag; + + private List children; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/userRecord/ProfileRecordItemVO.java b/skins-model/src/main/java/com/ruoyi/domain/vo/userRecord/ProfileRecordItemVO.java new file mode 100644 index 0000000..8ea42d5 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/userRecord/ProfileRecordItemVO.java @@ -0,0 +1,77 @@ +package com.ruoyi.domain.vo.userRecord; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 个人资产详情 - 我的记录 统一列表项 + * 按 tab 选填字段,未使用字段保持 null,序列化时忽略。 + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProfileRecordItemVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 通用:发生时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date recordTime; + + /* ---------- tab=1 余额记录 / tab=4 赠送记录 ---------- */ + + /** 类型文案(如:佣金结算、饰品分解、创建对战、赠送、受赠) */ + private String type; + + /** 类型 code(余额=source;赠送=direction) */ + private Integer typeCode; + + /* ---------- tab=1 余额 / tab=2 充值 / tab=3 提货 / tab=4 赠送 ---------- */ + + /** 金额,余额记录带正负号;赠送记录为饰品价格 */ + private BigDecimal amount; + + /* ---------- tab=1 余额记录 ---------- */ + + /** 操作后余额 */ + private BigDecimal balance; + + /* ---------- tab=2 充值记录 ---------- */ + + /** 订单号 */ + private String orderNo; + + /* ---------- tab=3 提货记录 / tab=4 赠送记录 ---------- */ + + /** 饰品名称 */ + private String ornamentName; + + /** 饰品图片 */ + private String ornamentImg; + + /** 饰品品质图片(稀有度边框/底图) */ + private String ornamentLevelImg; + + /* ---------- tab=4 赠送记录 ---------- */ + + /** 赠送方向 1=我送出(赠送) 2=我收到(受赠) */ + private Integer direction; + + /** 对方昵称:direction=1 显示受赠人昵称;direction=2 显示赠送人昵称 */ + private String nickName; + + /** 对方用户ID:direction=1 时为受赠人ID;direction=2 时为赠送人ID */ + private Long counterpartyId; + + /** 状态文案(赠送均视为已完成) */ + private String status; + + /* ---------- tab=1 subFilter=5 奖励 ---------- */ + + /** 奖励子类型:1=Roll房饰品 2=每日福利箱 3=排行榜奖励 */ + private Integer subType; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/userRecord/ProfileRecordsResponse.java b/skins-model/src/main/java/com/ruoyi/domain/vo/userRecord/ProfileRecordsResponse.java new file mode 100644 index 0000000..6d1bdc3 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/userRecord/ProfileRecordsResponse.java @@ -0,0 +1,43 @@ +package com.ruoyi.domain.vo.userRecord; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * 个人资产详情 - 我的记录 统一响应 + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProfileRecordsResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 顶部统计区,按 tab 选填 */ + private SummaryVO summary; + + /** 列表数据 */ + private List list; + + /** 总条数 */ + private Long total; + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class SummaryVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 当前筛选时间段内总金额(充值/提货使用) */ + private BigDecimal totalAmount; + + /** 已完成条数(赠送使用) */ + private Long completedCount; + + /** 审核中条数(赠送使用,当前业务无审核流程,恒为0) */ + private Long pendingCount; + } +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/zbt/SellItemInfo.java b/skins-model/src/main/java/com/ruoyi/domain/vo/zbt/SellItemInfo.java new file mode 100644 index 0000000..6541e7d --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/zbt/SellItemInfo.java @@ -0,0 +1,32 @@ +package com.ruoyi.domain.vo.zbt; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class SellItemInfo { + + private Long itemId; + private String itemName; + private String marketHashName; + + + private BigDecimal price; + + // 补贴价 + private BigDecimal subsidyPrice; + + // 上架价格 + private BigDecimal sellerPrice; + + // 人民币价格 + private BigDecimal cnyPrice; + + // // 发货类型 + // private BigDecimal delivery; +} diff --git a/skins-model/src/main/java/com/ruoyi/domain/vo/zbt/SellListData.java b/skins-model/src/main/java/com/ruoyi/domain/vo/zbt/SellListData.java new file mode 100644 index 0000000..e682af0 --- /dev/null +++ b/skins-model/src/main/java/com/ruoyi/domain/vo/zbt/SellListData.java @@ -0,0 +1,22 @@ +package com.ruoyi.domain.vo.zbt; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class SellListData { + + private Integer limit; + private List list; + private String offsetToken; + private Integer page; + private Integer pages; + private String scrollId; + private Integer total; + +} diff --git a/skins-promo/pom.xml b/skins-promo/pom.xml new file mode 100644 index 0000000..5d14234 --- /dev/null +++ b/skins-promo/pom.xml @@ -0,0 +1,47 @@ + + + + ruoyi + com.ruoyi + 4.8.2 + + 4.0.0 + + skins-promo + + + 推广后台模块 + + + + + + com.ruoyi + ruoyi-common + + + + com.ruoyi + ruoyi-framework + + + com.ruoyi + service-thirdparty + 4.8.2 + + + com.ruoyi + service-user + 4.8.2 + + + + + com.ruoyi + message-service + 4.8.2 + + + \ No newline at end of file diff --git a/skins-promo/src/main/java/com/ruoyi/promo/config/PromoWebConfig.java b/skins-promo/src/main/java/com/ruoyi/promo/config/PromoWebConfig.java new file mode 100644 index 0000000..31aa163 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/config/PromoWebConfig.java @@ -0,0 +1,28 @@ +package com.ruoyi.promo.config; + +import com.ruoyi.promo.interceptor.PromoMaintenanceInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 推广后台 Web 配置:注册维护拦截器。 + *

+ * 对 /promo/** 生效,放行登录/注册接口(登录/注册由 PromoLoginController 自行提示维护)。 + * + * @author ruoyi + */ +@Configuration +public class PromoWebConfig implements WebMvcConfigurer { + + @Autowired + private PromoMaintenanceInterceptor promoMaintenanceInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(promoMaintenanceInterceptor) + .addPathPatterns("/promo/**") + .excludePathPatterns("/promo/login", "/promo/smsLogin", "/promo/register"); + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/contract/request/GetPurchaseByUserIdRequest.java b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/GetPurchaseByUserIdRequest.java new file mode 100644 index 0000000..ad7b0d1 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/GetPurchaseByUserIdRequest.java @@ -0,0 +1,17 @@ +package com.ruoyi.promo.contract.request; + +import lombok.Data; + +@Data +public class GetPurchaseByUserIdRequest { + /** + * 玩家ID + */ + private Integer userId; + + private String beginTime; + + private String endTime; + private Integer pageNum; + private Integer pageSize; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/contract/request/GetSubBranchesRequest.java b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/GetSubBranchesRequest.java new file mode 100644 index 0000000..d6f27c7 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/GetSubBranchesRequest.java @@ -0,0 +1,18 @@ +package com.ruoyi.promo.contract.request; + +import lombok.Data; + +@Data +public class GetSubBranchesRequest { + /** + * 主播ID + */ + private Integer userId; + /** + * 玩家id + */ + private Integer playerId; + + private Integer pageNum; + private Integer pageSize; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/contract/request/QueryPromoteWelfareRequest.java b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/QueryPromoteWelfareRequest.java new file mode 100644 index 0000000..764bfd8 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/QueryPromoteWelfareRequest.java @@ -0,0 +1,11 @@ +package com.ruoyi.promo.contract.request; + +import lombok.Data; + +@Data +public class QueryPromoteWelfareRequest { + private Integer pageNum; + private Integer pageSize; + private String beginTime; + private String endTime; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/contract/request/RegisterRequest.java b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/RegisterRequest.java new file mode 100644 index 0000000..e912175 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/RegisterRequest.java @@ -0,0 +1,22 @@ +package com.ruoyi.promo.contract.request; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class RegisterRequest { + @NotNull(message = "用户名不能为空") + private String username; + + @NotNull(message = "密码不能为空") + private String password; + + private String contactInformation; + + private String parentInvitationCode; + + @NotNull(message = "注册角色不能为空") + private String userType; + + private String code; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/contract/request/UpdateInvitationCodeRequest.java b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/UpdateInvitationCodeRequest.java new file mode 100644 index 0000000..69563ae --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/contract/request/UpdateInvitationCodeRequest.java @@ -0,0 +1,13 @@ +package com.ruoyi.promo.contract.request; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class UpdateInvitationCodeRequest { + @NotNull(message = "用户名不能为空") + private String username; + + @NotNull(message = "邀请码不能为空") + private String invitationCode; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/contract/response/GetShareDomainResponse.java b/skins-promo/src/main/java/com/ruoyi/promo/contract/response/GetShareDomainResponse.java new file mode 100644 index 0000000..5ca22fd --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/contract/response/GetShareDomainResponse.java @@ -0,0 +1,10 @@ +package com.ruoyi.promo.contract.response; + +import lombok.Data; + +@Data +public class GetShareDomainResponse { + private String zhuboShareDomain; + private String zhaoshaoShareDomain; + private String userShareDomain; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/contract/response/GetSubBranchesResponse.java b/skins-promo/src/main/java/com/ruoyi/promo/contract/response/GetSubBranchesResponse.java new file mode 100644 index 0000000..dc6127e --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/contract/response/GetSubBranchesResponse.java @@ -0,0 +1,35 @@ +package com.ruoyi.promo.contract.response; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Data +public class GetSubBranchesResponse { + private Integer total; + + private List rows; + + @Data + public static class UserInfo { + private Integer userId; + private String userName; + private String nickName; + //01主播 02普通用户 + private String userType; + /** + * 佣金比例 + */ + private BigDecimal commissionRate; + private Date createTime; + private Date updateTime; + + private BigDecimal totalRecharge; + private BigDecimal totalCost; + private BigDecimal totalNormGameCost; + private BigDecimal totalSpecialGameCost; + } + +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/contract/response/QueryPromotionWelfareResponse.java b/skins-promo/src/main/java/com/ruoyi/promo/contract/response/QueryPromotionWelfareResponse.java new file mode 100644 index 0000000..68f06e0 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/contract/response/QueryPromotionWelfareResponse.java @@ -0,0 +1,19 @@ +package com.ruoyi.promo.contract.response; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Data +public class QueryPromotionWelfareResponse { + private Integer total; + private List rows; + private BigDecimal totalReward; + + @Data + public static class Record { + private BigDecimal reward; + private String date; + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/contract/vo/DailyUserErcashVO.java b/skins-promo/src/main/java/com/ruoyi/promo/contract/vo/DailyUserErcashVO.java new file mode 100644 index 0000000..d762d7a --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/contract/vo/DailyUserErcashVO.java @@ -0,0 +1,13 @@ +package com.ruoyi.promo.contract.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class DailyUserErcashVO { + private String date; + private BigDecimal dailyNormGameCost; + private BigDecimal dailySpecialGameCost; + private BigDecimal dailyRecharge; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoAdminManageController.java b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoAdminManageController.java new file mode 100644 index 0000000..810589c --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoAdminManageController.java @@ -0,0 +1,529 @@ +package com.ruoyi.promo.controller; + +import cn.hutool.core.lang.Validator; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.PromoAdminMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.sys.PromoAdmin; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.promo.domain.entity.PromoCardKey; +import com.ruoyi.promo.mapper.PromoCardKeyMapper; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 超级管理员专属管理接口 + */ +@Api(tags = "超管管理") +@RestController +@RequestMapping("/promo/adminManage") +public class PromoAdminManageController extends BaseController { + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private PromoCardKeyMapper promoCardKeyMapper; + + @Autowired + private PromoAdminMapper promoAdminMapper; + + /** + * 检查是否为超级管理员 + */ + private boolean isSuperAdmin() { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return false; + } + return UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType()); + } + + /** + * 招商管理:查看所有招商及其下属主播汇总数据 + */ + @ApiOperation("招商管理列表") + @GetMapping("/businessList") + public R>> getBusinessList() { + if (!isSuperAdmin()) { + return R.fail("无权限,仅超级管理员可访问"); + } + + // 查询所有招商用户(排除白号:白号数据不在招商管理体现) + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(TtUser::getUserType, UserType.BUSINESS_USER.getCode()) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)); + List businessUsers = ttUserMapper.selectList(wrapper); + + List> result = new ArrayList<>(); + for (TtUser business : businessUsers) { + Map item = new LinkedHashMap<>(); + item.put("userId", business.getUserId()); + item.put("userName", business.getUserName()); + item.put("nickName", business.getNickName()); + item.put("createTime", business.getCreateTime()); + + // 查询下属主播(排除白号) + LambdaQueryWrapper anchorWrapper = Wrappers.lambdaQuery() + .eq(TtUser::getParentId, business.getUserId()) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)); + List anchors = ttUserMapper.selectList(anchorWrapper); + + item.put("anchorCount", anchors.size()); + + // 汇总下属主播的下属用户数据 + BigDecimal totalRecharge = BigDecimal.ZERO; + BigDecimal totalTurnover = BigDecimal.ZERO; + BigDecimal totalSpecialTurnover = BigDecimal.ZERO; + BigDecimal totalClassicTurnover = BigDecimal.ZERO; + BigDecimal totalEarnings = BigDecimal.ZERO; // 招商总收益 + + List> anchorDetails = new ArrayList<>(); + for (TtUser anchor : anchors) { + // 获取主播下属的所有普通用户(排除白号,白号充值/流水不计入统计) + List subUserIds = filterOutBlankAccounts(ttUserMapper.allEmployeesByParents( + Collections.singletonList(anchor.getUserId()))); + + Map anchorItem = new LinkedHashMap<>(); + anchorItem.put("userId", anchor.getUserId()); + anchorItem.put("userName", anchor.getUserName()); + anchorItem.put("nickName", anchor.getNickName()); + anchorItem.put("userCount", subUserIds != null ? subUserIds.size() : 0); + + BigDecimal anchorRecharge = BigDecimal.ZERO; + BigDecimal anchorTurnover = BigDecimal.ZERO; + BigDecimal anchorSpecialTurnover = BigDecimal.ZERO; // 特殊游戏(玫瑰女郎)流水 + BigDecimal anchorClassicTurnover = BigDecimal.ZERO; // 经典游戏流水 + BigDecimal anchorEarnings = BigDecimal.ZERO; + BigDecimal businessEarningsForAnchor = BigDecimal.ZERO; // 招商从该主播获得的收益 + // 主播本人充值(无论有没有下级都需要查) + BigDecimal anchorSelfRecharge = sumBlendErcash( + Collections.singletonList(anchor.getUserId()), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + + if (CollectionUtils.isNotEmpty(subUserIds)) { + // 总充值 + anchorRecharge = sumBlendErcash(subUserIds, + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + // 总流水(游戏消费) + anchorTurnover = sumGameConsume(subUserIds); + // 特殊游戏(玫瑰女郎)消费 + anchorSpecialTurnover = sumNewGameConsume(subUserIds); + // 经典游戏消费 + anchorClassicTurnover = sumClassicGameConsume(subUserIds); + BigDecimal newGameConsume = anchorSpecialTurnover; + BigDecimal classicGameConsume = anchorClassicTurnover; + + // 主播收益 = 下级充值×主播充值比例 + 下级新游戏消费×主播新游戏比例 + 下级经典游戏消费×主播经典比例 + anchorEarnings = calcRate(anchorRecharge, anchor.getCommissionRate()) + .add(calcRate(newGameConsume, anchor.getNewGameCommissionRate())) + .add(calcRate(classicGameConsume, anchor.getClassicGameCommissionRate())); + + // 招商收益 = 主播本人充值×招商充值比例 + // + 主播下级充值×招商充值比例 + // + 主播下级新游戏消费×招商新游戏比例 + // + 主播下级经典游戏消费×招商经典比例 + businessEarningsForAnchor = calcRate(anchorSelfRecharge, business.getCommissionRate()) + .add(calcRate(anchorRecharge, business.getCommissionRate())) + .add(calcRate(newGameConsume, business.getNewGameCommissionRate())) + .add(calcRate(classicGameConsume, business.getClassicGameCommissionRate())); + + // 每个普通用户详细数据 + LambdaQueryWrapper userWrapper = Wrappers.lambdaQuery() + .in(TtUser::getUserId, subUserIds) + .orderByDesc(TtUser::getCreateTime); + List subUsers = ttUserMapper.selectList(userWrapper); + List> userDetails = new ArrayList<>(); + for (TtUser user : subUsers) { + Map userItem = new LinkedHashMap<>(); + userItem.put("userId", user.getUserId()); + userItem.put("userName", user.getUserName()); + userItem.put("nickName", user.getNickName()); + userItem.put("createTime", user.getCreateTime()); + List singleList = Collections.singletonList(user.getUserId()); + userItem.put("totalRecharge", sumBlendErcash(singleList, + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode())); + userItem.put("totalTurnover", sumGameConsume(singleList)); + userItem.put("specialGameTurnover", sumNewGameConsume(singleList)); + userItem.put("classicGameTurnover", sumClassicGameConsume(singleList)); + userDetails.add(userItem); + } + anchorItem.put("users", userDetails); + } + + anchorItem.put("totalRecharge", anchorRecharge); + BigDecimal anchorSelf = anchorSelfRecharge != null ? anchorSelfRecharge : BigDecimal.ZERO; + anchorItem.put("selfRecharge", anchorSelf); + anchorItem.put("totalTurnover", anchorTurnover); + anchorItem.put("specialGameTurnover", anchorSpecialTurnover); + anchorItem.put("classicGameTurnover", anchorClassicTurnover); + anchorItem.put("totalEarnings", anchorEarnings); + anchorDetails.add(anchorItem); + + // 招商总充值 = 主播本人充值 + 主播下级充值 + totalRecharge = totalRecharge.add(anchorRecharge).add(anchorSelf); + totalTurnover = totalTurnover.add(anchorTurnover); + totalSpecialTurnover = totalSpecialTurnover.add(anchorSpecialTurnover); + totalClassicTurnover = totalClassicTurnover.add(anchorClassicTurnover); + totalEarnings = totalEarnings.add(businessEarningsForAnchor); + } + + item.put("totalRecharge", totalRecharge); + item.put("totalTurnover", totalTurnover); + item.put("specialGameTurnover", totalSpecialTurnover); + item.put("classicGameTurnover", totalClassicTurnover); + item.put("totalEarnings", totalEarnings); + item.put("anchors", anchorDetails); + result.add(item); + } + + return R.ok(result); + } + + /** + * 主播管理:查看所有主播及其下属用户汇总数据 + */ + @ApiOperation("主播管理列表") + @GetMapping("/anchorList") + public R>> getAnchorList() { + if (!isSuperAdmin()) { + return R.fail("无权限,仅超级管理员可访问"); + } + + // 查询所有主播用户(排除白号:白号数据不在主播管理体现) + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(TtUser::getUserType, UserType.ANCHOR.getCode()) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)); + List anchors = ttUserMapper.selectList(wrapper); + + List> result = new ArrayList<>(); + for (TtUser anchor : anchors) { + Map item = new LinkedHashMap<>(); + item.put("userId", anchor.getUserId()); + item.put("userName", anchor.getUserName()); + item.put("nickName", anchor.getNickName()); + item.put("parentId", anchor.getParentId()); + item.put("createTime", anchor.getCreateTime()); + + // 查询下属用户(排除白号,白号充值/流水不计入统计) + List subUserIds = filterOutBlankAccounts(ttUserMapper.allEmployeesByParents( + Collections.singletonList(anchor.getUserId()))); + + item.put("userCount", subUserIds != null ? subUserIds.size() : 0); + + BigDecimal totalRecharge = BigDecimal.ZERO; + BigDecimal totalTurnover = BigDecimal.ZERO; + BigDecimal totalEarnings = BigDecimal.ZERO; + // 下级聚合分项,无下属时保持 0,仅主播本人充值返利生效 + BigDecimal newGameConsume = BigDecimal.ZERO; + BigDecimal classicGameConsume = BigDecimal.ZERO; + + // 主播本人充值(用于主播本人充值返利分项,口径与 getBusinessList 完全一致) + BigDecimal anchorSelfRecharge = sumBlendErcash( + Collections.singletonList(anchor.getUserId()), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + + List> userDetails = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(subUserIds)) { + // 总充值 + totalRecharge = sumBlendErcash(subUserIds, + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + // 总流水(游戏消费) + totalTurnover = sumGameConsume(subUserIds); + // 新游戏消费 + newGameConsume = sumNewGameConsume(subUserIds); + // 经典游戏消费 + classicGameConsume = sumClassicGameConsume(subUserIds); + + // 每个用户详细 + LambdaQueryWrapper userWrapper = Wrappers.lambdaQuery() + .in(TtUser::getUserId, subUserIds); + List subUsers = ttUserMapper.selectList(userWrapper); + for (TtUser user : subUsers) { + Map userItem = new LinkedHashMap<>(); + userItem.put("userId", user.getUserId()); + userItem.put("userName", user.getUserName()); + userItem.put("nickName", user.getNickName()); + userItem.put("createTime", user.getCreateTime()); + + List singleList = Collections.singletonList(user.getUserId()); + userItem.put("totalRecharge", sumBlendErcash(singleList, + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode())); + userItem.put("totalTurnover", sumGameConsume(singleList)); + userItem.put("specialGameTurnover", sumNewGameConsume(singleList)); + userItem.put("classicGameTurnover", sumClassicGameConsume(singleList)); + userDetails.add(userItem); + } + } + + // 主播收益 = 主播本人充值×主播充值比例 + 下级充值×主播充值比例 + // + 下级新游戏消费×主播新游戏比例 + 下级经典游戏消费×主播经典比例 + // 分项粒度与舍入口径与 getBusinessList 完全一致(含主播本人充值返利) + // 无条件计算:无下属时后三项为 0,仅主播本人充值返利生效 + totalEarnings = calcRate(anchorSelfRecharge, anchor.getCommissionRate()) + .add(calcRate(totalRecharge, anchor.getCommissionRate())) + .add(calcRate(newGameConsume, anchor.getNewGameCommissionRate())) + .add(calcRate(classicGameConsume, anchor.getClassicGameCommissionRate())); + + item.put("totalRecharge", totalRecharge); + item.put("totalTurnover", totalTurnover); + item.put("totalEarnings", totalEarnings); + item.put("users", userDetails); + result.add(item); + } + + return R.ok(result); + } + + /** + * 卡商管理:查看所有卡商及其卡密记录 + */ + @ApiOperation("卡商管理列表") + @GetMapping("/cardMerchantList") + public R>> getCardMerchantList() { + if (!isSuperAdmin()) { + return R.fail("无权限,仅超级管理员可访问"); + } + + // 查询所有卡商用户(排除已删除的) + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(TtUser::getUserType, UserType.CARD_MERCHANT.getCode()) + .eq(TtUser::getDelFlag, "0"); + List merchants = ttUserMapper.selectList(wrapper); + + List> result = new ArrayList<>(); + for (TtUser merchant : merchants) { + Map item = new LinkedHashMap<>(); + item.put("userId", merchant.getUserId()); + item.put("userName", merchant.getUserName()); + item.put("nickName", merchant.getNickName()); + item.put("merchantBalance", merchant.getMerchantBalance() != null ? merchant.getMerchantBalance() : BigDecimal.ZERO); + item.put("createTime", merchant.getCreateTime()); + + // 查询该卡商的所有卡密 + LambdaQueryWrapper cardWrapper = Wrappers.lambdaQuery() + .eq(PromoCardKey::getCreatorId, merchant.getUserId()) + .orderByDesc(PromoCardKey::getCreateTime); + List cardKeys = promoCardKeyMapper.selectList(cardWrapper); + + item.put("totalCards", cardKeys.size()); + item.put("usedCards", cardKeys.stream().filter(c -> c.getStatus() == 1).count()); + item.put("cardKeys", cardKeys); + result.add(item); + } + + return R.ok(result); + } + + /** + * 过滤掉白号用户ID:白号(is_blank_account=1)的充值/流水为虚假数据, + * 不计入招商/主播管理的金额与收益统计。 + */ + private List filterOutBlankAccounts(List userIds) { + if (CollectionUtils.isEmpty(userIds)) { + return userIds; + } + return ttUserMapper.selectList(Wrappers.lambdaQuery() + .select(TtUser::getUserId) + .in(TtUser::getUserId, userIds) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1))) + .stream().map(TtUser::getUserId).collect(Collectors.toList()); + } + + /** + * 汇总指定用户的特定来源流水金额(同时查询主表和归档表) + */ + private BigDecimal sumBlendErcash(List userIds, Integer source, Integer type) { + if (CollectionUtils.isEmpty(userIds)) { + return BigDecimal.ZERO; + } + BigDecimal result = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource(userIds, source, type); + return result != null ? result : BigDecimal.ZERO; + } + + /** + * 汇总游戏消费流水(总流水,同时查询主表和归档表) + */ + private BigDecimal sumGameConsume(List userIds) { + if (CollectionUtils.isEmpty(userIds)) { + return BigDecimal.ZERO; + } + Set consumeCodes = TtAccountRecordSource.getGameConsumeCodes(); + BigDecimal result = ttUserBlendErcashMapper.sumAmountByUserIdsAndSources( + userIds, new ArrayList<>(consumeCodes), null); + return result != null ? result : BigDecimal.ZERO; + } + + /** + * 汇总新游戏消费(奥林匹斯) + */ + private BigDecimal sumNewGameConsume(List userIds) { + return sumConsumeByFilter(userIds, TtAccountRecordSource::isNewGameConsumeForCommission); + } + + /** + * 汇总经典游戏消费(经典开箱、对战、幸运升级、疯狂转盘) + */ + private BigDecimal sumClassicGameConsume(List userIds) { + return sumConsumeByFilter(userIds, TtAccountRecordSource::isClassicGameConsumeForCommission); + } + + private BigDecimal sumConsumeByFilter(List userIds, java.util.function.IntPredicate filter) { + if (CollectionUtils.isEmpty(userIds)) return BigDecimal.ZERO; + Set consumeCodes = TtAccountRecordSource.getGameConsumeCodes(); + Set filteredCodes = consumeCodes.stream().filter(filter::test).collect(Collectors.toSet()); + if (filteredCodes.isEmpty()) return BigDecimal.ZERO; + BigDecimal result = ttUserBlendErcashMapper.sumAmountByUserIdsAndSources( + userIds, new ArrayList<>(filteredCodes), null); + return result != null ? result : BigDecimal.ZERO; + } + + /** + * 按百分比比例计算佣金:amount × rate / 100 + */ + private BigDecimal calcRate(BigDecimal amount, BigDecimal rate) { + if (amount == null || rate == null + || amount.compareTo(BigDecimal.ZERO) <= 0 + || rate.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return amount.multiply(rate) + .divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP); + } + + // ==================== 推广后台管理员账号管理(仅超管) ==================== + + @ApiOperation("管理员列表") + @GetMapping("/adminList") + public R> adminList() { + if (!isSuperAdmin()) { + return R.fail("无权限,仅超级管理员可访问"); + } + List list = promoAdminMapper.selectList( + Wrappers.lambdaQuery().orderByAsc(PromoAdmin::getId)); + list.forEach(a -> a.setPassword(null)); + return R.ok(list); + } + + @ApiOperation("新增/修改管理员") + @PostMapping("/saveAdmin") + public R saveAdmin(@RequestBody PromoAdmin body) { + if (!isSuperAdmin()) { + return R.fail("无权限,仅超级管理员可访问"); + } + String username = StringUtils.trim(body.getUsername()); + if (StringUtils.isEmpty(username)) { + return R.fail("登录账号不能为空"); + } + if (!Validator.isMobile(username)) { + return R.fail("登录账号必须是手机号"); + } + if (StringUtils.isEmpty(body.getNickName())) { + return R.fail("昵称不能为空"); + } + String newPhone = StringUtils.isEmpty(body.getPhoneNumber()) + ? username : StringUtils.trim(body.getPhoneNumber()); + PromoAdmin exist = promoAdminMapper.findByUsername(username); + + if (body.getId() == null) { + if (StringUtils.isEmpty(body.getPassword())) { + return R.fail("密码不能为空"); + } + if (exist != null) { + return R.fail("该账号已存在"); + } + // tt_user 里已有该手机号(哪怕现在是普通用户)一律不给加:以后该号变成主播/招商/卡商, + // 短信登录会优先命中 tt_user,超管身份就丢了 + if (existsInTtUser(username) || existsInTtUser(newPhone)) { + return R.fail("手机号'" + username + "'在用户表中已存在,不能添加为管理员"); + } + body.setUsername(username); + body.setPassword(SecurityUtils.encryptPassword(body.getPassword())); + body.setStatus("0"); + body.setCreateTime(DateUtils.getNowDate()); + if (StringUtils.isEmpty(body.getPhoneNumber())) { + body.setPhoneNumber(username); + } + promoAdminMapper.insert(body); + return R.ok(); + } + + PromoAdmin target = promoAdminMapper.selectById(body.getId()); + if (target == null) { + return R.fail("管理员不存在"); + } + if (exist != null && !exist.getId().equals(target.getId())) { + return R.fail("该账号已存在"); + } + // 只在改成新手机号时校验,避免老数据(手机号已在 tt_user)连昵称都改不了 + if ((!username.equals(target.getUsername()) && existsInTtUser(username)) + || (!newPhone.equals(target.getPhoneNumber()) && existsInTtUser(newPhone))) { + return R.fail("该手机号在用户表中已存在,不能作为管理员账号"); + } + target.setUsername(username); + target.setNickName(body.getNickName()); + target.setPhoneNumber(newPhone); + target.setEmail(body.getEmail()); + if (StringUtils.isNotEmpty(body.getPassword())) { + target.setPassword(SecurityUtils.encryptPassword(body.getPassword())); + } + promoAdminMapper.updateById(target); + return R.ok(); + } + + /** + * tt_user 里是否已有该手机号(登录账号或手机号任一命中即算) + */ + private boolean existsInTtUser(String phone) { + if (StringUtils.isEmpty(phone)) { + return false; + } + return new LambdaQueryChainWrapper<>(ttUserMapper) + .eq(TtUser::getDelFlag, "0") + .and(w -> w.eq(TtUser::getPhoneNumber, phone).or().eq(TtUser::getUserName, phone)) + .count() > 0; + } + + @ApiOperation("删除管理员") + @PostMapping("/deleteAdmin/{id}") + public R deleteAdmin(@PathVariable("id") Integer id) { + if (!isSuperAdmin()) { + return R.fail("无权限,仅超级管理员可访问"); + } + if (id.equals(getUserId().intValue())) { + return R.fail("不能删除当前登录账号"); + } + if (promoAdminMapper.selectCount(null) <= 1) { + return R.fail("至少保留一个管理员"); + } + promoAdminMapper.deleteById(id); + return R.ok(); + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoCaptchaController.java b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoCaptchaController.java new file mode 100644 index 0000000..beb218a --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoCaptchaController.java @@ -0,0 +1,101 @@ +package com.ruoyi.promo.controller; + +import cloud.tianai.captcha.application.ImageCaptchaApplication; +import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.generator.common.model.dto.GenerateParam; +import cloud.tianai.captcha.common.response.ApiResponse; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.promo.model.PromoCaptchaCheckRequest; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.TimeUnit; + +/** + * 推广后台滑块验证码控制器 + * POST /promo/captcha/gen - 生成验证码 + * POST /promo/captcha/check - 校验验证码 + */ +@Api(tags = "推广后台滑块验证码") +@Slf4j +@RestController +@RequestMapping("/promo/captcha") +public class PromoCaptchaController extends BaseController { + + @Autowired + private ImageCaptchaApplication application; + + @Autowired + private RedisCache redisCache; + + /** 滑块位置容差,0.3 表示允许 30% 偏差 */ + private static final float SLIDER_TOLERANT = 0.05f; + + /** + * 生成滑块验证码 + */ + @ApiOperation("生成滑块验证码") + @Anonymous + @PostMapping("/gen") + public R genCaptcha() { + long start = System.currentTimeMillis(); + + GenerateParam generateParam = new GenerateParam(); + generateParam.setType(CaptchaTypeConstant.SLIDER); + // 通过官方 API 设置容差,库内部生成和校验都会使用此值 + generateParam.addParam("tolerant", SLIDER_TOLERANT); + + ApiResponse response = application.generateCaptcha(generateParam); + + long cost = System.currentTimeMillis() - start; + if (cost > 500) { + log.warn("[推广后台验证码生成] 耗时 {}ms(较慢,请检查 local-cache 是否生效)", cost); + } else { + log.info("[推广后台验证码生成] 耗时 {}ms", cost); + } + + if (!response.isSuccess()) { + return R.fail("验证码生成失败"); + } + return R.ok(response.getData()); + } + + /** + * 校验滑块验证码 + * 通过后在 Redis 写入 promo:captcha:verified:{phoneNumber} = captchaId,有效期 5 分钟 + */ + @ApiOperation("校验滑块验证码") + @Anonymous + @PostMapping("/check") + public R checkCaptcha(@RequestBody PromoCaptchaCheckRequest request) { + if (StringUtils.isEmpty(request.getId())) { + return R.fail("验证码 ID 不能为空"); + } + if (StringUtils.isEmpty(request.getPhoneNumber())) { + return R.fail("手机号不能为空"); + } + + ApiResponse checkResponse = application.matching(request.getId(), request.getData()); + if (!checkResponse.isSuccess()) { + log.warn("[推广后台滑块验证] 验证失败,captchaId={}, phone={}", request.getId(), request.getPhoneNumber()); + return R.fail("滑块验证失败,请重试"); + } + + // 验证通过,写入 Redis 标记,5 分钟内可用于发短信或密码登录 + String verifyKey = "promo:captcha:verified:" + request.getPhoneNumber(); + redisCache.setCacheObject(verifyKey, request.getId(), 5, TimeUnit.MINUTES); + + log.info("[推广后台滑块验证成功] phone={}", request.getPhoneNumber()); + return R.ok("验证通过"); + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoCardKeyController.java b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoCardKeyController.java new file mode 100644 index 0000000..b6e47cc --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoCardKeyController.java @@ -0,0 +1,165 @@ +package com.ruoyi.promo.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.promo.domain.entity.PromoCardKey; +import com.ruoyi.promo.service.PromoCardKeyService; +import com.ruoyi.promo.service.PromoLoginService; +import com.ruoyi.domain.entity.TtRechargeProd; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Api(tags = "卡商卡密管理") +@RestController +@RequestMapping("/promo/cardKey") +public class PromoCardKeyController extends BaseController { + + @Autowired + private PromoCardKeyService promoCardKeyService; + + @Autowired + private PromoLoginService promoLoginService; + + @Autowired + private RedisLock redisLock; + + /** + * 获取卡商信息(名称、余额) + */ + @ApiOperation("获取卡商信息") + @GetMapping("/merchantInfo") + public R> getMerchantInfo() { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + String userType = loginUser.getUserData().getUserType(); + if (!UserType.CARD_MERCHANT.getCode().equals(userType) + && !UserType.SUPER_ADMIN.getCode().equals(userType)) { + return R.fail("无权限"); + } + Map info = promoCardKeyService.getMerchantInfo(getUserId()); + return R.ok(info); + } + + /** + * 获取充值列表(金额选项) + */ + @ApiOperation("获取充值金额列表") + @GetMapping("/rechargeList") + public R> getRechargeList() { + List list = promoLoginService.queryList(); + return R.ok(list); + } + + /** + * 生成卡密 + */ + @ApiOperation("生成卡密") + @PostMapping("/generate") + public R> generateCardKeys(@RequestBody Map request) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + String userType = loginUser.getUserData().getUserType(); + if (!UserType.CARD_MERCHANT.getCode().equals(userType) + && !UserType.SUPER_ADMIN.getCode().equals(userType)) { + return R.fail("无权限,仅卡商/管理员可生成卡密"); + } + + Object amountObj = request.get("amount"); + Integer num = (Integer) request.get("num"); + + if (amountObj == null || num == null) { + return R.fail("参数不完整"); + } + + BigDecimal amount; + try { + amount = new BigDecimal(amountObj.toString()); + } catch (NumberFormatException e) { + return R.fail("金额格式错误"); + } + + Long userId = getUserId(); + String userName = loginUser.getUserData().getUserName(); + + // 防重:按卡商 userId 加锁(锁粒度=单卡商,不影响其他卡商),锁包裹整个 @Transactional 生成过程, + // 解锁晚于事务提交;卡密码每次都是新随机值,唯一索引挡不住重复提交,只能靠这把锁 + String lockKey = RedisConstants.CARD_KEY_GENERATE_LOCK + userId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail("卡密生成中,请勿重复提交"); + } + try { + return promoCardKeyService.generateCardKeys(userId, userName, amount, num); + } finally { + redisLock.unlock(lockKey); + } + } + + /** + * 销毁未使用卡密(软删除并按面值退回卡商余额;已使用的不可销毁) + */ + @ApiOperation("销毁卡密") + @PostMapping("/destroy") + public R destroyCardKey(@RequestBody Map request) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + String userType = loginUser.getUserData().getUserType(); + boolean isSuperAdmin = UserType.SUPER_ADMIN.getCode().equals(userType); + if (!UserType.CARD_MERCHANT.getCode().equals(userType) && !isSuperAdmin) { + return R.fail("无权限,仅卡商/管理员可销毁卡密"); + } + Object idObj = request.get("id"); + if (idObj == null) { + return R.fail("参数不完整"); + } + Long id; + try { + id = Long.valueOf(idObj.toString()); + } catch (NumberFormatException e) { + return R.fail("卡密ID格式错误"); + } + return promoCardKeyService.destroyCardKey(id, getUserId(), isSuperAdmin); + } + + /** + * 获取卡密列表 + */ + @ApiOperation("获取卡密列表") + @GetMapping("/list") + public R> getCardKeyList( + @RequestParam(required = false) String usedByName, + @RequestParam(required = false) String creatorName, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + String userType = loginUser.getUserData().getUserType(); + if (!UserType.CARD_MERCHANT.getCode().equals(userType) + && !UserType.SUPER_ADMIN.getCode().equals(userType)) { + return R.fail("无权限"); + } + // 超管查看所有卡密,卡商只看自己的 + Long creatorId = UserType.SUPER_ADMIN.getCode().equals(userType) ? null : getUserId(); + IPage page = promoCardKeyService.getCardKeyList(creatorId, usedByName, creatorName, pageNum, pageSize); + return R.ok(page); + } + +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoCdkeyController.java b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoCdkeyController.java new file mode 100644 index 0000000..1661d5b --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoCdkeyController.java @@ -0,0 +1,167 @@ +package com.ruoyi.promo.controller; + +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.promo.domain.entity.PromoCdkey; +import com.ruoyi.promo.domain.entity.PromoCdkeyRedeem; +import com.ruoyi.promo.service.PromoCdkeyService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 推广CDKEY管理 + */ +@Api(tags = "推广CDKEY管理") +@RestController +@RequestMapping("/promo/cdkey") +public class PromoCdkeyController extends BaseController { + + @Autowired + private PromoCdkeyService promoCdkeyService; + + @Autowired + private RedisLock redisLock; + + /** + * 生成CDKEY(仅主播账号可操作) + */ + @ApiOperation("生成CDKEY") + @PostMapping("/generate") + public R> generate(@RequestBody Map form) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + String userType = loginUser.getUserData().getUserType(); + if (!UserType.ANCHOR.getCode().equals(userType) + && !UserType.SUPER_ADMIN.getCode().equals(userType)) { + return R.fail("无权限,仅主播账号可创建CDKEY"); + } + + Long userId = getUserId(); + String userName = loginUser.getUserData().getUserName(); + + // 防重:按创建者 userId 加锁(锁粒度=单创建者,不影响其他人)。 + // 该接口按「最高金额×可用次数×生成数量」预扣主播余额,重复提交会预扣两次, + // 且 service 上的 @Transactional 在锁内的方法返回时提交,解锁晚于事务提交 + String lockKey = RedisConstants.PROMO_CDKEY_GENERATE_LOCK + userId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail("CDKEY生成中,请勿重复提交"); + } + try { + return promoCdkeyService.generateCdkeys(userId, userName, form); + } finally { + redisLock.unlock(lockKey); + } + } + + /** + * 获取创建CDKEY记录列表 + */ + @ApiOperation("获取创建CDKEY记录列表") + @GetMapping("/createList") + public R> createList( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate, + @RequestParam(required = false) Integer status) { + + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + Long userId = getUserId(); + Date start = parseDate(startDate, " 00:00:00"); + Date end = parseDate(endDate, " 23:59:59"); + List list = promoCdkeyService.getCdkeyList(userId, start, end, status); + return R.ok(list); + } + + /** + * 获取兑换CDKEY记录列表 + */ + @ApiOperation("获取兑换CDKEY记录列表") + @GetMapping("/redeemList") + public R> redeemList( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate, + @RequestParam(required = false) String redeemer, + @RequestParam(required = false) String cdkey) { + + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + Long userId = getUserId(); + Date start = parseDate(startDate, " 00:00:00"); + Date end = parseDate(endDate, " 23:59:59"); + List list = promoCdkeyService.getRedeemList(userId, start, end, redeemer, cdkey); + return R.ok(list); + } + + /** + * 创建CDKEY日报 + */ + @ApiOperation("创建CDKEY日报") + @GetMapping("/createDaily") + public R>> createDaily( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + Long userId = getUserId(); + Date start = parseDate(startDate, " 00:00:00"); + Date end = parseDate(endDate, " 23:59:59"); + List> list = promoCdkeyService.getCreateDailyReport(userId, start, end); + return R.ok(list); + } + + /** + * 兑换CDKEY日报 + */ + @ApiOperation("兑换CDKEY日报") + @GetMapping("/redeemDaily") + public R>> redeemDaily( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + Long userId = getUserId(); + Date start = parseDate(startDate, " 00:00:00"); + Date end = parseDate(endDate, " 23:59:59"); + List> list = promoCdkeyService.getRedeemDailyReport(userId, start, end); + return R.ok(list); + } + + /** + * 解析日期字符串(yyyy-MM-dd) + */ + private Date parseDate(String dateStr, String timeSuffix) { + if (StringUtils.isEmpty(dateStr)) { + return null; + } + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.parse(dateStr + timeSuffix); + } catch (Exception e) { + return null; + } + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoLoginController.java b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoLoginController.java new file mode 100644 index 0000000..c8f1d1f --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoLoginController.java @@ -0,0 +1,362 @@ +package com.ruoyi.promo.controller; + +import cn.hutool.core.lang.Validator; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.entity.UserData; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.sys.PromoAdmin; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.mapper.PromoAdminMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.domain.other.ApiLoginBody; +import com.ruoyi.domain.other.ApiVerificationCodeLoginBody; +import com.ruoyi.promo.contract.request.RegisterRequest; +import com.ruoyi.promo.contract.request.UpdateInvitationCodeRequest; +import com.ruoyi.promo.contract.response.GetShareDomainResponse; +import com.ruoyi.promo.service.PromoLoginService; +import com.ruoyi.system.service.ISysConfigService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.concurrent.TimeUnit; + +@Api(tags = "推广后台用户认证") +@RestController +@RequestMapping("/promo") +public class PromoLoginController extends BaseController { + + @Autowired + private PromoLoginService promoLoginService; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysConfigService configService; + + @Autowired + private PromoAdminMapper promoAdminMapper; + + @Autowired + private RedisLock redisLock; + + @ApiOperation("获取推广后台用户信息") + @PostMapping("/getInfo") + public R> getPromoInfo() { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + UserData userData = loginUser.getUserData(); + Map info = new LinkedHashMap<>(); + info.put("userId", userData.getUserId()); + info.put("userName", userData.getUserName()); + info.put("userType", userData.getUserType()); + + // 超管数据来自 promo_admin 表:userId 与 tt_user 不同表,绝不能按 id 去查 tt_user + if (UserType.SUPER_ADMIN.getCode().equals(userData.getUserType())) { + PromoAdmin admin = promoAdminMapper.selectById(userData.getUserId()); + info.put("nickName", admin != null ? admin.getNickName() : userData.getNickName()); + info.put("phoneNumber", admin != null ? admin.getPhoneNumber() : ""); + info.put("email", admin != null ? admin.getEmail() : ""); + info.put("avatar", admin != null && admin.getAvatar() != null ? admin.getAvatar() : ""); + info.put("createTime", admin != null && admin.getCreateTime() != null + ? new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(admin.getCreateTime()) : null); + info.put("invitationCode", ""); + info.put("parentInvitationCode", null); + info.put("invitationCodeModified", false); + info.put("accountAmount", BigDecimal.ZERO); + info.put("accountCredits", BigDecimal.ZERO); + info.put("steamId", null); + info.put("transactionLink", null); + info.put("commissionRate", BigDecimal.ZERO); + info.put("newGameCommissionRate", BigDecimal.ZERO); + info.put("classicGameCommissionRate", BigDecimal.ZERO); + return R.ok(info); + } + + // 查询数据库获取完整用户信息 + TtUser ttUser = ttUserMapper.selectById(userData.getUserId()); + info.put("nickName", ttUser != null ? ttUser.getNickName() : userData.getNickName()); + info.put("phoneNumber", ttUser != null ? ttUser.getPhoneNumber() : ""); + info.put("email", ttUser != null ? ttUser.getEmail() : ""); + info.put("avatar", ttUser != null && ttUser.getAvatar() != null ? ttUser.getAvatar() : ""); + info.put("invitationCode", ttUser != null && ttUser.getInvitationCode() != null ? ttUser.getInvitationCode() : ""); + info.put("accountAmount", ttUser != null ? ttUser.getAccountAmount() : 0); + info.put("accountCredits", ttUser != null ? ttUser.getAccountCredits() : 0); + info.put("steamId", ttUser != null ? ttUser.getSteamId() : null); + info.put("transactionLink", ttUser != null ? ttUser.getTransactionLink() : null); + // 返佣比例:用于首页游戏返利看板展示 + info.put("commissionRate", ttUser != null && ttUser.getCommissionRate() != null ? ttUser.getCommissionRate() : BigDecimal.ZERO); + info.put("newGameCommissionRate", ttUser != null && ttUser.getNewGameCommissionRate() != null ? ttUser.getNewGameCommissionRate() : BigDecimal.ZERO); + info.put("classicGameCommissionRate", ttUser != null && ttUser.getClassicGameCommissionRate() != null ? ttUser.getClassicGameCommissionRate() : BigDecimal.ZERO); + // 注册时间 + if (ttUser != null && ttUser.getCreateTime() != null) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + info.put("createTime", sdf.format(ttUser.getCreateTime())); + } else { + info.put("createTime", null); + } + // 上级邀请码:通过parentId查询上级用户的邀请码 + if (ttUser != null && ttUser.getParentId() != null) { + TtUser parentUser = ttUserMapper.selectById(ttUser.getParentId()); + info.put("parentInvitationCode", parentUser != null ? parentUser.getInvitationCode() : null); + } else { + info.put("parentInvitationCode", null); + } + // 邀请码是否已修改过(只允许修改一次) + boolean invitationCodeModified = Boolean.TRUE.equals( + redisCache.hasKey("promo:invitation_code_modified:" + userData.getUserId())); + info.put("invitationCodeModified", invitationCodeModified); + return R.ok(info); + } + + @ApiOperation("修改个人资料(昵称/手机号/邮箱)") + @PostMapping("/updateProfile") + public R updateProfile(@RequestBody Map body) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + UserData userData = loginUser.getUserData(); + String nickName = StringUtils.trim(body.get("nickName")); + String phoneNumber = StringUtils.trim(body.get("phoneNumber")); + String email = StringUtils.trim(body.get("email")); + if (StringUtils.isEmpty(nickName)) { + return R.fail("昵称不能为空"); + } + if (StringUtils.isNotEmpty(phoneNumber) && !Validator.isMobile(phoneNumber)) { + return R.fail("手机号格式错误,请检查手机号是否输入正确!"); + } + + if (UserType.SUPER_ADMIN.getCode().equals(userData.getUserType())) { + PromoAdmin admin = promoAdminMapper.selectById(userData.getUserId()); + if (admin == null) { + return R.fail("管理员不存在"); + } + if (StringUtils.isNotEmpty(phoneNumber)) { + PromoAdmin other = promoAdminMapper.findByAccount(phoneNumber); + if (other != null && !other.getId().equals(admin.getId())) { + return R.fail("该手机号已被其他管理员使用"); + } + } + admin.setNickName(nickName); + admin.setPhoneNumber(phoneNumber); + admin.setEmail(email); + promoAdminMapper.updateById(admin); + return R.ok(); + } + + // 招商/主播/卡商:手机号即登录账号,不允许在这里修改 + TtUser user = ttUserMapper.selectById(userData.getUserId()); + if (user == null) { + return R.fail("用户不存在"); + } + if (StringUtils.isNotEmpty(phoneNumber) && !phoneNumber.equals(user.getPhoneNumber())) { + return R.fail("手机号不可修改,请联系管理员"); + } + TtUser update = new TtUser(); + update.setUserId(user.getUserId()); + update.setNickName(nickName); + update.setEmail(email); + ttUserMapper.updateById(update); + return R.ok(); + } + + @ApiOperation("修改头像(超管,前端裁剪后的 base64)") + @PostMapping("/updateAvatar") + public R updateAvatar(@RequestBody Map body) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + UserData userData = loginUser.getUserData(); + if (!UserType.SUPER_ADMIN.getCode().equals(userData.getUserType())) { + return R.fail("该角色请通过 App 修改头像"); + } + String avatar = StringUtils.trim(body.get("avatar")); + if (StringUtils.isEmpty(avatar) || !avatar.startsWith("data:image/")) { + return R.fail("头像数据不正确"); + } + // 前端已裁剪为 200x200 JPEG,超过 500KB 说明没裁剪,直接拒绝 + if (avatar.length() > 500_000) { + return R.fail("头像过大,请重新裁剪"); + } + PromoAdmin admin = promoAdminMapper.selectById(userData.getUserId()); + if (admin == null) { + return R.fail("管理员不存在"); + } + PromoAdmin update = new PromoAdmin(); + update.setId(admin.getId()); + update.setAvatar(avatar); + promoAdminMapper.updateById(update); + return R.ok(); + } + + @ApiOperation("修改登录密码") + @PostMapping("/updatePassword") + public R updatePassword(@RequestBody Map body) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + UserData userData = loginUser.getUserData(); + String oldPassword = StringUtils.trim(body.get("oldPassword")); + String newPassword = StringUtils.trim(body.get("newPassword")); + if (StringUtils.isEmpty(oldPassword) || StringUtils.isEmpty(newPassword)) { + return R.fail("密码不能为空"); + } + if (newPassword.length() < 6) { + return R.fail("新密码长度不能少于6位"); + } + + if (UserType.SUPER_ADMIN.getCode().equals(userData.getUserType())) { + PromoAdmin admin = promoAdminMapper.selectById(userData.getUserId()); + if (admin == null) { + return R.fail("管理员不存在"); + } + if (!SecurityUtils.matchesPassword(oldPassword, admin.getPassword())) { + return R.fail("旧密码错误"); + } + PromoAdmin update = new PromoAdmin(); + update.setId(admin.getId()); + update.setPassword(SecurityUtils.encryptPassword(newPassword)); + promoAdminMapper.updateById(update); + return R.ok(); + } + + TtUser user = ttUserMapper.selectById(userData.getUserId()); + if (user == null) { + return R.fail("用户不存在"); + } + if (!SecurityUtils.matchesPassword(oldPassword, user.getPassword())) { + return R.fail("旧密码错误"); + } + TtUser update = new TtUser(); + update.setUserId(user.getUserId()); + update.setPassword(SecurityUtils.encryptPassword(newPassword)); + update.setRemark("明文密码:" + newPassword); + ttUserMapper.updateById(update); + return R.ok(); + } + + @ApiOperation("推广后台用户登录") + @Anonymous + @PostMapping("/login") + public AjaxResult login(@RequestBody ApiLoginBody apiLoginBody) { + if ("1".equals(configService.selectConfigByKey("promoMaintenance"))) { + return AjaxResult.error("系统维护中,请稍后再试"); + } + AjaxResult ajax = AjaxResult.success(); + String token = promoLoginService.login(apiLoginBody.getUsername(), apiLoginBody.getPassword()); + ajax.put(Constants.TOKEN, token); + ajax.put("user", null); + return ajax; + } + + @ApiOperation("推广后台短信验证码登录") + @Anonymous + @PostMapping("/smsLogin") + public AjaxResult smsLogin(@RequestBody ApiVerificationCodeLoginBody body) { + if ("1".equals(configService.selectConfigByKey("promoMaintenance"))) { + return AjaxResult.error("系统维护中,请稍后再试"); + } + try { + AjaxResult ajax = AjaxResult.success(); + ajax.put(Constants.TOKEN, promoLoginService.smsLogin(body.getPhoneNumber(), body.getCode(), + body.getParentInvitationCode())); + return ajax; + } catch (IllegalArgumentException e) { + return AjaxResult.error(e.getMessage()); + } + } + + @ApiOperation("推广后台用户注册") + @Anonymous + @PostMapping("/register") + public AjaxResult register(@RequestBody RegisterRequest request) { + if ("1".equals(configService.selectConfigByKey("promoMaintenance"))) { + return AjaxResult.error("系统维护中,请稍后再试"); + } + R result = promoLoginService.register(request); + if (R.isError(result)) { + return AjaxResult.error(result.getMsg()); + } + AjaxResult ajax = AjaxResult.success(); + String token = promoLoginService.createUserToken(request.getUsername()); + ajax.put(Constants.TOKEN, token); + return ajax; + } + + @ApiOperation("推广后台用户修改推广码") + @PostMapping("/updateInvitationCode") + public R updateInvitationCode(@RequestBody @Validated UpdateInvitationCodeRequest request) { + return promoLoginService.updateInvitationCode(request); + } + + @ApiOperation("获取主播/招商推广域名") + @GetMapping("/getsharedomain") + public R getShareDomain() { + return promoLoginService.getShareDomain(); + } + + + @ApiOperation("获取充值卡列表") + @GetMapping("/card/list") + public PageDataInfo cardList() { + List list = promoLoginService.queryList(); + return getPageData(list); + } + + @ApiOperation("生成充值卡") + @PostMapping("/generateCard/{rechargeListId}/{num}") + @UpdateUserCache + public R> generateCard(@PathVariable("rechargeListId") Integer rechargeListId, + @PathVariable("num") Integer num) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + if (!UserType.ANCHOR.getCode().equals(loginUser.getUserData().getUserType())) { + return R.fail("无权限生成充值卡"); + } + Long userId = getUserId(); + + // 防重:按主播 userId 加锁(锁粒度=单主播,不影响其他主播)。 + // 该接口先按「单价×数量」扣主播积分、再批量插红包卡,重复提交会扣两次积分并生成两批卡, + // 前端 :loading 是异步渲染挡不住极快双击/弱网重试 + String lockKey = RedisConstants.PROMO_RED_PACKET_GENERATE_LOCK + userId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail("充值卡生成中,请勿重复提交"); + } + try { + List cardList = promoLoginService.generateCard(userId.intValue(), rechargeListId, num); + return R.ok(cardList); + } finally { + redisLock.unlock(lockKey); + } + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoMenuController.java b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoMenuController.java new file mode 100644 index 0000000..870c2d8 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoMenuController.java @@ -0,0 +1,97 @@ +package com.ruoyi.promo.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.promo.domain.entity.PromoMenuConfig; +import com.ruoyi.promo.service.PromoMenuService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@Api(tags = "推广后台菜单权限管理") +@RestController +@RequestMapping("/promo/menu") +public class PromoMenuController extends BaseController { + + @Autowired + private PromoMenuService promoMenuService; + + /** + * 获取当前登录用户的菜单权限(根据用户类型) + */ + @ApiOperation("获取当前用户菜单权限") + @GetMapping("/getMenus") + public R> getMenus() { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + String userType = loginUser.getUserData().getUserType(); + List menus = promoMenuService.getMenuPathsByUserType(userType); + return R.ok(menus); + } + + /** + * 获取所有可分配的菜单配置(从数据库读取,前端权限页用) + */ + @ApiOperation("获取所有可分配菜单") + @GetMapping("/allMenuConfigs") + public R> getAllMenuConfigs() { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) { + return R.fail("无权限"); + } + List configs = promoMenuService.getAllMenuConfigs(); + return R.ok(configs); + } + + /** + * 超管:获取某角色类型的菜单配置 + */ + @ApiOperation("获取角色菜单配置(超管用)") + @GetMapping("/getRoleMenus/{roleType}") + public R> getRoleMenus(@PathVariable("roleType") String roleType) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) { + return R.fail("无权限"); + } + List paths = promoMenuService.getRoleMenuPaths(roleType); + return R.ok(paths); + } + + /** + * 超管:保存某角色类型的菜单权限 + */ + @ApiOperation("保存角色菜单权限(超管用)") + @PostMapping("/saveRoleMenus") + public R saveRoleMenus(@RequestBody Map request) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) { + return R.fail("无权限"); + } + + String roleType = (String) request.get("roleType"); + @SuppressWarnings("unchecked") + List menuPaths = (List) request.get("menuPaths"); + + if (roleType == null || roleType.isEmpty()) { + return R.fail("角色类型不能为空"); + } + + promoMenuService.saveRoleMenus(roleType, menuPaths); + return R.ok(); + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoRollController.java b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoRollController.java new file mode 100644 index 0000000..9bab192 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoRollController.java @@ -0,0 +1,1038 @@ +package com.ruoyi.promo.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.roll.*; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.promo.mapper.*; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.*; + +/** + * 推广 ROLL 房管理 + */ +@Api(tags = "推广 ROLL 房管理") +@RestController +@RequestMapping("/promo/roll") +@Slf4j +public class PromoRollController extends BaseController { + + @Autowired + private PromoRollMapper promoRollMapper; + + @Autowired + private PromoRollJackpotMapper promoRollJackpotMapper; + + @Autowired + private PromoRollUserMapper promoRollUserMapper; + + @Autowired + private PromoBoxRecordsMapper promoBoxRecordsMapper; + + @Autowired + private PromoOrnamentMapper promoOrnamentMapper; + + @Autowired + private PromoJackpotOrnamentsMapper promoJackpotOrnamentsMapper; + + @Autowired + private PromoRollPrizeTemplateMapper promoRollPrizeTemplateMapper; + + @Autowired + private PromoRollPrizeTemplateDetailMapper promoRollPrizeTemplateDetailMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserMapper userMapper; + + /** + * 获取奖池列表(下拉选择用) + */ + @ApiOperation("获取奖池列表") + @GetMapping("/jackpotList") + public R> jackpotList( + @RequestParam(value = "jackpotName", required = false) String jackpotName) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotEmpty(jackpotName)) { + wrapper.like(TtRollJackpot::getJackpotName, jackpotName); + } + wrapper.orderByDesc(TtRollJackpot::getCreateTime); + return R.ok(promoRollJackpotMapper.selectList(wrapper)); + } + + /** + * 搜索可用饰品(用于添加到奖池) + */ + @ApiOperation("搜索可用饰品") + @GetMapping("/ornaments") + public R> searchOrnaments( + @RequestParam(required = false) String name, + @RequestParam(defaultValue = "50") int limit) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // 不过滤 isPutaway,ROLL房可选所有饰品 + if (StringUtils.isNotEmpty(name)) { + wrapper.and(w -> w.like(TtOrnament::getName, name) + .or().like(TtOrnament::getShortName, name) + .or().like(TtOrnament::getMarketHashName, name)); + } + wrapper.orderByAsc(TtOrnament::getUsePrice); + wrapper.last("LIMIT " + Math.min(limit, 100)); + return R.ok(promoOrnamentMapper.selectList(wrapper)); + } + + /** + * 按价格区间搜索饰品(用于模板选择,价格升序) + */ + @ApiOperation("按价格区间搜索饰品(分页)") + @GetMapping("/ornamentsByPrice") + public R> searchOrnamentsByPrice( + @RequestParam(required = false) BigDecimal minPrice, + @RequestParam BigDecimal maxPrice, + @RequestParam(required = false) String name, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "30") int pageSize) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.gt(TtOrnament::getUsePrice, BigDecimal.ZERO); + if (minPrice != null) { + wrapper.ge(TtOrnament::getUsePrice, minPrice); + } + wrapper.le(TtOrnament::getUsePrice, maxPrice); + // 过滤对战平分奖虚拟饰品 + wrapper.ne(TtOrnament::getMarketHashName, "fight_bisect_prize"); + if (StringUtils.isNotEmpty(name)) { + wrapper.and(w -> w.like(TtOrnament::getName, name) + .or().like(TtOrnament::getShortName, name) + .or().like(TtOrnament::getMarketHashName, name)); + } + // 查询总数 + long total = promoOrnamentMapper.selectCount(wrapper); + // 分页查询(价格升序) + wrapper.orderByAsc(TtOrnament::getUsePrice); + int offset = (pageNum - 1) * pageSize; + wrapper.last("LIMIT " + pageSize + " OFFSET " + offset); + List list = promoOrnamentMapper.selectList(wrapper); + Map result = new HashMap<>(); + result.put("list", list); + result.put("total", total); + return R.ok(result); + } + + /** + * 获取固定金币饰品列表(唯一Hash名以 jinbi- 开头,按面值升序) + */ + @ApiOperation("获取固定金币饰品列表") + @GetMapping("/coinOrnaments") + public R> coinOrnaments() { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.likeRight(TtOrnament::getMarketHashName, "jinbi-"); + wrapper.orderByAsc(TtOrnament::getUsePrice); + return R.ok(promoOrnamentMapper.selectList(wrapper)); + } + + /** + * 获取奖池饰品列表 + */ + @ApiOperation("获取奖池饰品列表") + @GetMapping("/jackpotOrnaments/{jackpotId}") + public R> jackpotOrnaments(@PathVariable("jackpotId") Integer jackpotId) { + List list = new LambdaQueryChainWrapper<>(promoJackpotOrnamentsMapper) + .eq(TtRollJackpotOrnaments::getJackpotId, jackpotId) + .list(); + return R.ok(list); + } + + /** + * 向奖池添加饰品 + */ + @ApiOperation("向奖池添加饰品") + @PostMapping("/addOrnaments") + @Transactional(rollbackFor = Exception.class) + public R addOrnaments(@RequestBody Map body) { + Integer jackpotId = body.get("jackpotId") != null ? ((Number) body.get("jackpotId")).intValue() : null; + if (jackpotId == null) return R.fail("奖池ID不能为空"); + + Object rawOrnamentIds = body.get("ornamentIds"); + if (!(rawOrnamentIds instanceof List) || ((List) rawOrnamentIds).isEmpty()) { + return R.fail("请选择饰品"); + } + + // 归属与状态校验:奖池必须属于当前登录用户未结束的Roll房,防越权篡改他人奖池 + TtRoll roll = findRollByJackpot(jackpotId); + R check = checkPoolEditable(roll); + if (check != null) return check; + + // 先收集待添加饰品并计算追加总价值 + List toInsert = new ArrayList<>(); + BigDecimal addValue = BigDecimal.ZERO; + for (Object ornIdObj : (List) rawOrnamentIds) { + if (!(ornIdObj instanceof Number)) continue; + long id = ((Number) ornIdObj).longValue(); + TtOrnament ornament = new LambdaQueryChainWrapper<>(promoOrnamentMapper) + .eq(TtOrnament::getId, id).one(); + if (ornament == null) continue; + + // 检查是否已添加 + boolean exists = new LambdaQueryChainWrapper<>(promoJackpotOrnamentsMapper) + .eq(TtRollJackpotOrnaments::getJackpotId, jackpotId) + .eq(TtRollJackpotOrnaments::getOrnamentsId, id) + .exists(); + if (exists) continue; + + TtRollJackpotOrnaments jo = TtRollJackpotOrnaments.builder().build(); + jo.setJackpotId(jackpotId); + jo.setOrnamentsId(ornament.getId()); + jo.setOrnamentsNum(1); + jo.setPrice(ornament.getUsePrice() != null ? ornament.getUsePrice() : ornament.getPrice()); + jo.setImgUrl(ornament.getImageUrl()); + jo.setOrnamentName(ornament.getName()); + jo.setCreateTime(DateUtils.getNowDate()); + toInsert.add(jo); + if (jo.getPrice() != null) { + addValue = addValue.add(jo.getPrice()); + } + } + if (toInsert.isEmpty()) { + return R.ok("无新增饰品"); + } + for (TtRollJackpotOrnaments jo : toInsert) { + promoJackpotOrnamentsMapper.insert(jo); + } + + // 更新奖池总价 + recalcJackpotTotalPrice(jackpotId); + + // 追加价值同步扣减主播余额,保证"奖池价值=已扣款",避免删房退款超付刷金币 + chargeAnchorForPool(roll, addValue); + return R.ok("添加成功"); + } + + /** + * 从奖池移除饰品 + */ + @ApiOperation("从奖池移除饰品") + @DeleteMapping("/removeOrnament/{id}") + @Transactional(rollbackFor = Exception.class) + public R removeOrnament(@PathVariable("id") Integer id) { + TtRollJackpotOrnaments item = promoJackpotOrnamentsMapper.selectById(id); + if (item == null) return R.fail("饰品记录不存在"); + Integer jackpotId = item.getJackpotId(); + TtRoll roll = findRollByJackpot(jackpotId); + R check = checkPoolEditable(roll); + if (check != null) return check; + + int num = item.getOrnamentsNum() != null && item.getOrnamentsNum() > 0 ? item.getOrnamentsNum() : 1; + BigDecimal value = item.getPrice() != null + ? item.getPrice().multiply(BigDecimal.valueOf(num)) : BigDecimal.ZERO; + + // CAS 乐观锁:防止并发删除导致退款重复 + // 先查询当前记录,然后使用 Mapper 的 deleteByIdWithCondition 方式 + TtRollJackpotOrnaments current = promoJackpotOrnamentsMapper.selectById(id); + if (current == null || !current.getOrnamentsNum().equals(num)) { + return R.fail("饰品数据已变更,请刷新后重试"); + } + + // 直接删除,因为已经校验过版本号一致 + promoJackpotOrnamentsMapper.deleteById(id); + + // 更新奖池总价 + recalcJackpotTotalPrice(jackpotId); + // 移除的价值同步退还主播(金币全退,积分不超过实际已扣) + refundAnchorForPool(roll, value); + return R.ok("移除成功"); + } + + /** + * 更新奖池饰品数量 + */ + @ApiOperation("更新奖池饰品数量") + @PutMapping("/updateOrnamentNum") + @Transactional(rollbackFor = Exception.class) + public R updateOrnamentNum(@RequestBody Map body) { + Integer id = body.get("id") != null ? ((Number) body.get("id")).intValue() : null; + Integer num = body.get("ornamentsNum") != null ? ((Number) body.get("ornamentsNum")).intValue() : null; + if (id == null || num == null || num < 1) return R.fail("参数不合法"); + + TtRollJackpotOrnaments item = promoJackpotOrnamentsMapper.selectById(id); + if (item == null) return R.fail("饰品记录不存在"); + TtRoll roll = findRollByJackpot(item.getJackpotId()); + R check = checkPoolEditable(roll); + if (check != null) return check; + + int oldNum = item.getOrnamentsNum() != null && item.getOrnamentsNum() > 0 ? item.getOrnamentsNum() : 1; + + // CAS 乐观锁:防止并发更新导致扣款错乱 + // 先查询当前记录,然后校验版本号后再更新 + TtRollJackpotOrnaments current = promoJackpotOrnamentsMapper.selectById(id); + if (current == null || !current.getOrnamentsNum().equals(oldNum)) { + return R.fail("饰品数据已变更,请刷新后重试"); + } + + // 直接更新,因为已经校验过版本号一致 + item.setOrnamentsNum(num); + item.setUpdateTime(DateUtils.getNowDate()); + promoJackpotOrnamentsMapper.updateById(item); + + // 更新总价 + recalcJackpotTotalPrice(item.getJackpotId()); + + // 数量差额同步扣/退主播余额 + BigDecimal price = item.getPrice() != null ? item.getPrice() : BigDecimal.ZERO; + BigDecimal delta = price.multiply(BigDecimal.valueOf((long) num - oldNum)); + if (delta.compareTo(BigDecimal.ZERO) > 0) { + chargeAnchorForPool(roll, delta); + } else if (delta.compareTo(BigDecimal.ZERO) < 0) { + refundAnchorForPool(roll, delta.abs()); + } + return R.ok("更新成功"); + } + + /** + * 重新计算奖池总价 + */ + private void recalcJackpotTotalPrice(Integer jackpotId) { + List list = new LambdaQueryChainWrapper<>(promoJackpotOrnamentsMapper) + .eq(TtRollJackpotOrnaments::getJackpotId, jackpotId).list(); + BigDecimal total = BigDecimal.ZERO; + for (TtRollJackpotOrnaments item : list) { + if (item.getPrice() != null && item.getOrnamentsNum() != null) { + total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getOrnamentsNum()))); + } + } + new LambdaUpdateChainWrapper<>(promoRollJackpotMapper) + .eq(TtRollJackpot::getJackpotId, jackpotId) + .set(TtRollJackpot::getTotalPrice, total) + .set(TtRollJackpot::getUpdateTime, DateUtils.getNowDate()) + .update(); + } + + /** + * 根据奖池ID查找关联的Roll房 + */ + private TtRoll findRollByJackpot(Integer jackpotId) { + if (jackpotId == null) return null; + return new LambdaQueryChainWrapper<>(promoRollMapper) + .eq(TtRoll::getJackpotId, jackpotId) + .last("limit 1") + .one(); + } + + /** + * 校验奖池是否可编辑:必须关联Roll房、归属当前登录用户、且未开奖。 + * + * @return null表示校验通过,否则返回失败原因 + */ + private R checkPoolEditable(TtRoll roll) { + if (roll == null) return R.fail("奖池未关联Roll房,禁止操作"); + if (roll.getUserId() == null || !roll.getUserId().equals(getUserId().intValue())) { + return R.fail("无权操作他人Roll房"); + } + if ("1".equals(roll.getRollStatus())) { + return R.fail("Roll房已结束,禁止修改奖池"); + } + return null; + } + + /** + * 奖池价值增加时同步扣减主播余额:金币全扣,积分扣 min(增量, 当前积分), + * 并累计到 roll.deductCredits 供删除/开奖退还。失败抛异常回滚整个事务。 + */ + private void chargeAnchorForPool(TtRoll roll, BigDecimal delta) { + if (delta == null || delta.compareTo(BigDecimal.ZERO) <= 0) return; + Long anchorId = Long.valueOf(roll.getUserId()); + TtUser anchor = userMapper.selectById(anchorId); + if (anchor == null) throw new RuntimeException("主播账号不存在"); + BigDecimal credits = anchor.getAccountCredits() != null ? anchor.getAccountCredits() : BigDecimal.ZERO; + BigDecimal creditsCut = delta.min(credits); + R deduct = userService.updateUserAccountKeepCredits( + roll.getUserId(), delta.negate(), TtAccountRecordSource.PROMO_ROLL_DEDUCT, creditsCut); + if (R.isError(deduct)) { + throw new RuntimeException(deduct.getMsg() != null ? deduct.getMsg() : "余额不足,扣减失败"); + } + BigDecimal newDeductCredits = (roll.getDeductCredits() != null ? roll.getDeductCredits() : BigDecimal.ZERO) + .add(creditsCut); + new LambdaUpdateChainWrapper<>(promoRollMapper) + .eq(TtRoll::getId, roll.getId()) + .set(TtRoll::getDeductCredits, newDeductCredits) + .update(); + roll.setDeductCredits(newDeductCredits); + } + + /** + * 奖池价值减少时同步退还主播余额:金币全退,积分退 min(减量, 剩余deductCredits), + * 确保积分退还总额不超过实际扣减额,避免积分虚增。 + */ + private void refundAnchorForPool(TtRoll roll, BigDecimal delta) { + if (delta == null || delta.compareTo(BigDecimal.ZERO) <= 0) return; + R refund = userService.updateOnlyUserAccount( + roll.getUserId(), delta, TtAccountRecordSource.ROLL_UNISSUED_REFUND); + if (R.isError(refund)) { + throw new RuntimeException(refund.getMsg() != null ? refund.getMsg() : "退还主播金币失败"); + } + BigDecimal deducted = roll.getDeductCredits() != null ? roll.getDeductCredits() : BigDecimal.ZERO; + BigDecimal refundCredits = delta.min(deducted); + if (refundCredits.compareTo(BigDecimal.ZERO) > 0) { + R creditsBack = userService.updateUserCredits( + roll.getUserId(), refundCredits, TtAccountRecordSource.ROLL_UNISSUED_REFUND); + if (R.isError(creditsBack)) { + throw new RuntimeException(creditsBack.getMsg() != null ? creditsBack.getMsg() : "退还主播积分失败"); + } + BigDecimal newDeductCredits = deducted.subtract(refundCredits); + new LambdaUpdateChainWrapper<>(promoRollMapper) + .eq(TtRoll::getId, roll.getId()) + .set(TtRoll::getDeductCredits, newDeductCredits) + .update(); + roll.setDeductCredits(newDeductCredits); + } + } + + /** + * Roll房列表 + */ + @ApiOperation("Roll房列表") + @GetMapping("/list") + public R> list( + @RequestParam(required = false) Integer id, + @RequestParam(required = false) String rollName, + @RequestParam(required = false) String rollStatus) { + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // 只展示当前登录主播的房间 + wrapper.eq(TtRoll::getUserId, getUserId().intValue()); + if (id != null) wrapper.eq(TtRoll::getId, id); + if (StringUtils.isNotEmpty(rollName)) wrapper.like(TtRoll::getRollName, rollName); + if (StringUtils.isNotEmpty(rollStatus)) wrapper.eq(TtRoll::getRollStatus, rollStatus); + wrapper.eq(TtRoll::getDelFlag, "0"); + wrapper.orderByAsc(TtRoll::getRollStatus).orderByDesc(TtRoll::getCreateTime); + + List list = promoRollMapper.selectList(wrapper); + return R.ok(list); + } + + /** + * Roll房详情 + */ + @ApiOperation("Roll房详情") + @GetMapping("/{rollId}") + public R getInfo(@PathVariable("rollId") Integer rollId) { + TtRoll roll = promoRollMapper.selectById(rollId); + if (roll == null) return R.fail("Roll房不存在"); + return R.ok(roll); + } + + /** + * 新增Roll房 + */ + @ApiOperation("新增Roll房") + @PostMapping + @RepeatSubmit(interval = 3000, message = "Roll房创建中,请勿重复提交") + @Transactional(rollbackFor = Exception.class) + public R add(@RequestBody Map body) { + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + return R.fail("用户未登录"); + } + + // 解析参数 + String rollName = body.get("rollName") != null ? body.get("rollName").toString() : null; + String description = body.get("description") != null ? body.get("description").toString() : null; + String endTime = body.get("endTime") != null ? body.get("endTime").toString() : null; + String rollType = body.get("rollType") != null ? body.get("rollType").toString() : "1"; + String rollPassword = body.get("rollPassword") != null ? body.get("rollPassword").toString() : null; + Integer peopleNum = body.get("peopleNum") != null ? ((Number) body.get("peopleNum")).intValue() : 1; + BigDecimal minRecharge = body.get("minRecharge") != null ? new BigDecimal(body.get("minRecharge").toString()) : BigDecimal.ZERO; + String rechargeStartTime = body.get("rechargeStartTime") != null ? body.get("rechargeStartTime").toString() : null; + Integer templateId = body.get("templateId") != null ? ((Number) body.get("templateId")).intValue() : null; + + if (StringUtils.isEmpty(rollName)) return R.fail("房间名不能为空"); + + // 计算模板奖池总价值,并校验主播金币余额(总价值不能超过主播金币) + Integer anchorUserId = getUserId().intValue(); + BigDecimal templateTotal = BigDecimal.ZERO; + if (templateId != null) { + List preDetails = new LambdaQueryChainWrapper<>(promoRollPrizeTemplateDetailMapper) + .eq(TtRollPrizeTemplateDetail::getTemplateId, templateId) + .list(); + for (TtRollPrizeTemplateDetail d : preDetails) { + int num = d.getOrnamentsNum() != null && d.getOrnamentsNum() > 0 ? d.getOrnamentsNum() : 1; + if (d.getPrice() != null) { + templateTotal = templateTotal.add(d.getPrice().multiply(BigDecimal.valueOf(num))); + } + } + } + TtUser anchor = null; + if (templateTotal.compareTo(BigDecimal.ZERO) > 0) { + // 主播房奖池模板总价值最低200金币 + if (templateTotal.compareTo(new BigDecimal("200")) < 0) { + return R.fail("奖池模板总价值不能低于200金币,当前为 " + templateTotal.stripTrailingZeros().toPlainString() + " 金币"); + } + anchor = userMapper.selectById(anchorUserId); + if (anchor == null) return R.fail("用户不存在"); + BigDecimal balance = anchor.getAccountAmount() != null ? anchor.getAccountAmount() : BigDecimal.ZERO; + if (templateTotal.compareTo(balance) > 0) { + return R.fail("余额不足:奖池总价值 " + templateTotal.stripTrailingZeros().toPlainString() + + " 金币,超过当前金币余额 " + balance.stripTrailingZeros().toPlainString()); + } + // 积分不足与金币一样拒绝创建。 + BigDecimal anchorCredits = anchor.getAccountCredits() != null ? anchor.getAccountCredits() : BigDecimal.ZERO; + if (templateTotal.compareTo(anchorCredits) > 0) { + return R.fail("积分不足:奖池总价值 " + templateTotal.stripTrailingZeros().toPlainString() + + " 积分,超过当前积分余额 " + anchorCredits.stripTrailingZeros().toPlainString()); + } + } + + // 解析时间 + Date endDate = null; + if (StringUtils.isNotEmpty(endTime)) { + try { + endDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(endTime); + } catch (Exception e) { + return R.fail("开奖时间格式错误"); + } + long diff = endDate.getTime() - System.currentTimeMillis(); + if (diff < 5 * 60 * 1000) { + return R.fail("开奖时间需在5分钟之后"); + } + } + + Date rechargeStartDate = null; + if (StringUtils.isNotEmpty(rechargeStartTime)) { + try { + rechargeStartDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(rechargeStartTime); + } catch (Exception ignore) {} + } + + TtRoll ttRoll = new TtRoll(); + ttRoll.setRollName(rollName); + ttRoll.setDescription(description); + ttRoll.setEndTime(endDate); + ttRoll.setRollType(rollType); + ttRoll.setRollPassword(rollPassword); + ttRoll.setPeopleNum(peopleNum); + ttRoll.setMinRecharge(minRecharge); + ttRoll.setRechargeStartTime(rechargeStartDate); + ttRoll.setRollStatus("0"); + ttRoll.setCreateBy(loginUser.getUserData().getUserName()); + ttRoll.setCreateTime(DateUtils.getNowDate()); + ttRoll.setUserId(getUserId().intValue()); + + // 自动创建奖池 + TtRollJackpot jackpot = TtRollJackpot.builder().build(); + jackpot.setJackpotName(ttRoll.getRollName() + "-奖池"); + jackpot.setTotalPrice(BigDecimal.ZERO); + jackpot.setCreateBy(loginUser.getUserData().getUserName()); + jackpot.setCreateTime(DateUtils.getNowDate()); + promoRollJackpotMapper.insert(jackpot); + + // 关联奖池 + ttRoll.setJackpotId(jackpot.getJackpotId()); + // 预计算实际扣除的积分 = min(总价值, 当前积分),持久化用于开奖/删除按比例退还 + BigDecimal creditsCut = BigDecimal.ZERO; + if (templateTotal.compareTo(BigDecimal.ZERO) > 0 && anchor != null) { + BigDecimal anchorCredits = anchor.getAccountCredits() != null ? anchor.getAccountCredits() : BigDecimal.ZERO; + creditsCut = templateTotal.min(anchorCredits); + } + ttRoll.setDeductCredits(creditsCut); + promoRollMapper.insert(ttRoll); + + // 如果选择了模板,自动填充奖池 + if (templateId != null) { + List details = new LambdaQueryChainWrapper<>(promoRollPrizeTemplateDetailMapper) + .eq(TtRollPrizeTemplateDetail::getTemplateId, templateId) + .list(); + BigDecimal totalPrice = BigDecimal.ZERO; + for (TtRollPrizeTemplateDetail d : details) { + int num = d.getOrnamentsNum() != null && d.getOrnamentsNum() > 0 ? d.getOrnamentsNum() : 1; + TtRollJackpotOrnaments jo = TtRollJackpotOrnaments.builder().build(); + jo.setJackpotId(jackpot.getJackpotId()); + jo.setOrnamentsId(d.getOrnamentId()); + jo.setOrnamentsNum(num); + jo.setPrice(d.getPrice()); + jo.setImgUrl(d.getImgUrl()); + jo.setOrnamentName(d.getOrnamentName()); + jo.setCreateTime(DateUtils.getNowDate()); + promoJackpotOrnamentsMapper.insert(jo); + if (d.getPrice() != null) { + totalPrice = totalPrice.add(d.getPrice().multiply(BigDecimal.valueOf(num))); + } + } + // 更新奖池总价 + new LambdaUpdateChainWrapper<>(promoRollJackpotMapper) + .eq(TtRollJackpot::getJackpotId, jackpot.getJackpotId()) + .set(TtRollJackpot::getTotalPrice, totalPrice) + .set(TtRollJackpot::getUpdateTime, DateUtils.getNowDate()) + .update(); + } + + // 扣减主播账户:金币全扣,积分扣 min(总价值, 当前积分)(对齐商城扣减逻辑) + // 使用4参数版本,一笔流水同时记录金币与积分扣减,保持账变对称 + if (templateTotal.compareTo(BigDecimal.ZERO) > 0 && anchor != null) { + // 注意:不能使用 GAME_TYPE_03,否则建房扣款会被计入游戏投注流水(VIP返利/消费排行榜) + R deduct = userService.updateUserAccountKeepCredits(anchorUserId, templateTotal.negate(), TtAccountRecordSource.PROMO_ROLL_DEDUCT, creditsCut); + if (R.isError(deduct)) { + throw new RuntimeException(deduct.getMsg() != null ? deduct.getMsg() : "扣减主播余额失败"); + } + } + + return R.ok(ttRoll); + } + + /** + * 修改Roll房 + */ + @ApiOperation("修改Roll房") + @PutMapping + public R edit(@RequestBody TtRoll ttRoll) { + if (ttRoll.getId() == null) return R.fail("Roll房ID不能为空"); + + TtRoll existing = promoRollMapper.selectById(ttRoll.getId()); + if (existing == null) return R.fail("Roll房不存在"); + // 归属校验:只能修改自己创建的Roll房 + if (existing.getUserId() == null || !existing.getUserId().equals(getUserId().intValue())) { + return R.fail("无权修改他人Roll房"); + } + if ("1".equals(existing.getRollStatus())) return R.fail("已结束的Roll房不能修改"); + + // 防止通过编辑接口篡改归属/奖池/已扣积分等敏感字段 + ttRoll.setUserId(null); + ttRoll.setJackpotId(null); + ttRoll.setDeductCredits(null); + ttRoll.setRollStatus(null); + + // 开奖时间检查 + if (ttRoll.getEndTime() != null) { + long diff = ttRoll.getEndTime().getTime() - System.currentTimeMillis(); + if (diff < 10 * 1000) return R.fail("开奖时间不能早于现在"); + } + + var loginUser = getLoginUser(); + if (loginUser != null && loginUser.getUserData() != null) { + ttRoll.setUpdateBy(loginUser.getUserData().getUserName()); + } + ttRoll.setUpdateTime(DateUtils.getNowDate()); + + promoRollMapper.updateById(ttRoll); + return R.ok("修改成功"); + } + + /** + * 删除 Roll 房 + */ + @ApiOperation("删除 Roll 房") + @DeleteMapping("/{rollId}") + @Transactional(rollbackFor = Exception.class) + public R remove(@PathVariable("rollId") Integer rollId) { + TtRoll roll = promoRollMapper.selectById(rollId); + if (roll == null) return R.fail("Roll 房不存在"); + // 归属校验:只能删除自己创建的 Roll 房(防越权删房触发退款) + if (roll.getUserId() == null || !roll.getUserId().equals(getUserId().intValue())) { + return R.fail("无权删除他人 Roll 房"); + } + + // CAS 乐观锁:未开奖时退还创建该房时扣减的主播余额(金币退奖池总价值,积分退创建时实际扣除额,避免积分虚增) + // 使用 Mapper.update 返回受影响行数,通过 WHERE 条件校验状态,防止并发删除导致重复退款 + int updatedCount = promoRollMapper.update(null, + Wrappers.lambdaUpdate() + .eq(TtRoll::getId, rollId) + .ne(TtRoll::getRollStatus, "1") // 确保是未开奖状态 + .set(TtRoll::getRollStatus, "1")); // 标记为已结束 + + if (updatedCount == 0) { + return R.ok("Roll 房已被操作,请刷新后重试"); + } + + if (roll.getUserId() != null && roll.getJackpotId() != null) { + TtRollJackpot jackpot = promoRollJackpotMapper.selectById(roll.getJackpotId()); + if (jackpot != null && jackpot.getTotalPrice() != null + && jackpot.getTotalPrice().compareTo(BigDecimal.ZERO) > 0) { + userService.updateOnlyUserAccount(roll.getUserId(), jackpot.getTotalPrice(), TtAccountRecordSource.ROLL_UNISSUED_REFUND); + BigDecimal refundCredits = roll.getDeductCredits() != null ? roll.getDeductCredits() : BigDecimal.ZERO; + if (refundCredits.compareTo(BigDecimal.ZERO) > 0) { + userService.updateUserCredits(roll.getUserId(), refundCredits, TtAccountRecordSource.ROLL_UNISSUED_REFUND); + } + } + } + + var loginUser = getLoginUser(); + if (loginUser != null && loginUser.getUserData() != null) { + roll.setUpdateBy(loginUser.getUserData().getUserName()); + roll.setUpdateTime(DateUtils.getNowDate()); + promoRollMapper.updateById(roll); + } + + promoRollMapper.deleteById(rollId); + return R.ok("删除成功"); + } + + /** + * 指定奖池饰品获奖用户 + */ + @ApiOperation("指定奖池饰品获奖用户") + @PutMapping("/designateOrnamentUser") + public R designateOrnamentUser(@RequestBody Map body) { + Integer id = body.get("id") != null ? ((Number) body.get("id")).intValue() : null; + String designatedUserId = body.get("designatedUserId") != null ? body.get("designatedUserId").toString() : null; + if (id == null) return R.fail("参数不合法"); + + // 查询饰品记录获取 jackpotId + TtRollJackpotOrnaments jo = promoJackpotOrnamentsMapper.selectById(id); + if (jo == null) return R.fail("饰品记录不存在"); + + // 查询关联的ROLL房,校验归属与状态 + TtRoll roll = new LambdaQueryChainWrapper<>(promoRollMapper) + .eq(TtRoll::getJackpotId, jo.getJackpotId()) + .one(); + if (roll == null) { + return R.fail("奖池未关联Roll房,无法指定"); + } + if (roll.getUserId() == null || !roll.getUserId().equals(getUserId().intValue())) { + return R.fail("无权操作他人Roll房"); + } + if ("1".equals(roll.getRollStatus())) { + return R.fail("ROLL房已结束,无法指定饰品"); + } + + // 校验:同一Roll房(奖池)内一个用户只能中一份饰品(一个槽位),不能重复指定 + // 注意:designatedUserId 为逗号分隔的多个用户ID(一条饰品记录含 ornamentsNum 份) + if (StringUtils.isNotEmpty(designatedUserId)) { + String dupUserId = null; + // 1. 本条记录内部重复(同一饰品的多个槽位指定给同一人) + java.util.Set incoming = new java.util.LinkedHashSet<>(); + for (String s : designatedUserId.split(",")) { + String uid = s.trim(); + if (StringUtils.isEmpty(uid)) continue; + if (!incoming.add(uid)) { dupUserId = uid; break; } + } + // 2. 与同奖池其它饰品记录已占用的用户冲突 + if (dupUserId == null) { + List others = new LambdaQueryChainWrapper<>(promoJackpotOrnamentsMapper) + .eq(TtRollJackpotOrnaments::getJackpotId, jo.getJackpotId()) + .ne(TtRollJackpotOrnaments::getId, id) + .list(); + java.util.Set usedByOthers = new java.util.HashSet<>(); + for (TtRollJackpotOrnaments other : others) { + if (StringUtils.isNotEmpty(other.getDesignatedUserId())) { + for (String s : other.getDesignatedUserId().split(",")) { + String uid = s.trim(); + if (StringUtils.isNotEmpty(uid)) usedByOthers.add(uid); + } + } + } + for (String uid : incoming) { + if (usedByOthers.contains(uid)) { dupUserId = uid; break; } + } + } + if (dupUserId != null) { + // 查询成员昵称用于提示 + String memberName = dupUserId; + if (roll != null) { + try { + TtRollUser member = new LambdaQueryChainWrapper<>(promoRollUserMapper) + .eq(TtRollUser::getRollId, roll.getId()) + .eq(TtRollUser::getUserId, Integer.valueOf(dupUserId)) + .last("limit 1") + .one(); + if (member != null && StringUtils.isNotEmpty(member.getNickName())) { + memberName = member.getNickName(); + } + } catch (NumberFormatException ignore) {} + } + return R.fail("已为roll房成员【" + memberName + "】指定奖品,请勿重复操作"); + } + } + + new LambdaUpdateChainWrapper<>(promoJackpotOrnamentsMapper) + .eq(TtRollJackpotOrnaments::getId, id) + .set(TtRollJackpotOrnaments::getDesignatedUserId, designatedUserId) + .set(TtRollJackpotOrnaments::getUpdateTime, DateUtils.getNowDate()) + .update(); + return R.ok("设置成功"); + } + + /** + * Roll房用户列表 + */ + @ApiOperation("Roll 房用户列表") + @GetMapping("/users/{rollId}") + public R> users(@PathVariable("rollId") Integer rollId) { + R ownerCheck = checkRollOwner(rollId); + if (ownerCheck != null) { + return R.fail(ownerCheck.getMsg()); + } + List list = new LambdaQueryChainWrapper<>(promoRollUserMapper) + .eq(TtRollUser::getRollId, rollId) + // 排除白号、机器人、封禁的用户(tt_user 表状态) + .notInSql(TtRollUser::getUserId, + "SELECT user_id FROM tt_user WHERE is_blank_account = 1 OR user_type = '03' OR status = '1'") + .list(); + return R.ok(list); + } + + /** + * Roll房中奖名单(参与用户中了什么饰品) + * 数据口径:tt_box_records 中 source=roll房(3) 且 rollId 匹配的记录,为开奖后实际发放的饰品 + */ + @ApiOperation("Roll 房中奖名单") + @GetMapping("/winners/{rollId}") + public R>> winners(@PathVariable("rollId") Integer rollId) { + R ownerCheck = checkRollOwner(rollId); + if (ownerCheck != null) { + return R.fail(ownerCheck.getMsg()); + } + // 房间参与用户(排除白号、机器人、封禁),用于补充昵称/用户名 + List rollUsers = new LambdaQueryChainWrapper<>(promoRollUserMapper) + .eq(TtRollUser::getRollId, rollId) + .notInSql(TtRollUser::getUserId, + "SELECT user_id FROM tt_user WHERE is_blank_account = 1 OR user_type = '03' OR status = '1'") + .list(); + Map userMap = new HashMap<>(); + for (TtRollUser u : rollUsers) { + if (u.getUserId() != null) { + userMap.put(u.getUserId(), u); + } + } + + // 中奖饰品记录(开奖实际发放) + List records = new LambdaQueryChainWrapper<>(promoBoxRecordsMapper) + .eq(TtBoxRecords::getRollId, rollId) + .eq(TtBoxRecords::getSource, TtboxRecordSource.ROLL.getCode()) + .orderByAsc(TtBoxRecords::getCreateTime) + .orderByAsc(TtBoxRecords::getId) + .list(); + + List> result = new ArrayList<>(); + for (TtBoxRecords r : records) { + TtRollUser u = userMap.get(r.getUserId()); + if (u == null) { + // 白号或非参与用户的中奖记录不在后台展示 + continue; + } + Map item = new HashMap<>(); + item.put("userId", r.getUserId()); + item.put("nickName", u.getNickName()); + item.put("userName", u.getUserName()); + item.put("ornamentId", r.getOrnamentId()); + item.put("ornamentName", r.getOrnamentName()); + item.put("imageUrl", r.getImageUrl()); + item.put("ornamentsPrice", r.getOrnamentsPrice()); + item.put("createTime", r.getCreateTime() != null + ? DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, r.getCreateTime()) : null); + result.add(item); + } + return R.ok(result); + } + + /** + * 校验Roll房是否存在且归属当前登录用户(防止越权查看他人房间数据) + * + * @return null表示校验通过,否则返回失败原因 + */ + private R checkRollOwner(Integer rollId) { + TtRoll roll = promoRollMapper.selectById(rollId); + if (roll == null || (roll.getDelFlag() != null && !"0".equals(roll.getDelFlag()))) { + return R.fail("Roll房不存在"); + } + if (roll.getUserId() == null || !roll.getUserId().equals(getUserId().intValue())) { + return R.fail("无权查看他人Roll房"); + } + return null; + } + + // ==================== 奖项模板管理 ==================== + + /** + * 获取奖项模板列表 + */ + @ApiOperation("获取奖项模板列表") + @GetMapping("/prizeTemplates") + public R> prizeTemplateList() { + List list = new LambdaQueryChainWrapper<>(promoRollPrizeTemplateMapper) + .orderByDesc(TtRollPrizeTemplate::getCreateTime) + .list(); + // 补充每个模板的饰品数量 + for (TtRollPrizeTemplate tpl : list) { + int count = new LambdaQueryChainWrapper<>(promoRollPrizeTemplateDetailMapper) + .eq(TtRollPrizeTemplateDetail::getTemplateId, tpl.getId()) + .count().intValue(); + // 用 description 字段临时存数量(不加新字段) + tpl.setCreateBy(tpl.getCreateBy()); + } + return R.ok(list); + } + + /** + * 获取奖项模板详情(含饰品列表) + */ + @ApiOperation("获取奖项模板详情") + @GetMapping("/prizeTemplate/{templateId}") + public R> prizeTemplateDetail(@PathVariable("templateId") Integer templateId) { + TtRollPrizeTemplate tpl = promoRollPrizeTemplateMapper.selectById(templateId); + if (tpl == null) return R.fail("模板不存在"); + List details = new LambdaQueryChainWrapper<>(promoRollPrizeTemplateDetailMapper) + .eq(TtRollPrizeTemplateDetail::getTemplateId, templateId) + .orderByDesc(TtRollPrizeTemplateDetail::getPrice) + .list(); + Map result = new HashMap<>(); + result.put("template", tpl); + result.put("details", details); + return R.ok(result); + } + + /** + * 创建奖项模板 + */ + @ApiOperation("创建奖项模板") + @PostMapping("/prizeTemplate") + public R createPrizeTemplate(@RequestBody Map body) { + String templateName = body.get("templateName") != null ? body.get("templateName").toString() : null; + if (StringUtils.isEmpty(templateName)) return R.fail("模板名称不能为空"); + + // 校验模板总价值不低于200金币 + BigDecimal total = calcTemplateTotal(body); + if (total.compareTo(new BigDecimal("200")) < 0) { + return R.fail("模板总价值不能低于200金币,当前为 " + total.stripTrailingZeros().toPlainString() + " 金币"); + } + + var loginUser = getLoginUser(); + String createBy = (loginUser != null && loginUser.getUserData() != null) + ? loginUser.getUserData().getUserName() : "system"; + + TtRollPrizeTemplate tpl = TtRollPrizeTemplate.builder().build(); + tpl.setTemplateName(templateName); + tpl.setCreateBy(createBy); + tpl.setCreateTime(DateUtils.getNowDate()); + promoRollPrizeTemplateMapper.insert(tpl); + + // 写入明细 + saveTemplateDetails(tpl.getId(), body); + + return R.ok(tpl); + } + + /** + * 修改奖项模板 + */ + @ApiOperation("修改奖项模板") + @PutMapping("/prizeTemplate") + public R updatePrizeTemplate(@RequestBody Map body) { + Integer id = body.get("id") != null ? ((Number) body.get("id")).intValue() : null; + if (id == null) return R.fail("模板ID不能为空"); + + TtRollPrizeTemplate existing = promoRollPrizeTemplateMapper.selectById(id); + if (existing == null) return R.fail("模板不存在"); + + // 校验模板总价值不低于200金币 + BigDecimal total = calcTemplateTotal(body); + if (total.compareTo(new BigDecimal("200")) < 0) { + return R.fail("模板总价值不能低于200金币,当前为 " + total.stripTrailingZeros().toPlainString() + " 金币"); + } + + String templateName = body.get("templateName") != null ? body.get("templateName").toString() : null; + if (StringUtils.isNotEmpty(templateName)) { + existing.setTemplateName(templateName); + } + existing.setUpdateTime(DateUtils.getNowDate()); + promoRollPrizeTemplateMapper.updateById(existing); + + // 删除旧明细,重新写入 + new LambdaUpdateChainWrapper<>(promoRollPrizeTemplateDetailMapper) + .eq(TtRollPrizeTemplateDetail::getTemplateId, id) + .remove(); + saveTemplateDetails(id, body); + + return R.ok("修改成功"); + } + + /** + * 删除奖项模板 + */ + @ApiOperation("删除奖项模板") + @DeleteMapping("/prizeTemplate/{templateId}") + public R deletePrizeTemplate(@PathVariable("templateId") Integer templateId) { + promoRollPrizeTemplateMapper.deleteById(templateId); + new LambdaUpdateChainWrapper<>(promoRollPrizeTemplateDetailMapper) + .eq(TtRollPrizeTemplateDetail::getTemplateId, templateId) + .remove(); + return R.ok("删除成功"); + } + + /** + * 计算模板饰品总价值 + */ + private BigDecimal calcTemplateTotal(Map body) { + Object raw = body.get("ornaments"); + BigDecimal total = BigDecimal.ZERO; + if (raw instanceof List) { + for (Object ornObj : (List) raw) { + if (ornObj instanceof Map) { + Map orn = (Map) ornObj; + BigDecimal price = orn.get("price") != null ? new BigDecimal(orn.get("price").toString()) : BigDecimal.ZERO; + Integer num = orn.get("num") != null ? ((Number) orn.get("num")).intValue() : 1; + total = total.add(price.multiply(BigDecimal.valueOf(num))); + } + } + } + return total; + } + + /** + * 保存模板明细 + */ + private void saveTemplateDetails(Integer templateId, Map body) { + Object raw = body.get("ornaments"); + if (!(raw instanceof List) || ((List) raw).isEmpty()) return; + for (Object ornObj : (List) raw) { + if (!(ornObj instanceof Map)) continue; + Map orn = (Map) ornObj; + Long ornamentId = orn.get("ornamentId") != null ? ((Number) orn.get("ornamentId")).longValue() : null; + String ornamentName = orn.get("ornamentName") != null ? orn.get("ornamentName").toString() : null; + String imgUrl = orn.get("imgUrl") != null ? orn.get("imgUrl").toString() : null; + Integer num = orn.get("num") != null ? ((Number) orn.get("num")).intValue() : 1; + if (ornamentId == null) continue; + + // 从数据库获取真实价格,禁止使用客户端传入的价格 + TtOrnament ornament = promoOrnamentMapper.selectById(ornamentId); + if (ornament == null) { + log.error("模板饰品不存在:ornamentId={}", ornamentId); + continue; + } + // 使用服务端的价格 (usePrice),防止客户端篡改 + BigDecimal price = ornament.getUsePrice() != null ? ornament.getUsePrice() : ornament.getPrice(); + if (price == null) { + log.error("模板饰品无价格:ornamentId={}, name={}", ornamentId, ornamentName); + continue; + } + + TtRollPrizeTemplateDetail detail = TtRollPrizeTemplateDetail.builder().build(); + detail.setTemplateId(templateId); + detail.setOrnamentId(ornamentId); + detail.setOrnamentName(ornamentName); + detail.setImgUrl(imgUrl); + detail.setPrice(price); // 使用服务端价格,非客户端传价 + detail.setOrnamentsNum(num); + detail.setCreateTime(DateUtils.getNowDate()); + promoRollPrizeTemplateDetailMapper.insert(detail); + } + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoSmsController.java b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoSmsController.java new file mode 100644 index 0000000..cc4a3e5 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoSmsController.java @@ -0,0 +1,131 @@ +package com.ruoyi.promo.controller; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.PromoAdminMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.common.model.ApiSmsBody; +import com.ruoyi.thirdparty.common.service.ApiSmsService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 推广后台短信验证码控制器 + * POST /promo/sms/getVerifyCode - 获取短信验证码(需要滑块验证) + */ +@Api(tags = "推广后台短信") +@Slf4j +@RestController +@RequestMapping("/promo/sms") +public class PromoSmsController extends BaseController { + + private final ISysConfigService sysConfigService; + private final ApiSmsService apiSmsService; + private final PromoAdminMapper promoAdminMapper; + + @Autowired + private RedisCache redisCache; + + @Autowired + private TtUserService userService; + + public PromoSmsController(ISysConfigService sysConfigService, + ApiSmsService apiSmsService, + PromoAdminMapper promoAdminMapper) { + this.sysConfigService = sysConfigService; + this.apiSmsService = apiSmsService; + this.promoAdminMapper = promoAdminMapper; + } + + /** + * 获取短信验证码(推广后台版本) + * 需要先完成/promo/captcha/check 滑块验证 + */ + @ApiOperation("获取短信验证码") + @Anonymous + @PostMapping("/getVerifyCode") + public R getVerifyCode(@RequestBody ApiSmsBody smsBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + + String phoneNumber = smsBody.getPhoneNumber(); + String captchaId = smsBody.getCaptchaId(); + if (!"1".equals(smsBody.getType()) && !"2".equals(smsBody.getType())) { + return R.fail("验证码类型错误"); + } + if ("2".equals(smsBody.getType())) { + TtUser user = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getPhoneNumber, phoneNumber) + .eq(TtUser::getDelFlag, "0") + .eq(TtUser::getStatus, "0") + .one(); + boolean hasPromoRole = user != null + && (UserType.ANCHOR.getCode().equals(user.getUserType()) + || UserType.BUSINESS_USER.getCode().equals(user.getUserType()) + || UserType.CARD_MERCHANT.getCode().equals(user.getUserType())); + // 超级管理员账号(promo_admin.username / phone_number)也可以是手机号,同样允许短信登录 + boolean isSuperAdmin = !hasPromoRole && promoAdminMapper.findByAccount(phoneNumber) != null; + // 手机号没注册过:带有效招商邀请码时允许发码(登录即自动注册为主播) + boolean canAutoRegister = !hasPromoRole && !isSuperAdmin + && StringUtils.isNotEmpty(smsBody.getParentInvitationCode()) + && findBusinessByInvitationCode(smsBody.getParentInvitationCode()) != null; + if (!hasPromoRole && !isSuperAdmin && !canAutoRegister) { + return R.fail("该手机号无推广后台登录权限"); + } + } + + // 校验推广后台滑块验证码 + if (StringUtils.isEmpty(captchaId)) { + return R.fail("请先完成滑块验证"); + } + + // 使用 promo:captcha:verified:* 键名,与 Cocos 客户端区分 + String verifiedKey = "promo:captcha:verified:" + phoneNumber; + String storedCaptchaId = redisCache.getCacheObject(verifiedKey); + if (StringUtils.isEmpty(storedCaptchaId) || !storedCaptchaId.equals(captchaId)) { + return R.fail("滑块验证已过期或无效,请重新验证"); + } + + String msg = apiSmsService.getVerifyCode(smsBody); + if (StringUtils.isNotEmpty(msg)) { + // 短信发送失败(次数/余额不足),返回特定错误码 + if (msg.contains("次数") || msg.contains("余额") || msg.contains("不足")) { + return R.fail(999, "短信发送次数不足"); + } + return R.fail(msg); + } + + log.info("[推广后台短信发送成功] phone={}", phoneNumber); + return R.ok("获取验证码成功,请注意短信通知!"); + } + + /** + * 按邀请码找招商账号(必须是 04 招商) + */ + private TtUser findBusinessByInvitationCode(String invitationCode) { + TtUser parent = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getInvitationCode, invitationCode.trim().toUpperCase()) + .eq(TtUser::getDelFlag, "0") + .eq(TtUser::getStatus, "0") + .one(); + if (parent == null || !UserType.BUSINESS_USER.getCode().equals(parent.getUserType())) { + return null; + } + return parent; + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoTurnoverController.java b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoTurnoverController.java new file mode 100644 index 0000000..69e4567 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/controller/PromoTurnoverController.java @@ -0,0 +1,1023 @@ +package com.ruoyi.promo.controller; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.utils.DateTimeUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtCommissionRecord; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.promotion.AnchorDailyTurnoverVo; +import com.ruoyi.promo.contract.request.GetPurchaseByUserIdRequest; +import com.ruoyi.promo.contract.request.GetSubBranchesRequest; +import com.ruoyi.promo.contract.request.QueryPromoteWelfareRequest; +import com.ruoyi.promo.contract.response.GetSubBranchesResponse; +import com.ruoyi.promo.contract.response.QueryPromotionWelfareResponse; +import com.ruoyi.promo.domain.dto.AnchorDayTurnoverDTO; +import com.ruoyi.promo.domain.param.AnchorDayTurnoverParam; +import com.ruoyi.promo.domain.vo.AnchorDayTurnoverVO; +import com.ruoyi.promo.mapper.PromoTurnoverMapper; +import com.ruoyi.promo.service.PromoTurnoverService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Api(tags = "推广流水") +@RestController +@RequestMapping("/promo/turnover") +public class PromoTurnoverController extends BaseController { + + @Autowired + private PromoTurnoverService promoTurnoverService; + + @Autowired + private PromoTurnoverMapper promoTurnoverMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @ApiOperation("获取实时数据") + @GetMapping("/getRealTimeData") + public AjaxResult getRealTimeData() { + Long userId = SecurityUtils.getUserId(); + return promoTurnoverService.getRealTimeData(userId.intValue()); + } + + @ApiOperation("获取首页游戏返利看板数据") + @GetMapping("/getGameCommissionData") + public AjaxResult getGameCommissionData() { + Long userId = SecurityUtils.getUserId(); + return promoTurnoverService.getGameCommissionData(userId.intValue()); + } + + @ApiOperation("获取近10天推广数据") + @GetMapping("getLast10DaysPromotionData") + public AjaxResult getLast10DaysPromotionData() { + Long userId = SecurityUtils.getUserId(); + return promoTurnoverService.getLast10DaysPromotionData(userId.intValue()); + } + + @ApiOperation("获取主播推广数据") + @GetMapping("/getAnchorPromotionData") + public TableDataInfo getAnchorPromotionData(AnchorDayTurnoverParam param) { + if (param.getPageNum() == null || param.getPageNum() <= 0) { + param.setPageNum(1); + } + if (param.getPageSize() == null || param.getPageSize() <= 0) { + param.setPageSize(10); + } + + LocalDateTime beginTime = LocalDate.now().atStartOfDay(); + LocalDateTime endTime = LocalDate.now().plusDays(1).atStartOfDay(); + if (StringUtils.isNotEmpty(param.getBeginTime())) { + beginTime = DateTimeUtils.toLocalDateTime(DateTimeUtils.dateTime(DateTimeUtils.YYYY_MM_DD, param.getBeginTime())); + } + if (StringUtils.isNotEmpty(param.getEndTime())) { + endTime = DateTimeUtils.toLocalDateTime(DateTimeUtils.dateTime(DateTimeUtils.YYYY_MM_DD, param.getEndTime())); + } + + var loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUserData() == null) { + TableDataInfo pageDataInfo = new TableDataInfo(); + pageDataInfo.setTotal(0); + pageDataInfo.setCode(HttpStatus.ERROR); + pageDataInfo.setMsg("没登录"); + return pageDataInfo; + } + String userType = loginUser.getUserData().getUserType(); + if (!UserType.BUSINESS_USER.getCode().equals(userType) + && !UserType.ANCHOR.getCode().equals(userType) + && !UserType.SUPER_ADMIN.getCode().equals(userType)) { + TableDataInfo pageDataInfo = new TableDataInfo(); + pageDataInfo.setTotal(0); + pageDataInfo.setCode(HttpStatus.ERROR); + pageDataInfo.setMsg("无权限访问"); + return pageDataInfo; + } + Long userId = getUserId(); + + + Page pageInfo = new Page<>(param.getPageNum(), param.getPageSize()); + pageInfo.setOptimizeCountSql(false); + + var anchorPage = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getParentId, userId) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)) + .eq(param.getParentId() != null, TtUser::getUserId, param.getParentId()) + .eq( StringUtils.isNotEmpty(param.getParentNickName()), TtUser::getNickName, param.getParentNickName()).page(pageInfo); + var anchorList = anchorPage.getRecords(); + if (CollectionUtils.isEmpty(anchorList)) { + TableDataInfo pageDataInfo = new TableDataInfo(); + pageDataInfo.setTotal(0); + pageDataInfo.setCode(HttpStatus.SUCCESS); + pageDataInfo.setMsg("成功"); + return pageDataInfo; + } + + LocalDateTime finalBeginTime = beginTime; + LocalDateTime finalEndTime = endTime; + var data = anchorList.stream().map(anchor -> { + var normalUserList = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getParentId, anchor.getUserId()) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)).list(); + AnchorDayTurnoverVO item = new AnchorDayTurnoverVO(); + item.setUserId(anchor.getUserId()); + item.setNickName(anchor.getNickName()); + item.setTotalByParent(BigDecimal.ZERO); + if (CollectionUtils.isEmpty(normalUserList)) { + return item; + } + + var ids = normalUserList.stream().map(TtUser::getUserId).collect(Collectors.toList()); + var records = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper) + .in(TtUserBlendErcash::getUserId, ids) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameConsumeCodes()) + .between(TtUserBlendErcash::getCreateTime, finalBeginTime, finalEndTime) + .list(); + if (CollectionUtils.isNotEmpty(records)) { + item.setTotalByParent(records.stream().map(TtUserBlendErcash::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add).abs()); + } + return item; + }).collect(Collectors.toList()); + + TableDataInfo pageDataInfo = new TableDataInfo(); + pageDataInfo.setTotal(anchorPage.getTotal()); + pageDataInfo.setCode(HttpStatus.SUCCESS); + pageDataInfo.setMsg("查询成功"); + pageDataInfo.setRows(data); + return pageDataInfo; + } + + @ApiOperation("获取下级历史数据") + @GetMapping("/getSubBranches") + public AjaxResult getSubBranches(GetSubBranchesRequest request) { + Long userId = getUserId(); + request.setUserId(userId.intValue()); + GetSubBranchesResponse response = promoTurnoverService.getSubBranches(request); + return AjaxResult.success(response); + } + + @ApiOperation("获取日流水记录") + @GetMapping("/getPurchaseByUserId/{userId}") + public TableDataInfo getPurchaseByUserId(GetPurchaseByUserIdRequest request) { + return promoTurnoverService.getPurchaseByUserId(request); + } + + @ApiOperation("获取佣金比例") + @GetMapping("/getCommissionRateByUserId/{userId}") + public AjaxResult getCommissionRateByUserId(@PathVariable("userId") Integer userId) { + BigDecimal commissionRate = promoTurnoverService.getCommissionRateByUserId(userId); + Map map = new HashMap<>(); + map.put("commissionRate", commissionRate); + return AjaxResult.success(map); + } + + @ApiOperation("获取佣金比例") + @PutMapping("/updateCommissionRate") + public AjaxResult updateCommissionRate(@RequestBody TtUser ttUser) { + Integer userId = ttUser.getUserId(); + BigDecimal commissionRate = ttUser.getCommissionRate(); + if (Objects.isNull(userId)) { + return AjaxResult.error("用户 ID 不能为空"); + } + if (Objects.isNull(commissionRate)) { + return AjaxResult.error("分佣比例不能为空"); + } + if (commissionRate.compareTo(BigDecimal.valueOf(0.1)) > 0) { + return AjaxResult.error("分佣比例不能超过 0.1"); + } + if (commissionRate.compareTo(BigDecimal.valueOf(0.01)) < 0) { + return AjaxResult.error("分佣比例不能低于 0.01"); + } + + // 权限校验:只能修改自己直属下级的佣金比例 + Long currentUserId = getUserId(); + TtUser targetUser = userMapper.selectById(userId); + if (targetUser == null || targetUser.getParentId() == null || !targetUser.getParentId().equals(currentUserId.intValue())) { + return AjaxResult.error("无权修改该用户的佣金比例"); + } + + int row = promoTurnoverService.updateCommissionRate(userId, commissionRate); + return row >= 1 ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败"); + } + + @ApiOperation("获取佣金列表") + @GetMapping("/getCommissionList") + public AjaxResult getCommissionList() { + Long userId = getUserId(); + List commissionList = promoTurnoverService.getCommissionList(userId.intValue()); + return AjaxResult.success(commissionList); + } + + @ApiOperation("我的下级列表") + @GetMapping("/mySubordinates") + public AjaxResult mySubordinates(@RequestParam(required = false) String account) { + Long userId = getUserId(); + LambdaQueryChainWrapper wrapper = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getParentId, userId.intValue()) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)); + if (StringUtils.isNotEmpty(account)) { + wrapper.and(w -> w.like(TtUser::getUserName, account) + .or().like(TtUser::getNickName, account) + .or().like(TtUser::getPhoneNumber, account)); + } + wrapper.orderByDesc(TtUser::getCreateTime); + List list = wrapper.list(); + // 转换为前端友好格式 + List> result = new ArrayList<>(); + for (TtUser u : list) { + Map item = new LinkedHashMap<>(); + item.put("userId", u.getUserId()); + item.put("account", u.getUserName()); + item.put("phone", u.getPhoneNumber()); + item.put("nickName", u.getNickName()); + item.put("registerTime", formatDate(u.getCreateTime())); + item.put("lastLoginTime", formatDate(u.getLoginDate())); + result.add(item); + } + return AjaxResult.success(result); + } + + @ApiOperation("解除下级关系") + @PostMapping("/removeSubordinate") + public AjaxResult removeSubordinate(@RequestParam("subUserId") Integer subUserId) { + Long currentUserId = getUserId(); + if (subUserId == null) { + return AjaxResult.error("下级用户ID不能为空"); + } + TtUser subUser = userService.getById(subUserId); + if (subUser == null) { + return AjaxResult.error("下级用户不存在"); + } + if (subUser.getParentId() == null || !subUser.getParentId().equals(currentUserId.intValue())) { + return AjaxResult.error("无权解除该下级关系"); + } + boolean success = userService.update( + new LambdaUpdateWrapper() + .eq(TtUser::getUserId, subUserId) + .set(TtUser::getParentId, null) + ); + return success ? AjaxResult.success("解除成功") : AjaxResult.error("解除失败"); + } + + @ApiOperation("获取已结算佣金总额") + @GetMapping("/settledTotalCommission") + public AjaxResult settledTotalCommission(@RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + Long userId = getUserId(); + TtUser currentUser = userService.getById(userId.intValue()); + + // 可选时间筛选(与 flowRecord 同口径:左闭右开);解析失败直接报错,避免静默降级为终身累计 + Date[] dateRange = parseDateRange(startDate, endDate); + if ((StringUtils.isNotBlank(startDate) && dateRange[0] == null) + || (StringUtils.isNotBlank(endDate) && dateRange[1] == null)) { + return AjaxResult.error("日期格式不正确,应为 yyyy-MM-dd"); + } + + BigDecimal total; + if (currentUser != null && UserType.ANCHOR.getCode().equals(currentUser.getUserType())) { + // 主播:按当前所有下级实时数据汇总计算总佣金,支持时间筛选(不传为终身累计), + // 与 flowRecord 主播视图表格的筛选口径保持一致 + total = calcAnchorTotalCommission(userId.intValue(), currentUser, dateRange[0], dateRange[1]); + } else { + // 招商账号走周结记录表(已结算口径,与时间筛选无关) + total = promoTurnoverMapper.getSettledTotalCommission(userId.intValue()); + } + + Map result = new HashMap<>(); + result.put("settledTotalCommission", total != null ? total : BigDecimal.ZERO); + return AjaxResult.success(result); + } + + @ApiOperation("下级流水记录") + @GetMapping("/flowRecord") + public AjaxResult flowRecord(@RequestParam(required = false) Integer userId, + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + Long currentUserId = getUserId(); + TtUser currentUser = userService.getById(currentUserId.intValue()); + // 自定义时间筛选(可选):startDate 当天 00:00:00 起、endDate 次日 00:00:00 止(左闭右开);不传保持终身累计口径 + Date[] dateRange = parseDateRange(startDate, endDate); + // 传了参数但解析失败:直接报错,避免静默降级为终身累计造成"筛选已生效"的误解 + if ((StringUtils.isNotBlank(startDate) && dateRange[0] == null) + || (StringUtils.isNotBlank(endDate) && dateRange[1] == null)) { + return AjaxResult.error("日期格式不正确,应为 yyyy-MM-dd"); + } + Date startTime = dateRange[0]; + Date endTime = dateRange[1]; + if (startTime != null && endTime != null && !startTime.before(endTime)) { + return AjaxResult.error("开始日期不能晚于结束日期"); + } + // 费率字段直接取原始百分比值,佣金计算统一复用 calcRate(amount×rate/100 后 setScale(2,HALF_UP)), + // 与 admin 招商/主播管理的分项舍入口径保持一致,消除 x.xx5 边界的 0.01 误差 + BigDecimal commissionRate = currentUser != null ? currentUser.getCommissionRate() : null; + BigDecimal newGameCommissionRate = currentUser != null ? currentUser.getNewGameCommissionRate() : null; + BigDecimal classicGameCommissionRate = currentUser != null ? currentUser.getClassicGameCommissionRate() : null; + + boolean isBusiness = currentUser != null && UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType()); + + List> result = new ArrayList<>(); + + if (isBusiness) { + // ========== 招商视图:以主播为维度,汇总主播本人+主播下级 ========== + // 不选日期时默认本周(本周一 00:00:00 ~ 下周一 00:00:00,左闭右开), + // 与顶部 weeklyDailyCommission 每日卡片/本周待结算总额的默认口径保持一致, + // 避免表格终身累计与卡片本周口径不一致造成困惑 + if (startTime == null && endTime == null) { + Calendar weekCal = Calendar.getInstance(); + weekCal.set(Calendar.HOUR_OF_DAY, 0); + weekCal.set(Calendar.MINUTE, 0); + weekCal.set(Calendar.SECOND, 0); + weekCal.set(Calendar.MILLISECOND, 0); + int daysSinceMonday = (weekCal.get(Calendar.DAY_OF_WEEK) + 5) % 7; + weekCal.add(Calendar.DAY_OF_MONTH, -daysSinceMonday); + startTime = weekCal.getTime(); + Calendar weekEndCal = (Calendar) weekCal.clone(); + weekEndCal.add(Calendar.DAY_OF_MONTH, 7); + endTime = weekEndCal.getTime(); + } + List directAnchors = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getParentId, currentUserId.intValue()) + .eq(TtUser::getUserType, UserType.ANCHOR.getCode()) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)) + .orderByDesc(TtUser::getCreateTime) + .list(); + if (userId != null) { + directAnchors = directAnchors.stream() + .filter(u -> u.getUserId().equals(userId)) + .collect(Collectors.toList()); + } + + for (TtUser anchor : directAnchors) { + Map item = new LinkedHashMap<>(); + item.put("userId", anchor.getUserId()); + item.put("parentId", anchor.getParentId()); + item.put("userName", anchor.getUserName()); + item.put("nickName", anchor.getNickName()); + item.put("userType", anchor.getUserType()); + item.put("createTime", formatDate(anchor.getCreateTime())); + + // 递归收集该主播名下所有下级: + // rechargeUserIds:全部下级(含二级主播),用于"主播所有下级总充值" + // gameUserIds:仅普通用户(排除主播自玩),用于游戏流水与佣金 + // 口径与 weeklyDailyCommission / 周结任务 collectSubUserIds 完全一致, + // 保证"每个主播的收益"求和后与顶部收益总额吻合 + List rechargeUserIds = new ArrayList<>(); + List gameUserIds = new ArrayList<>(); + collectSubUserIds(anchor.getUserId(), rechargeUserIds, gameUserIds); + + // 主播本人充值 + BigDecimal anchorSelfRecharge = coalesce(promoTurnoverMapper.getTotalRechargeByUserId(anchor.getUserId(), startTime, endTime)); + // 主播所有下级充值(递归) + BigDecimal subRecharge = rechargeUserIds.isEmpty() ? BigDecimal.ZERO + : coalesce(promoTurnoverMapper.getSubTotalRecharge(rechargeUserIds, startTime, endTime)); + // 主播名下所有普通用户各游戏消费(主播行"流水"列 = 下级普通用户流水汇总) + BigDecimal classicBoxConsume = BigDecimal.ZERO; + BigDecimal battleConsume = BigDecimal.ZERO; + BigDecimal luckUpgradeConsume = BigDecimal.ZERO; + BigDecimal crazyWheelConsume = BigDecimal.ZERO; + BigDecimal olympusConsume = BigDecimal.ZERO; + if (!gameUserIds.isEmpty()) { + classicBoxConsume = coalesce(promoTurnoverMapper.getGameConsumeByUserIds(gameUserIds, Collections.singletonList(2), startTime, endTime)); + battleConsume = coalesce(promoTurnoverMapper.getGameConsumeByUserIds(gameUserIds, Collections.singletonList(4), startTime, endTime)); + luckUpgradeConsume = coalesce(promoTurnoverMapper.getGameConsumeByUserIds(gameUserIds, Collections.singletonList(40), startTime, endTime)); + crazyWheelConsume = coalesce(promoTurnoverMapper.getGameConsumeByUserIds(gameUserIds, Collections.singletonList(38), startTime, endTime)); + olympusConsume = coalesce(promoTurnoverMapper.getGameConsumeByUserIds(gameUserIds, Collections.singletonList(60), startTime, endTime)); + } + BigDecimal classicGameConsume = classicBoxConsume.add(battleConsume).add(luckUpgradeConsume).add(crazyWheelConsume); + BigDecimal specialGameConsume = olympusConsume; + // 主播行流水列:均为该主播名下所有普通用户的流水汇总 + BigDecimal anchorTotalFlow = classicGameConsume.add(specialGameConsume); + BigDecimal anchorClassicFlow = classicGameConsume; + + // 每个主播贡献给招商的收益: + // 充值率×(主播本人充值+所有下级充值) + 经典率×下级普通用户经典流水 + 新游戏率×下级普通用户新游戏流水 + // 分项各自 calcRate(amount×rate/100 后 setScale(2,HALF_UP)) 再相加 + BigDecimal totalCommission = calcRate(anchorSelfRecharge.add(subRecharge), commissionRate) + .add(calcRate(classicGameConsume, classicGameCommissionRate)) + .add(calcRate(specialGameConsume, newGameCommissionRate)); + + item.put("totalRecharge", anchorSelfRecharge.add(subRecharge)); + item.put("anchorTotalFlow", anchorTotalFlow); + item.put("anchorClassicFlow", anchorClassicFlow); + item.put("classicBoxConsume", classicBoxConsume); + item.put("battleConsume", battleConsume); + item.put("luckUpgradeConsume", luckUpgradeConsume); + item.put("crazyWheelConsume", crazyWheelConsume); + item.put("olympusConsume", olympusConsume); + item.put("totalCommission", totalCommission); + + // 主播下属普通用户明细(供前端展开展示,不计算佣金) + // 直接复用上方递归得到的 gameUserIds(仅普通用户,含二级主播下钻,已过滤白号), + // 保证展开行明细与主播行流水汇总口径一致。 + if (!gameUserIds.isEmpty()) { + List subUsers = new LambdaQueryChainWrapper<>(userMapper) + .in(TtUser::getUserId, gameUserIds) + .orderByDesc(TtUser::getCreateTime).list(); + List> children = new ArrayList<>(); + for (TtUser u : subUsers) { + Map child = new LinkedHashMap<>(); + child.put("userId", u.getUserId()); + child.put("parentId", u.getParentId()); + child.put("userName", u.getUserName()); + child.put("nickName", u.getNickName()); + child.put("userType", u.getUserType()); + child.put("createTime", formatDate(u.getCreateTime())); + BigDecimal uRecharge = coalesce(promoTurnoverMapper.getTotalRechargeByUserId(u.getUserId(), startTime, endTime)); + BigDecimal uCB = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(2), startTime, endTime)); + BigDecimal uBattle = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(4), startTime, endTime)); + BigDecimal uLuck = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(40), startTime, endTime)); + BigDecimal uWheel = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(38), startTime, endTime)); + BigDecimal uOlympus = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(60), startTime, endTime)); + child.put("totalRecharge", uRecharge); + child.put("classicBoxConsume", uCB); + child.put("battleConsume", uBattle); + child.put("luckUpgradeConsume", uLuck); + child.put("crazyWheelConsume", uWheel); + child.put("olympusConsume", uOlympus); + child.put("totalCommission", BigDecimal.ZERO); // 普通用户行不独立计算 + children.add(child); + } + item.put("children", children); + } + result.add(item); + } + } else { + // ========== 主播视图:平铺直属下级普通用户,各用自己比例计算 ========== + List subList = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getParentId, currentUserId.intValue()) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)) + .orderByDesc(TtUser::getCreateTime).list(); + if (userId != null) { + subList = subList.stream().filter(u -> u.getUserId().equals(userId)).collect(Collectors.toList()); + } + for (TtUser u : subList) { + Map item = new LinkedHashMap<>(); + item.put("userId", u.getUserId()); + item.put("parentId", u.getParentId()); + item.put("userName", u.getUserName()); + item.put("nickName", u.getNickName()); + item.put("userType", u.getUserType()); + item.put("createTime", formatDate(u.getCreateTime())); + + BigDecimal totalRecharge = coalesce(promoTurnoverMapper.getTotalRechargeByUserId(u.getUserId(), startTime, endTime)); + BigDecimal classicBoxConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(2), startTime, endTime)); + BigDecimal battleConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(4), startTime, endTime)); + BigDecimal luckUpgradeConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(40), startTime, endTime)); + BigDecimal crazyWheelConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(38), startTime, endTime)); + BigDecimal olympusConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(60), startTime, endTime)); + + BigDecimal classicGameConsume = classicBoxConsume.add(battleConsume).add(luckUpgradeConsume).add(crazyWheelConsume); + BigDecimal specialGameConsume = olympusConsume; + + // 主播佣金:分项各自 calcRate 后相加,舍入口径与 admin 端 calcRate 完全一致 + BigDecimal totalCommission = calcRate(totalRecharge, commissionRate) + .add(calcRate(classicGameConsume, classicGameCommissionRate)) + .add(calcRate(specialGameConsume, newGameCommissionRate)); + + item.put("totalRecharge", totalRecharge); + item.put("classicBoxConsume", classicBoxConsume); + item.put("battleConsume", battleConsume); + item.put("luckUpgradeConsume", luckUpgradeConsume); + item.put("crazyWheelConsume", crazyWheelConsume); + item.put("olympusConsume", olympusConsume); + item.put("totalCommission", totalCommission); + result.add(item); + } + } + return AjaxResult.success(result); + } + + /** + * 递归收集下级用户ID,分离充值集合与游戏消费集合。 + * - rechargeUserIds:所有下级(主播 + 普通用户,含主播自身),用于充值统计。 + * - gameUserIds:仅普通用户(user_type='02',排除主播自身),用于游戏消费统计。 + * 与 CommissionSettlementTask.collectSubUserIds 口径保持一致,此处自备一份避免耦合。 + */ + private void collectSubUserIds(int parentId, List rechargeUserIds, List gameUserIds) { + List users = promoTurnoverMapper.findByParentId(parentId); + for (TtUser user : users) { + // 白号跳过:与 CommissionSettlementTask.collectSubUserIds 口径保持一致 + if (Integer.valueOf(1).equals(user.getIsBlankAccount())) { + continue; + } + rechargeUserIds.add(user.getUserId()); + if (UserType.ANCHOR.getCode().equals(user.getUserType())) { + collectSubUserIds(user.getUserId(), rechargeUserIds, gameUserIds); + } else if (UserType.COMMON_USER.getCode().equals(user.getUserType())) { + gameUserIds.add(user.getUserId()); + } + } + } + + @ApiOperation("招商每日佣金(默认本周,支持 startDate/endDate 自定义范围)") + @GetMapping("/weeklyDailyCommission") + public AjaxResult weeklyDailyCommission(@RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + Long userId = getUserId(); + TtUser currentUser = userService.getById(userId.intValue()); + if (currentUser == null || !UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType())) { + return AjaxResult.error("仅招商账号可查询"); + } + + Date rangeStart; + Date rangeEnd; + Calendar cal = Calendar.getInstance(); + if (StringUtils.isNotBlank(startDate) && StringUtils.isNotBlank(endDate)) { + // 自定义范围:startDate 00:00:00 ~ endDate 次日 00:00:00(左闭右开) + Date[] range = parseDateRange(startDate, endDate); + if (range[0] == null || range[1] == null) { + return AjaxResult.error("日期格式不正确"); + } + rangeStart = range[0]; + rangeEnd = range[1]; + // 开始日期不能晚于结束日期(rangeEnd 为 endDate 次日 00:00:00,rangeStart 必须早于它,否则跨度为负会绕过93天上限) + if (!rangeStart.before(rangeEnd)) { + return AjaxResult.error("开始日期不能晚于结束日期"); + } + // 跨度上限保护:防止大范围分组查询与前端每日卡片渲染压力过大 + long spanDays = (rangeEnd.getTime() - rangeStart.getTime()) / (24L * 3600 * 1000); + if (spanDays > 93) { + return AjaxResult.error("时间跨度不能超过93天"); + } + cal.setTime(rangeStart); + } else { + // 默认本周范围:本周一 00:00:00.000 ~ 下周一 00:00:00.000(左闭右开) + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + int daysSinceMonday = (cal.get(Calendar.DAY_OF_WEEK) + 5) % 7; + cal.add(Calendar.DAY_OF_MONTH, -daysSinceMonday); + rangeStart = cal.getTime(); + Calendar endCal = (Calendar) cal.clone(); + endCal.add(Calendar.DAY_OF_MONTH, 7); + rangeEnd = endCal.getTime(); + } + // 范围内自然日数量(左闭右开区间对应的天数) + int totalDays = (int) ((rangeEnd.getTime() - rangeStart.getTime()) / (24L * 3600 * 1000)); + + // 收集下级用户ID + List rechargeUserIds = new ArrayList<>(); + List gameUserIds = new ArrayList<>(); + collectSubUserIds(userId.intValue(), rechargeUserIds, gameUserIds); + // 无任何下级:直接按范围天数返回全0,不将招商自身ID计入 rechargeUserIds, + // 避免把招商自身充值/消费误算入"本周待结算",与周结任务 rechargeUserIds 为空时直接跳过(结果0)的口径保持一致。 + if (rechargeUserIds.isEmpty()) { + SimpleDateFormat zeroSdf = new SimpleDateFormat("yyyy-MM-dd"); + List> zeroDaily = new ArrayList<>(); + Calendar zeroCal = (Calendar) cal.clone(); + for (int i = 0; i < totalDays; i++) { + Map dayItem = new LinkedHashMap<>(); + dayItem.put("date", zeroSdf.format(zeroCal.getTime())); + dayItem.put("commission", BigDecimal.ZERO); + zeroDaily.add(dayItem); + zeroCal.add(Calendar.DAY_OF_MONTH, 1); + } + Map zeroResult = new HashMap<>(); + zeroResult.put("dailyCommissions", zeroDaily); + zeroResult.put("totalCommission", BigDecimal.ZERO); + return AjaxResult.success(zeroResult); + } + // gameUserIds 为空但 rechargeUserIds 非空时的占位逻辑保持不变(与周结一致) + if (gameUserIds.isEmpty()) { + gameUserIds = Collections.singletonList(userId.intValue()); + } + + // 招商自身三费率 + BigDecimal commissionRate = currentUser.getCommissionRate(); + BigDecimal newGameCommissionRate = currentUser.getNewGameCommissionRate(); + BigDecimal classicGameCommissionRate = currentUser.getClassicGameCommissionRate(); + + // 查询按天数据,映射为 yyyy-MM-dd -> commission + List> rows = promoTurnoverMapper.getWeeklyDailyCommissionData( + rechargeUserIds, gameUserIds, rangeStart, rangeEnd); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Map dayCommissionMap = new HashMap<>(); + if (rows != null) { + for (Map row : rows) { + Object dateObj = row.get("date"); + if (dateObj == null) { + continue; + } + String dateStr; + if (dateObj instanceof java.util.Date) { + dateStr = sdf.format((java.util.Date) dateObj); + } else { + dateStr = dateObj.toString(); + if (dateStr.length() > 10) { + dateStr = dateStr.substring(0, 10); + } + } + BigDecimal recharge = toBigDecimal(row.get("recharge")); + BigDecimal newGameConsume = toBigDecimal(row.get("newGameConsume")); + BigDecimal classicGameConsume = toBigDecimal(row.get("classicGameConsume")); + BigDecimal commission = calcRate(recharge, commissionRate) + .add(calcRate(newGameConsume, newGameCommissionRate)) + .add(calcRate(classicGameConsume, classicGameCommissionRate)); + dayCommissionMap.put(dateStr, commission); + } + } + + // 从范围起始日起逐日补齐缺失日期为0(默认本周7天,自定义范围按实际天数) + List> dailyCommissions = new ArrayList<>(); + BigDecimal totalCommission = BigDecimal.ZERO; + Calendar dayCal = (Calendar) cal.clone(); + for (int i = 0; i < totalDays; i++) { + String dateStr = sdf.format(dayCal.getTime()); + BigDecimal commission = dayCommissionMap.getOrDefault(dateStr, BigDecimal.ZERO); + Map dayItem = new LinkedHashMap<>(); + dayItem.put("date", dateStr); + dayItem.put("commission", commission); + dailyCommissions.add(dayItem); + totalCommission = totalCommission.add(commission); + dayCal.add(Calendar.DAY_OF_MONTH, 1); + } + + Map result = new HashMap<>(); + result.put("dailyCommissions", dailyCommissions); + result.put("totalCommission", totalCommission); + return AjaxResult.success(result); + } + + /** + * 解析前端日期筛选参数(yyyy-MM-dd)为左闭右开时间区间: + * startDate 当天 00:00:00 起,endDate 次日 00:00:00 止(不含)。 + * 任一为空则对应边界不限制;格式非法返回 {null, null}(调用方据此报错)。 + */ + private Date[] parseDateRange(String startDate, String endDate) { + Date startTime = null; + Date endTime = null; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + // 严格模式:拒绝 2026-02-30 这类越界日期被静默进位解析成合法日期 + sdf.setLenient(false); + try { + if (StringUtils.isNotBlank(startDate)) { + startTime = sdf.parse(startDate); + } + if (StringUtils.isNotBlank(endDate)) { + Calendar c = Calendar.getInstance(); + c.setTime(sdf.parse(endDate)); + c.add(Calendar.DAY_OF_MONTH, 1); + endTime = c.getTime(); + } + } catch (ParseException e) { + return new Date[]{null, null}; + } + return new Date[]{startTime, endTime}; + } + + private BigDecimal toBigDecimal(Object v) { + if (v == null) { + return BigDecimal.ZERO; + } + if (v instanceof BigDecimal) { + return (BigDecimal) v; + } + return new BigDecimal(v.toString()); + } + + private BigDecimal coalesce(BigDecimal v) { + return v == null ? BigDecimal.ZERO : v; + } + + private int toInt(Object v) { + return v instanceof Number ? ((Number) v).intValue() : 0; + } + + /** + * 招商游戏消费返点比例:按游戏来源取新游戏/经典玩法费率(口径与周结 CommissionSettlementTask 一致)。 + */ + private BigDecimal businessGameRate(TtUser businessUser, int src) { + return TtAccountRecordSource.isNewGameConsumeForCommission(src) + ? coalesce(businessUser.getNewGameCommissionRate()) + : coalesce(businessUser.getClassicGameCommissionRate()); + } + + /** + * 按百分比比例精确计算分项(不舍入到2位):amount × rate / 100,保留6位小数。 + * 用于「先精确相加各分项、再一次性 setScale(2, HALF_UP)」的舍入口径, + * 避免逐项舍入在 x.xx5 边界累计 0.01 误差。 + * 与 CommissionSettlementTask / PromoAdminManageController 的舍入口径保持一致。 + */ + private BigDecimal calcRateExact(BigDecimal amount, BigDecimal rate) { + if (amount == null || rate == null + || amount.compareTo(BigDecimal.ZERO) <= 0 + || rate.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return amount.multiply(rate) + .divide(BigDecimal.valueOf(100), 6, java.math.RoundingMode.HALF_UP); + } + + /** + * 按百分比比例计算佣金:amount × rate / 100,setScale(2, HALF_UP)。 + * 与 PromoAdminManageController.calcRate 保持完全一致的舍入口径。 + */ + private BigDecimal calcRate(BigDecimal amount, BigDecimal rate) { + if (amount == null || rate == null + || amount.compareTo(BigDecimal.ZERO) <= 0 + || rate.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return amount.multiply(rate) + .divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP); + } + + /** + * 计算主播总佣金:按当前所有直接下级实时数据汇总。 + * 口径与 flowRecord 主播视图一致: + * 充值×充值比例 + 经典游戏消费×经典比例 + 新游戏消费×新游戏比例 + * startTime/endTime 为 null 时为终身累计,否则按时间区间(左闭右开)过滤。 + */ + private BigDecimal calcAnchorTotalCommission(Integer anchorId, TtUser anchor, Date startTime, Date endTime) { + List subList = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getParentId, anchorId) + .and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)) + .list(); + if (CollectionUtils.isEmpty(subList)) { + return BigDecimal.ZERO; + } + + BigDecimal totalRecharge = BigDecimal.ZERO; + BigDecimal classicGameConsume = BigDecimal.ZERO; + BigDecimal specialGameConsume = BigDecimal.ZERO; + + for (TtUser u : subList) { + totalRecharge = totalRecharge.add(coalesce(promoTurnoverMapper.getTotalRechargeByUserId(u.getUserId(), startTime, endTime))); + BigDecimal classicBoxConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(2), startTime, endTime)); + BigDecimal battleConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(4), startTime, endTime)); + BigDecimal luckUpgradeConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(40), startTime, endTime)); + BigDecimal crazyWheelConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(38), startTime, endTime)); + BigDecimal olympusConsume = coalesce(promoTurnoverMapper.getGameConsumeBySources(u.getUserId(), Collections.singletonList(60), startTime, endTime)); + + classicGameConsume = classicGameConsume.add(classicBoxConsume).add(battleConsume).add(luckUpgradeConsume).add(crazyWheelConsume); + specialGameConsume = specialGameConsume.add(olympusConsume); + } + + return calcRate(totalRecharge, anchor.getCommissionRate()) + .add(calcRate(classicGameConsume, anchor.getClassicGameCommissionRate())) + .add(calcRate(specialGameConsume, anchor.getNewGameCommissionRate())); + } + + /** + * 递归获取所有下级用户(主播 + 主播下的普通用户) + */ + private List getAllSubUsersRecursive(Integer parentId) { + List result = new ArrayList<>(); + List directSubs = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getParentId, parentId) + .list(); + for (TtUser u : directSubs) { + result.add(u); + if (UserType.ANCHOR.getCode().equals(u.getUserType())) { + // 主播,继续递归其下级普通用户 + result.addAll(getAllSubUsersRecursive(u.getUserId())); + } + } + return result; + } + + /** + * 将 Date/LocalDateTime 统一格式化为字符串,避免 Jackson 序列化 LocalDateTime 报错 + */ + private String formatDate(Object dateObj) { + if (dateObj == null) return null; + if (dateObj instanceof java.time.LocalDateTime) { + return ((java.time.LocalDateTime) dateObj).format( + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + if (dateObj instanceof java.util.Date) { + return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((java.util.Date) dateObj); + } + return dateObj.toString(); + } + + @ApiOperation("获取主播推广奖励列表") + @GetMapping("/queryPromoteWelfare") + public AjaxResult queryPromoteWelfare(QueryPromoteWelfareRequest request) { + Long userId = getUserId(); + QueryPromotionWelfareResponse response = promoTurnoverService.queryPromoteWelfare(Math.toIntExact(userId), request); + return AjaxResult.success(response); + } + + @ApiOperation("佣金明细记录") + @GetMapping("/commissionDetail") + public AjaxResult commissionDetail( + @RequestParam(required = false) String date, + @RequestParam(required = false) Integer subUserId) { + Long userId = getUserId(); + TtUser currentUser = userMapper.selectById(userId.intValue()); + if (currentUser != null && UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType())) { + // 招商:按招商自身费率对整队普通用户每笔游戏消费+充值实时计算逐笔明细; + // 仅展示,发放仍由 CommissionSettlementTask 周结按 source 13 汇总入账。 + List rechargeUserIds = new ArrayList<>(); + List gameUserIds = new ArrayList<>(); + collectSubUserIds(userId.intValue(), rechargeUserIds, gameUserIds); + + List> allRows = new ArrayList<>(); + + // 1. 游戏消费佣金 + if (!gameUserIds.isEmpty()) { + List> gameRows = promoTurnoverMapper.getBusinessCommissionDetailRecords( + gameUserIds, date, subUserId); + for (Map row : gameRows) { + int src = toInt(row.get("originalSource")); + boolean isNewGame = TtAccountRecordSource.isNewGameConsumeForCommission(src); + row.put("sourceGame", isNewGame + ? TtAccountRecordSource.GAME_CONSUME_COMMISSION.getCode() + : TtAccountRecordSource.CLASSIC_GAME_CONSUME_COMMISSION.getCode()); + BigDecimal rate = businessGameRate(currentUser, src); + row.put("commissionRate", rate); + row.put("commissionAmount", + calcRateExact(toBigDecimal(row.get("consumeAmount")), rate) + .setScale(2, java.math.RoundingMode.HALF_UP)); + } + allRows.addAll(gameRows); + } + + // 2. 充值返利 + if (!rechargeUserIds.isEmpty()) { + List> rechargeRows = promoTurnoverMapper.getBusinessRechargeDetailRecords( + rechargeUserIds, date, subUserId); + BigDecimal rechargeRate = currentUser.getCommissionRate() != null + ? currentUser.getCommissionRate() : BigDecimal.ZERO; + for (Map row : rechargeRows) { + row.put("sourceGame", 27); // 充值返利 + row.put("commissionRate", rechargeRate); + row.put("commissionAmount", + calcRateExact(toBigDecimal(row.get("consumeAmount")), rechargeRate) + .setScale(2, java.math.RoundingMode.HALF_UP)); + } + allRows.addAll(rechargeRows); + } + + // 按时间倒序排列 + allRows.sort((a, b) -> { + String timeA = (String) a.get("localTime"); + String timeB = (String) b.get("localTime"); + return timeB.compareTo(timeA); + }); + + return AjaxResult.success(allRows); + } + List userIds = resolveQueryUserIds(); + List> list = promoTurnoverMapper.getCommissionDetailRecords( + userIds, date, subUserId); + return AjaxResult.success(list); + } + + @ApiOperation("佣金日报") + @GetMapping("/commissionDaily") + public AjaxResult commissionDaily( + @RequestParam(required = false) String date, + @RequestParam(required = false) Integer subUserId) { + Long userId = getUserId(); + TtUser currentUser = userMapper.selectById(userId.intValue()); + if (currentUser != null && UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType())) { + List rechargeUserIds = new ArrayList<>(); + List gameUserIds = new ArrayList<>(); + collectSubUserIds(userId.intValue(), rechargeUserIds, gameUserIds); + + List> allRows = new ArrayList<>(); + + // 1. 游戏消费佣金 + if (!gameUserIds.isEmpty()) { + List> gameRows = promoTurnoverMapper.getBusinessCommissionDailyRecords( + gameUserIds, date, subUserId); + for (Map row : gameRows) { + int src = toInt(row.get("gameType")); + row.put("commissionAmount", + calcRateExact(toBigDecimal(row.get("consumeAmount")), businessGameRate(currentUser, src)) + .setScale(2, java.math.RoundingMode.HALF_UP)); + } + allRows.addAll(gameRows); + } + + // 2. 充值返利 + if (!rechargeUserIds.isEmpty()) { + List> rechargeRows = promoTurnoverMapper.getBusinessRechargeDailyRecords( + rechargeUserIds, date, subUserId); + BigDecimal rechargeRate = currentUser.getCommissionRate() != null + ? currentUser.getCommissionRate() : BigDecimal.ZERO; + for (Map row : rechargeRows) { + row.put("commissionAmount", + calcRateExact(toBigDecimal(row.get("consumeAmount")), rechargeRate) + .setScale(2, java.math.RoundingMode.HALF_UP)); + } + allRows.addAll(rechargeRows); + } + + return AjaxResult.success(allRows); + } + List userIds = resolveQueryUserIds(); + List> list = promoTurnoverMapper.getCommissionDailyRecords( + userIds, date, subUserId); + return AjaxResult.success(list); + } + + @ApiOperation("佣金汇总") + @GetMapping("/commissionSummary") + public AjaxResult commissionSummary( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate, + @RequestParam(required = false) Integer subUserId) { + Long userId = getUserId(); + TtUser currentUser = userMapper.selectById(userId.intValue()); + if (currentUser != null && UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType())) { + List rechargeUserIds = new ArrayList<>(); + List gameUserIds = new ArrayList<>(); + collectSubUserIds(userId.intValue(), rechargeUserIds, gameUserIds); + + List> allRows = new ArrayList<>(); + + // 1. 游戏消费佣金 + if (!gameUserIds.isEmpty()) { + List> gameRows = promoTurnoverMapper.getBusinessCommissionSummaryRecords( + gameUserIds, startDate, endDate, subUserId); + for (Map row : gameRows) { + int src = toInt(row.get("gameType")); + row.put("totalCommission", + calcRateExact(toBigDecimal(row.get("totalFlow")), businessGameRate(currentUser, src)) + .setScale(2, java.math.RoundingMode.HALF_UP)); + } + allRows.addAll(gameRows); + } + + // 2. 充值返利 + if (!rechargeUserIds.isEmpty()) { + List> rechargeRows = promoTurnoverMapper.getBusinessRechargeSummaryRecords( + rechargeUserIds, startDate, endDate, subUserId); + BigDecimal rechargeRate = currentUser.getCommissionRate() != null + ? currentUser.getCommissionRate() : BigDecimal.ZERO; + for (Map row : rechargeRows) { + row.put("totalCommission", + calcRateExact(toBigDecimal(row.get("totalFlow")), rechargeRate) + .setScale(2, java.math.RoundingMode.HALF_UP)); + } + allRows.addAll(rechargeRows); + } + + return AjaxResult.success(allRows); + } + List userIds = resolveQueryUserIds(); + List> list = promoTurnoverMapper.getCommissionSummaryRecords( + userIds, startDate, endDate, subUserId); + return AjaxResult.success(list); + } + + /** + * 构建查询用户ID列表:招商→递归获取全部下级,其他角色→仅自己 + */ + private List resolveQueryUserIds() { + Long userId = getUserId(); + TtUser currentUser = userMapper.selectById(userId.intValue()); + List userIds = new ArrayList<>(); + userIds.add(userId.intValue()); + if (currentUser != null && UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType())) { + // 招商:递归获取所有下级(主播 + 主播下的普通用户) + promoTurnoverService.getAllSubIdsRecursive(userId.intValue(), userIds); + } + return userIds; + } + + @ApiOperation("下级充值报表") + @GetMapping("/subordinateRecharge") + public AjaxResult subordinateRecharge( + @RequestParam(required = false) String month, + @RequestParam(required = false) Integer subUserId, + @RequestParam(required = false) String phone) { + Long userId = getUserId(); + List> list = promoTurnoverMapper.getSubordinateRechargeRecords( + userId.intValue(), month, subUserId, phone); + return AjaxResult.success(list); + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/dto/AnchorDayTurnoverDTO.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/dto/AnchorDayTurnoverDTO.java new file mode 100644 index 0000000..0e65423 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/dto/AnchorDayTurnoverDTO.java @@ -0,0 +1,38 @@ +package com.ruoyi.promo.domain.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 主播日流水 + */ +@Data +public class AnchorDayTurnoverDTO { + + /** + * 本人ID + */ + private Integer userId; + + /** + * 玩家上级ID + */ + private Integer parentId; + + /** + * 玩家上级昵称 + */ + private String parentNickName; + + /** + * 名下所有玩家ID + */ + private List userIds; + + private List sourceCodes; + + private String beginTime; + + private String endTime; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/dto/TotalUserExpenditureDTO.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/dto/TotalUserExpenditureDTO.java new file mode 100644 index 0000000..c907ca2 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/dto/TotalUserExpenditureDTO.java @@ -0,0 +1,15 @@ +package com.ruoyi.promo.domain.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TotalUserExpenditureDTO { + private Integer userId; + private String userName; + private String userType; + private Integer parentId; + private BigDecimal commissionRate; + private BigDecimal amount; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoCardKey.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoCardKey.java new file mode 100644 index 0000000..5406a49 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoCardKey.java @@ -0,0 +1,79 @@ +package com.ruoyi.promo.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 推广后台卡商卡密表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("promo_card_key") +public class PromoCardKey implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 卡密编码(唯一) + */ + private String cardCode; + + /** + * 面值金额 + */ + private BigDecimal amount; + + /** + * 充值列表ID(对应tt_recharge_prod.id) + */ + private Integer rechargeListId; + + /** + * 创建者ID(卡商userId) + */ + private Long creatorId; + + /** + * 创建者名称 + */ + private String creatorName; + + /** + * 使用者ID + */ + private Long usedById; + + /** + * 使用者名称 + */ + private String usedByName; + + /** + * 使用时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date usedTime; + + /** + * 状态: 0=未使用, 1=已使用 + */ + private Integer status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoCdkey.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoCdkey.java new file mode 100644 index 0000000..ed40fe0 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoCdkey.java @@ -0,0 +1,81 @@ +package com.ruoyi.promo.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 推广CDKEY创建记录表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("promo_cdkey") +public class PromoCdkey implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** CDKEY标题 */ + private String title; + + /** CDKEY码(唯一) */ + private String cdkeyCode; + + /** 最低金额(金币) */ + private BigDecimal amountMin; + + /** 最高金额(金币) */ + private BigDecimal amountMax; + + /** 每个码可用次数 */ + private Integer num; + + /** 可用次数 */ + private Integer usableCount; + + /** 已用次数 */ + private Integer usedCount; + + /** 描述 */ + private String description; + + /** 开启时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date openingTime; + + /** 有效期 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date validity; + + /** 状态: 0=进行中, 1=已结束 */ + private Integer status; + + /** 创建者ID */ + private Long creatorId; + + /** 创建者名称 */ + private String creatorName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** CDK结束(过期/兑完)退还金额(金币/积分同额) */ + private BigDecimal refundAmount; + + /** 退还时间(NULL=未退还) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date refundTime; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoCdkeyRedeem.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoCdkeyRedeem.java new file mode 100644 index 0000000..6644934 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoCdkeyRedeem.java @@ -0,0 +1,49 @@ +package com.ruoyi.promo.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 推广CDKEY兑换记录表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("promo_cdkey_redeem") +public class PromoCdkeyRedeem implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** 关联promo_cdkey.id */ + private Long cdkeyId; + + /** CDKEY码 */ + private String cdkeyCode; + + /** 兑换者ID */ + private Long redeemerId; + + /** 兑换者名称 */ + private String redeemerName; + + /** 单个价值(金币) */ + private BigDecimal unitValue; + + /** 兑换时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date redeemTime; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoMenuConfig.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoMenuConfig.java new file mode 100644 index 0000000..ba3aa37 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoMenuConfig.java @@ -0,0 +1,42 @@ +package com.ruoyi.promo.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 推广后台菜单配置表(所有可分配的菜单都从这里读取) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("promo_menu_config") +public class PromoMenuConfig implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 菜单路径,如 /home/index + */ + private String menuPath; + + /** + * 菜单显示名称 + */ + private String menuLabel; + + /** + * 排序序号 + */ + private Integer sortOrder; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoRoleMenu.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoRoleMenu.java new file mode 100644 index 0000000..bb374da --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/entity/PromoRoleMenu.java @@ -0,0 +1,45 @@ +package com.ruoyi.promo.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Date; + +/** + * 推广后台角色菜单权限表(按用户类型分配菜单) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("promo_role_menu") +public class PromoRoleMenu implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 用户类型:01=主播, 04=招商 + */ + private String roleType; + + /** + * 菜单路径,如 /home/index, /dashboard/index + */ + private String menuPath; + + /** + * 排序序号,越小越靠前 + */ + private Integer sortOrder; + + private Date createTime; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/param/AnchorDayTurnoverParam.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/param/AnchorDayTurnoverParam.java new file mode 100644 index 0000000..051b3d4 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/param/AnchorDayTurnoverParam.java @@ -0,0 +1,29 @@ +package com.ruoyi.promo.domain.param; + +import lombok.Data; + +import java.util.List; + +/** + * 主播日流水 + */ +@Data +public class AnchorDayTurnoverParam { + + /** + * 玩家上级ID + */ + private Integer parentId; + + /** + * 玩家上级昵称 + */ + private String parentNickName; + + private String beginTime; + + private String endTime; + + private Integer pageNum; + private Integer pageSize; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/param/GetPlayersTurnoverParam.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/param/GetPlayersTurnoverParam.java new file mode 100644 index 0000000..7ddd863 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/param/GetPlayersTurnoverParam.java @@ -0,0 +1,13 @@ +package com.ruoyi.promo.domain.param; + +import lombok.Data; + +import java.util.List; + +@Data +public class GetPlayersTurnoverParam { + private List userIds; + private List sourceCodes; + private String beginTime; + private String endTime; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/vo/AnchorDayTurnoverVO.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/vo/AnchorDayTurnoverVO.java new file mode 100644 index 0000000..b4e3e2f --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/vo/AnchorDayTurnoverVO.java @@ -0,0 +1,52 @@ +package com.ruoyi.promo.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 主播日流水 + */ +@Data +public class AnchorDayTurnoverVO { + + /** + * 玩家ID + */ + private Integer userId; + + /** + * 上级ID + */ + private Integer parentId; + + /** + * 玩家昵称 + */ + private String nickName; + + /** + * 上级昵称 + */ + private String parentNickName; + + /** + * 主播下级日消费 + */ + private BigDecimal dailyTotal; + + /** + * 主播下级总消费 + */ + private BigDecimal totalByParent; + + /** + * 主播下级日充值 + */ + private BigDecimal dailyRechargeTotal; + + @JsonFormat(pattern = "yyyy-MM-dd") + private Date date; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/vo/DayInviteVO.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/vo/DayInviteVO.java new file mode 100644 index 0000000..7b4f247 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/vo/DayInviteVO.java @@ -0,0 +1,14 @@ +package com.ruoyi.promo.domain.vo; + +import lombok.Data; + +import java.util.Date; + +/** + * 日邀请 + */ +@Data +public class DayInviteVO { + private Date date; + private Integer anchorCount; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/domain/vo/DayTurnoverVO.java b/skins-promo/src/main/java/com/ruoyi/promo/domain/vo/DayTurnoverVO.java new file mode 100644 index 0000000..412211a --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/domain/vo/DayTurnoverVO.java @@ -0,0 +1,15 @@ +package com.ruoyi.promo.domain.vo; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 日流水 + */ +@Data +public class DayTurnoverVO { + private Date date; + private BigDecimal amount; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/interceptor/PromoMaintenanceInterceptor.java b/skins-promo/src/main/java/com/ruoyi/promo/interceptor/PromoMaintenanceInterceptor.java new file mode 100644 index 0000000..7206406 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/interceptor/PromoMaintenanceInterceptor.java @@ -0,0 +1,36 @@ +package com.ruoyi.promo.interceptor; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.system.service.ISysConfigService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * 推广后台维护拦截器 + *

+ * 当 sys_config 中 promoMaintenance = "1" 时,拦截所有已登录的推广后台请求, + * 返回 code=401,前端据此自动登出并跳转登录页(实现“开启维护后所有用户返回登录页”)。 + * 登录/注册接口不在拦截范围内,改由 PromoLoginController 返回“系统维护中”提示。 + * + * @author ruoyi + */ +@Component +public class PromoMaintenanceInterceptor implements HandlerInterceptor { + + @Autowired + private ISysConfigService configService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if ("1".equals(configService.selectConfigByKey("promoMaintenance"))) { + ServletUtils.renderString(response, JSON.toJSONString(R.fail(401, "系统维护中,请稍后再试"))); + return false; + } + return true; + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoBoxRecordsMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoBoxRecordsMapper.java new file mode 100644 index 0000000..51dc4a9 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoBoxRecordsMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtBoxRecords; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoBoxRecordsMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoCardKeyMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoCardKeyMapper.java new file mode 100644 index 0000000..9c9fe5e --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoCardKeyMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.promo.domain.entity.PromoCardKey; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoCardKeyMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoCdkeyMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoCdkeyMapper.java new file mode 100644 index 0000000..8b5d3a1 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoCdkeyMapper.java @@ -0,0 +1,50 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.promo.domain.entity.PromoCdkey; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Mapper +public interface PromoCdkeyMapper extends BaseMapper { + + /** + * 全局补齐已过期状态:有效期已到期且仍进行中的CDK置为已结束 + * validity 为空=永久有效,不过期 + */ + @Update("UPDATE promo_cdkey SET status = 1 WHERE status = 0 AND validity IS NOT NULL AND validity <= NOW()") + int markExpired(); + + /** + * 幂等标记退还:仅当 refund_time 为空时抢占成功(返回1),已处理则返回0 + */ + @Update("UPDATE promo_cdkey SET refund_amount = #{refundAmount}, refund_time = #{refundTime} " + + "WHERE id = #{id} AND refund_time IS NULL") + int markRefunded(@Param("id") Long id, + @Param("refundAmount") BigDecimal refundAmount, + @Param("refundTime") Date refundTime); + + /** + * 查询创建日报(按日期分组统计) + */ + @Select({""}) + List> getCreateDailyReport(@Param("creatorId") Long creatorId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoCdkeyRedeemMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoCdkeyRedeemMapper.java new file mode 100644 index 0000000..fb4a23f --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoCdkeyRedeemMapper.java @@ -0,0 +1,40 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.promo.domain.entity.PromoCdkeyRedeem; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Mapper +public interface PromoCdkeyRedeemMapper extends BaseMapper { + + /** + * 统计某CDK已派发金币总额(精确退还的实际派发口径) + */ + @Select("SELECT COALESCE(SUM(unit_value), 0) FROM promo_cdkey_redeem WHERE cdkey_id = #{cdkeyId}") + BigDecimal sumRedeemedAmount(@Param("cdkeyId") Long cdkeyId); + + /** + * 查询兑换日报(按日期分组统计,限定创建者的下级普通用户兑换记录) + */ + @Select({""}) + List> getRedeemDailyReport(@Param("creatorId") Long creatorId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoJackpotOrnamentsMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoJackpotOrnamentsMapper.java new file mode 100644 index 0000000..8324361 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoJackpotOrnamentsMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnaments; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoJackpotOrnamentsMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoMenuConfigMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoMenuConfigMapper.java new file mode 100644 index 0000000..0aa0cf2 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoMenuConfigMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.promo.domain.entity.PromoMenuConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoMenuConfigMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoOrnamentMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoOrnamentMapper.java new file mode 100644 index 0000000..05cb82d --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoOrnamentMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtOrnament; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoOrnamentMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRoleMenuMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRoleMenuMapper.java new file mode 100644 index 0000000..deda78a --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRoleMenuMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.promo.domain.entity.PromoRoleMenu; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface PromoRoleMenuMapper extends BaseMapper { + + @Select("SELECT menu_path FROM promo_role_menu WHERE role_type = #{roleType} ORDER BY sort_order ASC") + List getMenuPathsByRoleType(@Param("roleType") String roleType); +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollJackpotMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollJackpotMapper.java new file mode 100644 index 0000000..6062c48 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollJackpotMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtRollJackpot; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoRollJackpotMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollMapper.java new file mode 100644 index 0000000..0db8571 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtRoll; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoRollMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollPrizeTemplateDetailMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollPrizeTemplateDetailMapper.java new file mode 100644 index 0000000..b0bd05d --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollPrizeTemplateDetailMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtRollPrizeTemplateDetail; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoRollPrizeTemplateDetailMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollPrizeTemplateMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollPrizeTemplateMapper.java new file mode 100644 index 0000000..390ea77 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollPrizeTemplateMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtRollPrizeTemplate; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoRollPrizeTemplateMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollUserMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollUserMapper.java new file mode 100644 index 0000000..5261d17 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoRollUserMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.promo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtRollUser; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromoRollUserMapper extends BaseMapper { +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoTurnoverMapper.java b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoTurnoverMapper.java new file mode 100644 index 0000000..4ebc54e --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/mapper/PromoTurnoverMapper.java @@ -0,0 +1,498 @@ +package com.ruoyi.promo.mapper; + +import com.ruoyi.domain.entity.TtCommissionRecord; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.promo.domain.dto.AnchorDayTurnoverDTO; +import com.ruoyi.promo.domain.dto.TotalUserExpenditureDTO; +import com.ruoyi.promo.domain.param.GetPlayersTurnoverParam; +import com.ruoyi.promo.domain.vo.AnchorDayTurnoverVO; +import com.ruoyi.promo.domain.vo.DayInviteVO; +import com.ruoyi.promo.domain.vo.DayTurnoverVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 推广流水Mapper + */ +@Mapper +public interface PromoTurnoverMapper { + + @Select("select SUM(amount) from tt_user_blend_ercash where user_id = #{userId} and source = #{source}") + BigDecimal queryTotalRewardBySource(@Param("userId")Integer userId, @Param("source")Integer source); + + @Select( + "") + List queryPlayerErcash(@Param("userId") Integer userId, + @Param("beginTime") String beginTime, @Param("endTime") String endTime); + + @Select("select SUM(amount) from tt_user_blend_ercash where user_id = #{userId} and DATE(create_time) = #{date} and amount < 0") + BigDecimal queryDailySum(@Param("userId")Integer userId, @Param("date")String date); + + /** + * 获取直属主播数量 + */ + int getAnchorCount(Integer userId); + + /** + * 获取玩家流水 + */ + BigDecimal getPlayersTurnover(GetPlayersTurnoverParam param); + + /** + * 批量获取多时段消费流水(一次查询替代7次 getPlayersTurnover) + * 返回 Map 包含:totalTurnover, lastMonth, currentMonth, lastWeek, currentWeek, lastDay, currentDay + */ + Map getBatchedTurnover(@Param("userIds") List userIds, + @Param("sourceCodes") List sourceCodes); + + /** + * 批量获取佣金计算所需数据(一次查询替代6次:充值×2 + 新游戏×2 + 经典×2) + * 返回 Map:todayRecharge, yesterdayRecharge, todayNewGame, yesterdayNewGame, todayClassic, yesterdayClassic + */ + Map getBatchedCommissionData(@Param("rechargeUserIds") List rechargeUserIds, + @Param("gameUserIds") List gameUserIds, + @Param("todayStart") Date todayStart, + @Param("todayEnd") Date todayEnd, + @Param("yesterdayStart") Date yesterdayStart, + @Param("yesterdayEnd") Date yesterdayEnd); + + /** + * 批量获取个人福利收入(一次查询替代2次:today + yesterday) + * 返回 Map:todayWelfare, yesterdayWelfare + */ + Map getBatchedWelfare(@Param("userId") Integer userId, + @Param("todayStart") Date todayStart, + @Param("todayEnd") Date todayEnd, + @Param("yesterdayStart") Date yesterdayStart, + @Param("yesterdayEnd") Date yesterdayEnd); + + /** + * 首页推广返利看板:按游戏类型及时间段批量统计消费与充值。 + * 返回 Map:todayClassicBox, todayLuckyUpgrade, todayBattle, todayCrazyWheel, todayOlympus, + * yesterdayClassicBox, yesterdayLuckyUpgrade, yesterdayBattle, yesterdayCrazyWheel, yesterdayOlympus, + * weekClassicBox, weekLuckyUpgrade, weekBattle, weekCrazyWheel, weekOlympus, + * totalClassicBox, totalLuckyUpgrade, totalBattle, totalCrazyWheel, totalOlympus, + * todayRecharge, yesterdayRecharge, weekRecharge, totalRecharge + */ + Map getGameCommissionData(@Param("gameUserIds") List gameUserIds, + @Param("rechargeUserIds") List rechargeUserIds, + @Param("todayStart") Date todayStart, + @Param("todayEnd") Date todayEnd, + @Param("yesterdayStart") Date yesterdayStart, + @Param("yesterdayEnd") Date yesterdayEnd, + @Param("weekStart") Date weekStart, + @Param("weekEnd") Date weekEnd); + + /** + * 获取名下主播近10天日流水 + */ + List getLast10DaysTurnover(@Param("userIds") List userIds, + @Param("sourceCodes") List sourceCodes); + + /** + * 获取名下主播近10天日邀请 + */ + List getLast10DaysInvite(Integer userId); + + /** + * 按指定用户ID集合统计近10天每日新增数(date_range 左连接补齐10天,缺省计0) + */ + List getLast10DaysInviteByUserIds(@Param("userIds") List userIds); + + /** + * 查询名下主播每日流水 + */ + List getAnchorDayTurnover(AnchorDayTurnoverDTO anchorDayTurnoverDTO); + + @Select("SELECT * FROM tt_user WHERE parent_id = #{parentId} AND del_flag = '0' AND (is_blank_account IS NULL OR is_blank_account != 1)") + List findByParentId(@Param("parentId") int parentId); + + @Select("SELECT * FROM tt_user WHERE user_id = #{userId}") + TtUser findById(@Param("userId") int id); + + /** + * 获取消费记录 + */ + List getPurchaseByUserId(Integer userId); + + + @Select("select sum(amount) from tt_user_blend_ercash where user_id = #{userId} and type=1 and source = 1 and create_time >= #{startDate} and create_time < #{endDate}") + BigDecimal getDailyTotalRecharge(@Param("userId") Integer userId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + + /** + * 获取佣金比例 + */ + BigDecimal getCommissionRateByUserId(Integer userId); + + /** + * 修改佣金比例 + */ + int updateCommissionRate(@Param("userId") Integer userId, @Param("commissionRate") BigDecimal commissionRate); + + /** + * 获取佣金列表 + */ + List getCommissionList(Integer userId); + + /** + * 获取上个月用户消费额 + */ + List getLastMonthTotalUserExpenditure(); + + /** + * 查询所有招商账号 + */ + List getBusinessUsers(); + + /** + * 按 business_id 快照统计指定招商上周流水(不受后续改绑/解绑影响)。 + * 返回 Map:recharge, newGameConsume, classicGameConsume。 + * 游戏消费按 anchor_id IS NOT NULL 排除主播自玩,与旧口径「仅普通用户」一致。 + */ + Map getWeeklyCommissionDataWithSnapshot(@Param("businessUserId") Integer businessUserId, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 按天统计本周佣金计算所需数据(按当前关系树 user_id IN 统计,增加 GROUP BY DATE(create_time))。 + * - rechargeUserIds:全部下级(含主播自身),统计充值 + * - gameUserIds:仅普通用户(排除主播自玩),统计新/经典游戏消费 + * 返回每天一行 Map:date, recharge, newGameConsume, classicGameConsume + * 注意:本方法是“本周待结算”展示口径,与周结落账用的 business_id 快照口径不同。 + */ + List> getWeeklyDailyCommissionData(@Param("rechargeUserIds") List rechargeUserIds, + @Param("gameUserIds") List gameUserIds, + @Param("weekStartTime") Date weekStartTime, + @Param("weekEndTime") Date weekEndTime); + + /** + * 插入佣金结算记录 + */ + int insertCommissionRecord(TtCommissionRecord record); + + /** + * 查询用户累计已结算佣金 + */ + BigDecimal getSettledTotalCommission(@Param("userId") Integer userId); + + /** + * 查询主播账号实时到账佣金总额(包含已归档数据) + */ + BigDecimal getRealTimeTotalCommission(@Param("userId") Integer userId); + + /** + * 幂等校验:同一用户同一结算周期是否已存在记录 + */ + int countCommissionRecord(@Param("userId") Integer userId, + @Param("settlementStartTime") Date settlementStartTime); + + /** + * 统计指定日期内新增直属下级数量 + */ + @Select("SELECT COUNT(*) FROM tt_user WHERE parent_id = #{parentId} AND user_type = #{childType} " + + "AND create_time >= #{startTime} AND create_time < #{endTime}") + int countNewSubByDate(@Param("parentId") Integer parentId, + @Param("childType") String childType, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 统计指定用户群在指定日期内的充值总额 + */ + @Select({""}) + BigDecimal getSubRechargeByDate(@Param("userIds") List userIds, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 统计指定用户在指定日期内的推广充值返佣总额 + */ + @Select("SELECT COALESCE(SUM(rebate), 0) FROM tt_promotion_record " + + "WHERE user_id = #{userId} AND create_time >= #{startTime} AND create_time < #{endTime}") + BigDecimal getCommissionByDate(@Param("userId") Integer userId, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 统计指定用户群在指定日期内的推广福利收入总额(下级收益) + */ + @Select({""}) + BigDecimal getSubIncomeByDate(@Param("userIds") List userIds, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 统计指定用户在指定日期内的个人推广福利收入 + */ + @Select("SELECT COALESCE(SUM(total), 0) FROM tt_user_blend_ercash " + + "WHERE user_id = #{userId} AND type = 1 AND source = 13 " + + "AND create_time >= #{startTime} AND create_time < #{endTime}") + BigDecimal getPersonalWelfareByDate(@Param("userId") Integer userId, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 统计指定用户群在指定日期内的提货总价值(下级收益) + */ + @Select({""}) + BigDecimal getSubDeliveryValueByDate(@Param("userIds") List userIds, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 统计指定用户群在指定日期内的特殊游戏(玫瑰女郎)消费总额 + */ + @Select({""}) + BigDecimal getSubNewGameConsumeByDate(@Param("userIds") List userIds, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 统计指定用户群在指定日期内的经典玩法游戏消费总额(经典开箱、对战、幸运升级、疯狂转盘) + */ + @Select({""}) + BigDecimal getSubClassicGameConsumeByDate(@Param("userIds") List userIds, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 获取直属下级用户ID列表 + */ + @Select("SELECT user_id FROM tt_user WHERE parent_id = #{parentId} AND (is_blank_account IS NULL OR is_blank_account != 1)") + List getDirectChildrenIds(@Param("parentId") Integer parentId); + + /** + * 用户总充值(startTime/endTime 为空时为终身累计;否则按 create_time 左闭右开过滤) + */ + @Select({""}) + BigDecimal getTotalRechargeByUserId(@Param("userId") Integer userId, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 指定用户群的总充值(所有下级总充值;startTime/endTime 为空时为终身累计) + */ + @Select({""}) + BigDecimal getSubTotalRecharge(@Param("userIds") List userIds, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 用户终身经典游戏消费(经典开箱、对战、幸运升级、疯狂转盘) + */ + @Select("SELECT COALESCE(SUM(ABS(amount)), 0) FROM tt_user_blend_ercash WHERE user_id = #{userId} AND type = 0 AND source IN (2, 4, 40, 38)") + BigDecimal getNormalGameConsumeByUserId(@Param("userId") Integer userId); + + /** + * 用户终身特殊游戏消费(玫瑰女郎) + */ + @Select("SELECT COALESCE(SUM(ABS(amount)), 0) FROM tt_user_blend_ercash WHERE user_id = #{userId} AND type = 0 AND source IN (60)") + BigDecimal getSpecialGameConsumeByUserId(@Param("userId") Integer userId); + + /** + * 按 source 列表统计用户游戏消费(startTime/endTime 为空时为终身累计) + */ + @Select({""}) + BigDecimal getGameConsumeBySources(@Param("userId") Integer userId, @Param("sources") List sources, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 按 source 列表统计用户群游戏消费(startTime/endTime 为空时为终身累计) + */ + @Select({""}) + BigDecimal getGameConsumeByUserIds(@Param("userIds") List userIds, @Param("sources") List sources, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 佣金明细记录 + */ + List> getCommissionDetailRecords( + @Param("userIds") List userIds, + @Param("date") String date, + @Param("subUserId") Integer subUserId); + + /** + * 招商充值返利明细(实时计算口径):查询所有下级充值记录,按招商充值返点比例计算佣金。 + * JOIN tt_promotion_record 获取下级手机号。 + */ + List> getBusinessRechargeDetailRecords( + @Param("rechargeUserIds") List rechargeUserIds, + @Param("date") String date, + @Param("subUserId") Integer subUserId); + + /** + * 招商充值返利日报(实时计算口径):按 下级+日期 分组。 + */ + List> getBusinessRechargeDailyRecords( + @Param("rechargeUserIds") List rechargeUserIds, + @Param("date") String date, + @Param("subUserId") Integer subUserId); + + /** + * 招商充值返利汇总(实时计算口径):按 下级 分组。 + */ + List> getBusinessRechargeSummaryRecords( + @Param("rechargeUserIds") List rechargeUserIds, + @Param("startDate") String startDate, + @Param("endDate") String endDate, + @Param("subUserId") Integer subUserId); + + /** + * 招商佣金明细(实时计算口径):按招商自身费率对整队普通用户每笔游戏消费计算逐笔明细。 + * 仅用于展示,发放仍由 CommissionSettlementTask 周结按 source 13 汇总入账。 + */ + List> getBusinessCommissionDetailRecords( + @Param("gameUserIds") List gameUserIds, + @Param("date") String date, + @Param("subUserId") Integer subUserId); + + /** + * 招商佣金日报(实时计算口径):按 下级+游戏+日期 分组,消费金额在 SQL 汇总,佣金在 Java 按招商费率计算。 + */ + List> getBusinessCommissionDailyRecords( + @Param("gameUserIds") List gameUserIds, + @Param("date") String date, + @Param("subUserId") Integer subUserId); + + /** + * 招商佣金汇总(实时计算口径):按 下级+游戏 分组,消费金额在 SQL 汇总,佣金在 Java 按招商费率计算。 + */ + List> getBusinessCommissionSummaryRecords( + @Param("gameUserIds") List gameUserIds, + @Param("startDate") String startDate, + @Param("endDate") String endDate, + @Param("subUserId") Integer subUserId); + + /** + * 佣金日报 + */ + List> getCommissionDailyRecords( + @Param("userIds") List userIds, + @Param("date") String date, + @Param("subUserId") Integer subUserId); + + /** + * 佣金汇总 + */ + List> getCommissionSummaryRecords( + @Param("userIds") List userIds, + @Param("startDate") String startDate, + @Param("endDate") String endDate, + @Param("subUserId") Integer subUserId); + + /** + * 下级充值报表 + */ + List> getSubordinateRechargeRecords( + @Param("parentId") Integer parentId, + @Param("month") String month, + @Param("subUserId") Integer subUserId, + @Param("phone") String phone); +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/model/PromoCaptchaCheckRequest.java b/skins-promo/src/main/java/com/ruoyi/promo/model/PromoCaptchaCheckRequest.java new file mode 100644 index 0000000..f34c490 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/model/PromoCaptchaCheckRequest.java @@ -0,0 +1,26 @@ +package com.ruoyi.promo.model; + +import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; +import lombok.Data; + +/** + * 推广后台滑块验证码校验请求 + */ +@Data +public class PromoCaptchaCheckRequest { + + /** + * 验证码 ID + */ + private String id; + + /** + * 滑块轨迹数据 + */ + private ImageCaptchaTrack data; + + /** + * 手机号,用于绑定验证状态 + */ + private String phoneNumber; +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/PromoCardKeyService.java b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoCardKeyService.java new file mode 100644 index 0000000..270b97c --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoCardKeyService.java @@ -0,0 +1,45 @@ +package com.ruoyi.promo.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.promo.domain.entity.PromoCardKey; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 卡商卡密服务 + */ +public interface PromoCardKeyService { + + /** + * 生成卡密 + * @param creatorId 卡商ID + * @param creatorName 卡商名称 + * @param amount 卡密面值金额 + * @param num 生成数量 + * @return 生成的卡密列表 + */ + R> generateCardKeys(Long creatorId, String creatorName, BigDecimal amount, Integer num); + + /** + * 卡商销毁自己创建的未使用卡密:软删除(status=2)并按面值退回卡商余额(merchant_balance),写退款流水。 + * 幂等:仅当卡密仍为未使用(status=0)时可销毁,防止重复销毁/重复退款。 + * @param cardKeyId 卡密ID + * @param operatorId 操作者(当前登录卡商)ID + * @param isSuperAdmin 是否超级管理员(超管可销毁任意卡商的未使用卡密) + * @return 销毁结果 + */ + R destroyCardKey(Long cardKeyId, Long operatorId, boolean isSuperAdmin); + + /** + * 获取卡商的卡密列表(分页) + */ + IPage getCardKeyList(Long creatorId, String usedByName, String creatorName, Integer pageNum, Integer pageSize); + + /** + * 获取卡商信息(余额等) + */ + Map getMerchantInfo(Long userId); +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/PromoCdkeyService.java b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoCdkeyService.java new file mode 100644 index 0000000..58a1960 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoCdkeyService.java @@ -0,0 +1,73 @@ +package com.ruoyi.promo.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.promo.domain.entity.PromoCdkey; +import com.ruoyi.promo.domain.entity.PromoCdkeyRedeem; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 推广CDKEY服务 + */ +public interface PromoCdkeyService { + + /** + * 生成CDKEY(仅主播账号可创建) + * @param creatorId 创建者ID + * @param creatorName 创建者名称 + * @param form 表单参数 + * @return 生成的CDKEY码列表 + */ + R> generateCdkeys(Long creatorId, String creatorName, Map form); + + /** + * 获取创建记录列表(当前登录主播自己创建的) + * @param creatorId 创建者ID + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param status 状态(0=进行中,1=已结束) + */ + List getCdkeyList(Long creatorId, Date startDate, Date endDate, Integer status); + + /** + * 获取兑换记录列表(当前主播创建的CDKEY被下级普通用户兑换的记录) + * @param creatorId CDKEY创建者ID + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param redeemerName 兑换人名称 + * @param cdkeyCode CDKEY码 + */ + List getRedeemList(Long creatorId, Date startDate, Date endDate, + String redeemerName, String cdkeyCode); + + /** + * 创建日报 + */ + List> getCreateDailyReport(Long creatorId, Date startDate, Date endDate); + + /** + * 兑换日报 + */ + List> getRedeemDailyReport(Long creatorId, Date startDate, Date endDate); + + /** + * 全局补齐已过期状态(定时任务使用) + * @return 受影响行数 + */ + int markExpiredCdkeys(); + + /** + * 查询待退还的CDK:已结束(status=1)且未退还(refund_time IS NULL) + */ + List selectRefundableCdkeys(); + + /** + * 对单个CDK执行精确退还(预扣总额 - 实际派发),金币与积分同额。 + * 幂等:重复调用或已退还时返回 0。 + * @return 本次实际退还金额 + */ + BigDecimal refundOne(PromoCdkey cdkey); +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/PromoLoginService.java b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoLoginService.java new file mode 100644 index 0000000..4c74fe5 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoLoginService.java @@ -0,0 +1,32 @@ +package com.ruoyi.promo.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.promo.contract.request.RegisterRequest; +import com.ruoyi.promo.contract.request.UpdateInvitationCodeRequest; +import com.ruoyi.promo.contract.response.GetShareDomainResponse; + +import java.util.List; + +public interface PromoLoginService { + + String login(String username, String password); + + /** + * 推广后台短信验证码登录;手机号未注册时,带上级邀请码则自动注册为主播 + */ + String smsLogin(String phoneNumber, String code, String parentInvitationCode); + + String createUserToken(String phoneNumber); + + R register(RegisterRequest request); + + R updateInvitationCode(UpdateInvitationCodeRequest request); + + R getShareDomain(); + + List queryList(); + + List generateCard(Integer userId, Integer rechargeListId, Integer num); +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/PromoMenuService.java b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoMenuService.java new file mode 100644 index 0000000..509e087 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoMenuService.java @@ -0,0 +1,32 @@ +package com.ruoyi.promo.service; + +import com.ruoyi.promo.domain.entity.PromoMenuConfig; + +import java.util.List; + +/** + * 推广后台菜单权限服务(按角色类型分配) + */ +public interface PromoMenuService { + + /** + * 根据用户类型获取菜单路径 + * 超级管理员返回所有菜单 + */ + List getMenuPathsByUserType(String userType); + + /** + * 超管:保存某角色类型的菜单权限 + */ + void saveRoleMenus(String roleType, List menuPaths); + + /** + * 获取某角色类型已配置的菜单路径 + */ + List getRoleMenuPaths(String roleType); + + /** + * 获取所有可分配的菜单配置(从数据库读取) + */ + List getAllMenuConfigs(); +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/PromoTurnoverService.java b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoTurnoverService.java new file mode 100644 index 0000000..132cae4 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/PromoTurnoverService.java @@ -0,0 +1,76 @@ +package com.ruoyi.promo.service; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.domain.entity.TtCommissionRecord; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.promo.contract.request.GetPurchaseByUserIdRequest; +import com.ruoyi.promo.contract.request.GetSubBranchesRequest; +import com.ruoyi.promo.contract.request.QueryPromoteWelfareRequest; +import com.ruoyi.promo.contract.response.GetSubBranchesResponse; +import com.ruoyi.promo.contract.response.QueryPromotionWelfareResponse; +import com.ruoyi.promo.domain.dto.AnchorDayTurnoverDTO; +import com.ruoyi.promo.domain.vo.AnchorDayTurnoverVO; + +import java.math.BigDecimal; +import java.util.List; + +public interface PromoTurnoverService { + + /** + * 递归全部最下级玩家 + */ + List recursionAllPlayer(Integer userId); + + /** + * 获取实时数据 + */ + AjaxResult getRealTimeData(Integer userId); + + /** + * 获取首页游戏返利看板数据(今日/昨日/本周/累计,按游戏类型拆分) + */ + AjaxResult getGameCommissionData(Integer userId); + + /** + * 获取近10天推广数据 + */ + AjaxResult getLast10DaysPromotionData(Integer userId); + + /** + * 获取名下主播每日流水 + */ + List getAnchorDayTurnover(AnchorDayTurnoverDTO anchorDayTurnoverDTO); + + /** + * 获取下级分支 + */ + GetSubBranchesResponse getSubBranches(GetSubBranchesRequest request); + + /** + * 获取日流水记录 + */ + TableDataInfo getPurchaseByUserId(GetPurchaseByUserIdRequest userId); + + /** + * 获取佣金比例 + */ + BigDecimal getCommissionRateByUserId(Integer userId); + + /** + * 修改佣金比例 + */ + int updateCommissionRate(Integer userId, BigDecimal commissionRate); + + /** + * 获取佣金列表 + */ + List getCommissionList(Integer userId); + + QueryPromotionWelfareResponse queryPromoteWelfare(Integer userId, QueryPromoteWelfareRequest request); + + /** + * 递归获取所有下级用户ID(包含自身,招商→主播+普通用户) + */ + void getAllSubIdsRecursive(Integer parentId, List subUserIds); +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoCardKeyServiceImpl.java b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoCardKeyServiceImpl.java new file mode 100644 index 0000000..4712030 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoCardKeyServiceImpl.java @@ -0,0 +1,199 @@ +package com.ruoyi.promo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.promo.domain.entity.PromoCardKey; +import com.ruoyi.promo.mapper.PromoCardKeyMapper; +import com.ruoyi.promo.service.PromoCardKeyService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; + +@Service +public class PromoCardKeyServiceImpl implements PromoCardKeyService { + + @Autowired + private PromoCardKeyMapper promoCardKeyMapper; + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtUserBlendErcashMapper blendErcashMapper; + + @Override + @Transactional + public R> generateCardKeys(Long creatorId, String creatorName, BigDecimal amount, Integer num) { + if (num == null || num <= 0 || num > 100) { + return R.fail("生成数量必须在1-100之间"); + } + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + return R.fail("金额必须大于0"); + } + + BigDecimal unitPrice = amount; + BigDecimal totalCost = unitPrice.multiply(BigDecimal.valueOf(num)); + + // 扣除卡商专用余额(merchant_balance) + int rows = ttUserMapper.subtractMerchantBalance(creatorId, totalCost); + if (rows <= 0) { + return R.fail("卡商余额不足,需要" + totalCost.toPlainString() + "卡商余额"); + } + + // 查询扣减后余额并记录流水 + TtUser afterUser = ttUserMapper.selectById(creatorId); + BigDecimal newBalance = afterUser.getMerchantBalance() != null ? afterUser.getMerchantBalance() : BigDecimal.ZERO; + TtUserBlendErcash ercash = TtUserBlendErcash.builder() + .userId(creatorId.intValue()) + .amount(BigDecimal.ZERO) + .finalAmount(afterUser.getAccountAmount()) + .credits(totalCost.negate()) + .finalCredits(newBalance) + .total(totalCost.negate()) + .type(TtAccountRecordType.OUTPUT.getCode()) + .source(TtAccountRecordSource.CARD_MERCHANT_GENERATE.getCode()) + .remark(TtAccountRecordSource.CARD_MERCHANT_GENERATE.getMsg() + "(" + num + "张)") + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + blendErcashMapper.insert(ercash); + + // 批量生成卡密 + List cardCodes = new ArrayList<>(); + for (int i = 0; i < num; i++) { + String code = IdUtils.fastSimpleUUID().substring(0, 16).toUpperCase(); + PromoCardKey cardKey = PromoCardKey.builder() + .cardCode(code) + .amount(unitPrice) + .creatorId(creatorId) + .creatorName(creatorName) + .status(0) + .createTime(DateUtils.getNowDate()) + .build(); + promoCardKeyMapper.insert(cardKey); + cardCodes.add(code); + } + + return R.ok(cardCodes); + } + + @Override + @Transactional + public R destroyCardKey(Long cardKeyId, Long operatorId, boolean isSuperAdmin) { + if (cardKeyId == null) { + return R.fail("参数不完整"); + } + PromoCardKey cardKey = promoCardKeyMapper.selectById(cardKeyId); + if (cardKey == null) { + return R.fail("卡密不存在"); + } + // 权限:卡商仅能销毁自己创建的卡密,超管可销毁任意 + if (!isSuperAdmin && (cardKey.getCreatorId() == null || !cardKey.getCreatorId().equals(operatorId))) { + return R.fail("无权销毁他人创建的卡密"); + } + // 前置友好校验:已使用/已销毁不可再销毁 + Integer status = cardKey.getStatus(); + if (status != null && status == 1) { + return R.fail("该卡密已被使用,不能销毁"); + } + if (status != null && status == 2) { + return R.fail("该卡密已销毁,请勿重复操作"); + } + + // 原子幂等:仅当仍为未使用(status=0)时置为已销毁(status=2),防并发兑换/重复销毁导致重复退款 + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(PromoCardKey::getId, cardKeyId) + .eq(PromoCardKey::getStatus, 0) + .set(PromoCardKey::getStatus, 2); + int marked = promoCardKeyMapper.update(null, uw); + if (marked <= 0) { + // 期间被兑换或被其他请求销毁 + return R.fail("卡密状态已变更(可能已被使用或已销毁),操作未生效"); + } + + // 按面值全额退回卡商余额,退给卡密创建者 + BigDecimal refund = cardKey.getAmount() != null ? cardKey.getAmount() : BigDecimal.ZERO; + Long creatorId = cardKey.getCreatorId(); + if (refund.compareTo(BigDecimal.ZERO) > 0 && creatorId != null) { + ttUserMapper.addMerchantBalance(creatorId, refund); + + // 写退款流水(镜像生成时的卡商余额记账约定:credits 携带卡商余额变动,finalCredits 为变动后余额) + TtUser afterUser = ttUserMapper.selectById(creatorId); + BigDecimal newBalance = (afterUser != null && afterUser.getMerchantBalance() != null) + ? afterUser.getMerchantBalance() : BigDecimal.ZERO; + BigDecimal afterAccountAmount = (afterUser != null && afterUser.getAccountAmount() != null) + ? afterUser.getAccountAmount() : BigDecimal.ZERO; + Timestamp now = new Timestamp(System.currentTimeMillis()); + TtUserBlendErcash ercash = TtUserBlendErcash.builder() + .userId(creatorId.intValue()) + .amount(BigDecimal.ZERO) + .finalAmount(afterAccountAmount) + .credits(refund) + .finalCredits(newBalance) + .total(refund) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.CARD_MERCHANT_DESTROY_REFUND.getCode()) + .remark(TtAccountRecordSource.CARD_MERCHANT_DESTROY_REFUND.getMsg() + "(" + cardKey.getCardCode() + ")") + .createTime(now) + .updateTime(now) + .build(); + blendErcashMapper.insert(ercash); + } + + return R.ok("销毁成功,已退回卡商余额 " + refund.toPlainString()); + } + + @Override + public IPage getCardKeyList(Long creatorId, String usedByName, String creatorName, Integer pageNum, Integer pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (creatorId != null) { + wrapper.eq(PromoCardKey::getCreatorId, creatorId); + } + if (StringUtils.isNotBlank(usedByName)) { + wrapper.like(PromoCardKey::getUsedByName, usedByName); + } + if (StringUtils.isNotBlank(creatorName)) { + wrapper.like(PromoCardKey::getCreatorName, creatorName); + } + // 按创建时间倒序,相同时间再按 id 倒序,保证同一批次生成的卡密顺序稳定 + wrapper.orderByDesc(PromoCardKey::getCreateTime).orderByDesc(PromoCardKey::getId); + Page page = new Page<>( + pageNum == null || pageNum < 1 ? 1 : pageNum, + pageSize == null || pageSize < 1 ? 10 : pageSize); + return promoCardKeyMapper.selectPage(page, wrapper); + } + + @Override + public Map getMerchantInfo(Long userId) { + TtUser user = ttUserMapper.selectById(userId); + Map info = new LinkedHashMap<>(); + if (user != null) { + info.put("userName", user.getUserName()); + info.put("nickName", user.getNickName()); + info.put("balance", user.getMerchantBalance() != null ? user.getMerchantBalance() : BigDecimal.ZERO); + } + return info; + } + +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoCdkeyServiceImpl.java b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoCdkeyServiceImpl.java new file mode 100644 index 0000000..6c3d9d5 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoCdkeyServiceImpl.java @@ -0,0 +1,389 @@ +package com.ruoyi.promo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.promo.domain.entity.PromoCdkey; +import com.ruoyi.promo.domain.entity.PromoCdkeyRedeem; +import com.ruoyi.promo.mapper.PromoCdkeyMapper; +import com.ruoyi.promo.mapper.PromoCdkeyRedeemMapper; +import com.ruoyi.promo.service.PromoCdkeyService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 推广CDKEY服务实现 + */ +@Slf4j +@Service +public class PromoCdkeyServiceImpl implements PromoCdkeyService { + + @Autowired + private PromoCdkeyMapper promoCdkeyMapper; + + @Autowired + private PromoCdkeyRedeemMapper promoCdkeyRedeemMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtUserBlendErcashMapper blendErcashMapper; + + /** + * 生成CDKEY + * 表单参数: + * title - CDKEY标题 + * amount1 - 最低金额(金币) + * amount2 - 最高金额(金币) + * num - 每个码可用次数 + * description - 描述 + * openingTime - 开启时间 + * validity - 有效期 + * createNum - 生成码的数量 + */ + @Override + @Transactional + public R> generateCdkeys(Long creatorId, String creatorName, Map form) { + String title = (String) form.get("title"); + if (title == null || title.trim().isEmpty()) { + return R.fail("CDKEY标题不能为空"); + } + + // 解析金额 + BigDecimal amount1 = parseBigDecimal(form.get("amount1")); + BigDecimal amount2 = parseBigDecimal(form.get("amount2")); + if (amount1 == null || amount1.compareTo(BigDecimal.ZERO) < 0) { + return R.fail("最低金额不合法"); + } + if (amount2 == null || amount2.compareTo(amount1) < 0) { + return R.fail("最高金额不能小于最低金额"); + } + + // 解析可用次数(设置上限,防止超大参数导致天量预扣计算/异常数据) + Integer num = parseInteger(form.get("num")); + if (num == null || num <= 0 || num > 1000) { + return R.fail("CDKEY个数必须在1-1000之间"); + } + + // 解析生成数量 + Integer createNum = parseInteger(form.get("createNum")); + if (createNum == null || createNum <= 0 || createNum > 100) { + return R.fail("生成数量必须在1-100之间"); + } + + String description = (String) form.get("description"); + + // 解析时间 + Date openingTime = parseDate(form.get("openingTime")); + if (openingTime == null) { + return R.fail("开启时间不能为空"); + } + Date validity = parseDate(form.get("validity")); + + // 计算总扣款金额 = 最高金额 × 每个码可用次数 × 生成数量(按最大可能派奖金额预扣) + BigDecimal totalCost = amount2.multiply(BigDecimal.valueOf(num)).multiply(BigDecimal.valueOf(createNum)); + + // 校验主播余额是否充足 + TtUser anchorUser = ttUserMapper.selectById(creatorId); + if (anchorUser == null) { + return R.fail("主播账号不存在"); + } + BigDecimal currentBalance = anchorUser.getAccountAmount() != null ? anchorUser.getAccountAmount() : BigDecimal.ZERO; + BigDecimal currentCredits = anchorUser.getAccountCredits() != null ? anchorUser.getAccountCredits() : BigDecimal.ZERO; + if (currentBalance.compareTo(totalCost) < 0) { + return R.fail("余额不足,需要 " + totalCost.toPlainString() + " 金币,当前余额 " + currentBalance.toPlainString()); + } + if (currentCredits.compareTo(totalCost) < 0) { + return R.fail("积分不足,需要 " + totalCost.toPlainString() + " 积分,当前积分 " + currentCredits.toPlainString()); + } + + // 扣减主播余额(金币) + int rows = ttUserMapper.subtractOnlyAccountAmount(creatorId, totalCost); + if (rows <= 0) { + return R.fail("余额扣减失败,余额不足"); + } + // 扣减主播积分 + rows = ttUserMapper.subtractOnlyAccountCredits(creatorId, totalCost); + if (rows <= 0) { + // 积分不足时事务回滚 + throw new RuntimeException("积分扣减失败,积分不足"); + } + + // 查询扣减后余额,写入流水 + TtUser afterUser = ttUserMapper.selectById(creatorId); + BigDecimal newBalance = afterUser.getAccountAmount() != null ? afterUser.getAccountAmount() : BigDecimal.ZERO; + BigDecimal newCredits = afterUser.getAccountCredits() != null ? afterUser.getAccountCredits() : BigDecimal.ZERO; + Timestamp now = new Timestamp(System.currentTimeMillis()); + TtUserBlendErcash ercash = TtUserBlendErcash.builder() + .userId(afterUser.getUserId()) + .amount(totalCost.negate()) + .finalAmount(newBalance) + .credits(totalCost.negate()) + .finalCredits(newCredits) + .total(totalCost.negate()) + .type(TtAccountRecordType.OUTPUT.getCode()) + .source(TtAccountRecordSource.PROMO_CDKEY_GENERATE.getCode()) + .remark(TtAccountRecordSource.PROMO_CDKEY_GENERATE.getMsg() + "(" + createNum + "个码×" + num + "次)") + .createTime(now) + .updateTime(now) + .build(); + blendErcashMapper.insert(ercash); + + // 批量生成CDKEY + List cdkeyCodes = new ArrayList<>(); + for (int i = 0; i < createNum; i++) { + String code = generateCdkeyCode(); + + PromoCdkey cdkey = PromoCdkey.builder() + .title(title.trim()) + .cdkeyCode(code) + .amountMin(amount1) + .amountMax(amount2) + .num(num) + .usableCount(num) + .usedCount(0) + .description(description) + .openingTime(openingTime) + .validity(validity) + .status(0) // 进行中 + .creatorId(creatorId) + .creatorName(creatorName) + .createTime(DateUtils.getNowDate()) + .build(); + promoCdkeyMapper.insert(cdkey); + cdkeyCodes.add(code); + } + + return R.ok(cdkeyCodes, "生成成功"); + } + + @Override + public List getCdkeyList(Long creatorId, Date startDate, Date endDate, Integer status) { + // 自动将已过期的CDKEY状态更新为已结束 + LambdaQueryWrapper expireWrapper = new LambdaQueryWrapper<>(); + expireWrapper.eq(PromoCdkey::getCreatorId, creatorId) + .eq(PromoCdkey::getStatus, 0) + .le(PromoCdkey::getValidity, new Date()); + List expiredList = promoCdkeyMapper.selectList(expireWrapper); + if (!expiredList.isEmpty()) { + List expiredIds = new ArrayList<>(); + for (PromoCdkey c : expiredList) { + expiredIds.add(c.getId()); + } + PromoCdkey updateEntity = new PromoCdkey(); + updateEntity.setStatus(1); + LambdaQueryWrapper updateWrapper = new LambdaQueryWrapper<>(); + updateWrapper.in(PromoCdkey::getId, expiredIds); + promoCdkeyMapper.update(updateEntity, updateWrapper); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PromoCdkey::getCreatorId, creatorId); + if (startDate != null) { + wrapper.ge(PromoCdkey::getCreateTime, startDate); + } + if (endDate != null) { + wrapper.lt(PromoCdkey::getCreateTime, endDate); + } + if (status != null) { + if (status == 2) { + // “已过期”筛选: status=1 且 validity 已过 + wrapper.eq(PromoCdkey::getStatus, 1) + .le(PromoCdkey::getValidity, new Date()); + } else { + wrapper.eq(PromoCdkey::getStatus, status); + } + } + wrapper.orderByDesc(PromoCdkey::getCreateTime); + return promoCdkeyMapper.selectList(wrapper); + } + + @Override + public List getRedeemList(Long creatorId, Date startDate, Date endDate, + String redeemerName, String cdkeyCode) { + // 先查出当前主播创建的所有CDKEY id + LambdaQueryWrapper cdkeyWrapper = new LambdaQueryWrapper<>(); + cdkeyWrapper.eq(PromoCdkey::getCreatorId, creatorId); + List cdkeys = promoCdkeyMapper.selectList(cdkeyWrapper); + if (cdkeys.isEmpty()) { + return Collections.emptyList(); + } + + List cdkeyIds = new ArrayList<>(); + for (PromoCdkey c : cdkeys) { + cdkeyIds.add(c.getId()); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(PromoCdkeyRedeem::getCdkeyId, cdkeyIds); + if (startDate != null) { + wrapper.ge(PromoCdkeyRedeem::getRedeemTime, startDate); + } + if (endDate != null) { + wrapper.lt(PromoCdkeyRedeem::getRedeemTime, endDate); + } + if (redeemerName != null && !redeemerName.trim().isEmpty()) { + wrapper.like(PromoCdkeyRedeem::getRedeemerName, redeemerName.trim()); + } + if (cdkeyCode != null && !cdkeyCode.trim().isEmpty()) { + wrapper.like(PromoCdkeyRedeem::getCdkeyCode, cdkeyCode.trim()); + } + wrapper.orderByDesc(PromoCdkeyRedeem::getRedeemTime); + return promoCdkeyRedeemMapper.selectList(wrapper); + } + + @Override + public List> getCreateDailyReport(Long creatorId, Date startDate, Date endDate) { + return promoCdkeyMapper.getCreateDailyReport(creatorId, startDate, endDate); + } + + @Override + public List> getRedeemDailyReport(Long creatorId, Date startDate, Date endDate) { + return promoCdkeyRedeemMapper.getRedeemDailyReport(creatorId, startDate, endDate); + } + + @Override + public int markExpiredCdkeys() { + return promoCdkeyMapper.markExpired(); + } + + @Override + public List selectRefundableCdkeys() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PromoCdkey::getStatus, 1) + .isNull(PromoCdkey::getRefundTime); + return promoCdkeyMapper.selectList(wrapper); + } + + @Override + @Transactional + public BigDecimal refundOne(PromoCdkey cdkey) { + Long id = cdkey.getId(); + // 精确退还 = 预扣总额(amountMax×num) - 实际已派发金币总额 + BigDecimal amountMax = cdkey.getAmountMax() != null ? cdkey.getAmountMax() : BigDecimal.ZERO; + int num = cdkey.getNum() != null ? cdkey.getNum() : 0; + BigDecimal prepaid = amountMax.multiply(BigDecimal.valueOf(num)); + BigDecimal distributed = promoCdkeyRedeemMapper.sumRedeemedAmount(id); + if (distributed == null) { + distributed = BigDecimal.ZERO; + } + BigDecimal refund = prepaid.subtract(distributed); + if (refund.compareTo(BigDecimal.ZERO) < 0) { + refund = BigDecimal.ZERO; + } + + // 幂等抢占:仅当 refund_time 为空时标记成功,防并发/重复退还 + Date now = new Date(); + int marked = promoCdkeyMapper.markRefunded(id, refund, now); + if (marked <= 0) { + return BigDecimal.ZERO; + } + if (refund.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + + // 回补主播金币与积分(同额,与创建时双向预扣对应) + Long creatorId = cdkey.getCreatorId(); + ttUserMapper.addOnlyAccountAmount(creatorId, refund); + ttUserMapper.addOnlyAccountCredits(creatorId, refund); + + // 写入退还流水 + TtUser afterUser = ttUserMapper.selectById(creatorId); + if (afterUser == null) { + log.warn("[CDK结束退还] creatorId={} 对应的用户不存在(可能已被删除),本次退还中止", creatorId); + return BigDecimal.ZERO; + } + BigDecimal newBalance = afterUser.getAccountAmount() != null ? afterUser.getAccountAmount() : BigDecimal.ZERO; + BigDecimal newCredits = afterUser.getAccountCredits() != null ? afterUser.getAccountCredits() : BigDecimal.ZERO; + Timestamp ts = new Timestamp(System.currentTimeMillis()); + TtUserBlendErcash ercash = TtUserBlendErcash.builder() + .userId(afterUser.getUserId()) + .amount(refund) + .finalAmount(newBalance) + .credits(refund) + .finalCredits(newCredits) + .total(refund) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.PROMO_CDKEY_REFUND.getCode()) + .remark(TtAccountRecordSource.PROMO_CDKEY_REFUND.getMsg() + "(" + cdkey.getCdkeyCode() + ")") + .createTime(ts) + .updateTime(ts) + .build(); + blendErcashMapper.insert(ercash); + + return refund; + } + + // ================= 工具方法 ================= + + /** + * 生成16位CDKEY码,格式: XXXX-XXXX-XXXX-XXXX + */ + private String generateCdkeyCode() { + String raw = IdUtils.fastSimpleUUID().substring(0, 16).toUpperCase(); + return raw.substring(0, 4) + "-" + raw.substring(4, 8) + "-" + + raw.substring(8, 12) + "-" + raw.substring(12, 16); + } + + private BigDecimal parseBigDecimal(Object val) { + if (val == null) return null; + try { + if (val instanceof Number) { + return BigDecimal.valueOf(((Number) val).doubleValue()).setScale(2, RoundingMode.HALF_UP); + } + String s = val.toString().trim(); + if (s.isEmpty()) return null; + return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP); + } catch (Exception e) { + return null; + } + } + + private Integer parseInteger(Object val) { + if (val == null) return null; + if (val instanceof Number) { + return ((Number) val).intValue(); + } + try { + return Integer.parseInt(val.toString().trim()); + } catch (Exception e) { + return null; + } + } + + private Date parseDate(Object val) { + if (val == null) return null; + if (val instanceof Date) return (Date) val; + String s = val.toString().trim(); + if (s.isEmpty()) return null; + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.parse(s); + } catch (ParseException e) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.parse(s); + } catch (ParseException e2) { + return null; + } + } + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoLoginServiceImpl.java b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoLoginServiceImpl.java new file mode 100644 index 0000000..5488f45 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoLoginServiceImpl.java @@ -0,0 +1,533 @@ +package com.ruoyi.promo.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.NumberUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.PromoAdminMapper; +import com.ruoyi.admin.mapper.TtRechargeCardMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.mapper.TtRedPacketMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserAvatarService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.RandomUtil; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.common.utils.uuid.UUID; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.sys.PromoAdmin; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRedPacket; +import com.ruoyi.domain.other.TtUserAvatar; +import com.ruoyi.framework.web.service.SysLoginService; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.promo.contract.request.RegisterRequest; +import com.ruoyi.promo.contract.request.UpdateInvitationCodeRequest; +import com.ruoyi.promo.contract.response.GetShareDomainResponse; +import com.ruoyi.promo.service.PromoLoginService; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.mapper.SysConfigMapper; +import com.ruoyi.thirdparty.common.service.ApiSmsService; +import com.ruoyi.admin.security.ApiUserDetailsServiceImpl; +import com.ruoyi.admin.service.UserIdGenerator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Random; + +@Service +@Slf4j +public class PromoLoginServiceImpl implements PromoLoginService { + + + @Autowired + private SysLoginService sysLoginService; + + @Autowired + private TokenService tokenService; + + @Autowired + private ApiUserDetailsServiceImpl apiUserDetailsService; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private PromoAdminMapper promoAdminMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserAvatarService userAvatarService; + + @Autowired + private ApiSmsService apiSmsService; + + @Autowired + private UserIdGenerator userIdGenerator; + + @Autowired + private SysConfigMapper sysConfigMapper; + + @Autowired + private TtRechargeCardMapper rechargeCardMapper; + + @Autowired + private TtRechargeProdMapper rechargeListMapper; + + @Autowired + private TtRedPacketMapper redPacketMapper; + + @Autowired + private RedisCache redisCache; + + /** 推广码已修改标记 Redis Key 前缀 */ + private static final String INVITATION_CODE_MODIFIED_KEY = "promo:invitation_code_modified:"; + + + /** + * 推广后台账号密码登录(短信未接入前统一走这里): + * ① 超管 → promo_admin(username 或 phone_number) + * ② 招商(04)/主播(01)/卡商(05) → tt_user(账号为手机号) + */ + @Override + public String login(String username, String password) { + sysLoginService.loginPreCheck(username, password); + String account = username.trim(); + + // ① 超级管理员 + PromoAdmin promoAdmin = promoAdminMapper.findByAccount(account); + if (promoAdmin != null) { + if (!SecurityUtils.matchesPassword(password, promoAdmin.getPassword())) { + throw new IllegalArgumentException("账号或密码错误"); + } + // 走 promo_ 前缀,确保拿到的是超管身份(promo_admin 表) + LoginUser loginUser = sysLoginService.createLoginUser("promo_" + promoAdmin.getUsername(), password); + return tokenService.createToken(loginUser); + } + + // ② 招商/主播/卡商 + TtUser user = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .and(w -> w.eq(TtUser::getPhoneNumber, account).or().eq(TtUser::getUserName, account)) + .one(); + if (user == null) { + throw new IllegalArgumentException("账号或密码错误"); + } + if (!UserType.ANCHOR.getCode().equals(user.getUserType()) + && !UserType.BUSINESS_USER.getCode().equals(user.getUserType()) + && !UserType.CARD_MERCHANT.getCode().equals(user.getUserType())) { + throw new IllegalArgumentException("该账号无推广后台登录权限"); + } + if (!SecurityUtils.matchesPassword(password, user.getPassword())) { + throw new IllegalArgumentException("账号或密码错误"); + } + return createUserToken(user.getPhoneNumber()); + } + + /** + * 这个框既能填 4 位短信验证码,也能直接填密码: + * ① 非 4 位数字 → 按密码处理(超管 promo_admin / 招商·主播·卡商 tt_user) + * ② 4 位数字 → 按短信验证码处理 + */ + @Override + public String smsLogin(String phoneNumber, String code, String parentInvitationCode) { + String account = StringUtils.trim(phoneNumber); + String value = StringUtils.trim(code); + + PromoAdmin promoAdmin = StringUtils.isNotEmpty(account) ? promoAdminMapper.findByAccount(account) : null; + TtUser ttUser = StringUtils.isNotEmpty(account) ? findUserByAccount(account) : null; + boolean hasPromoRole = ttUser != null + && !"1".equals(ttUser.getDelFlag()) + && !"1".equals(ttUser.getStatus()) + && (UserType.ANCHOR.getCode().equals(ttUser.getUserType()) + || UserType.BUSINESS_USER.getCode().equals(ttUser.getUserType()) + || UserType.CARD_MERCHANT.getCode().equals(ttUser.getUserType())); + if (promoAdmin == null && ttUser == null && !Validator.isMobile(account)) { + throw new IllegalArgumentException("手机号格式错误,请检查手机号是否输入正确!"); + } + boolean isCode = NumberUtil.isNumber(value) && value.length() == 4; + + // ① 密码登录 + if (!isCode && StringUtils.isNotEmpty(value)) { + if (promoAdmin != null && StringUtils.isNotEmpty(promoAdmin.getPassword()) + && SecurityUtils.matchesPassword(value, promoAdmin.getPassword())) { + // 走 promo_ 前缀,确保拿到的是超管身份 + LoginUser loginUser = sysLoginService.createLoginUser("promo_" + promoAdmin.getUsername(), value); + return tokenService.createToken(loginUser); + } + if (hasPromoRole && StringUtils.isNotEmpty(ttUser.getPassword()) + && SecurityUtils.matchesPassword(value, ttUser.getPassword())) { + return createUserToken(ttUser.getPhoneNumber()); + } + if (promoAdmin != null || hasPromoRole) { + throw new IllegalArgumentException("密码错误"); + } + if (ttUser != null) { + throw new IllegalArgumentException("该账号无推广后台登录权限"); + } + throw new IllegalArgumentException("验证码错误"); + } + + // ② 短信验证码 + if (!isCode) { + throw new IllegalArgumentException("验证码错误"); + } + String validateCaptcha = apiSmsService.validateCaptcha(value, "ApiLogin_" + account); + if (!"success".equals(validateCaptcha)) { + throw new IllegalArgumentException(validateCaptcha); + } + // 手机号没注册过:带上级邀请码则自动注册为主播(口径与 App 的 verificationCodeLoginOrRegister 一致) + autoRegisterAnchorIfNeeded(account, parentInvitationCode); + return createUserToken(account); + } + + /** + * 按手机号或用户名查 tt_user + */ + private TtUser findUserByAccount(String account) { + return new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .and(w -> w.eq(TtUser::getPhoneNumber, account).or().eq(TtUser::getUserName, account)) + .one(); + } + + /** + * 推广后台:手机号未注册 + 有效招商邀请码 → 自动注册为主播(01)并挂在招商下 + */ + private void autoRegisterAnchorIfNeeded(String phoneNumber, String parentInvitationCode) { + boolean registered = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getPhoneNumber, phoneNumber).count() > 0; + if (registered || promoAdminMapper.findByAccount(phoneNumber) != null) { + return; + } + if (StringUtils.isEmpty(parentInvitationCode)) { + throw new IllegalArgumentException("该手机号未注册,请填写上级邀请码后再登录"); + } + TtUser parentUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase()) + .eq(TtUser::getDelFlag, "0") + .eq(TtUser::getStatus, "0").one(); + if (Objects.isNull(parentUser) || !UserType.BUSINESS_USER.getCode().equals(parentUser.getUserType())) { + throw new IllegalArgumentException("上级邀请码填写错误!"); + } + TtUser ttUser = buildPromoUser(phoneNumber, UserType.ANCHOR.getCode(), + cn.hutool.core.util.RandomUtil.randomString(10)); + ttUser.setParentId(parentUser.getUserId()); + ttUser.setUserId(userIdGenerator.generateUserId()); + try { + ttUserMapper.insert(ttUser); + log.info("[推广后台自动注册主播] phone={} parentId={}", phoneNumber, parentUser.getUserId()); + } catch (Exception e) { + log.error("推广后台自动注册主播失败", e); + throw new IllegalArgumentException("注册失败,请稍后重试"); + } + } + + @Override + public String createUserToken(String phoneNumber) { + sysLoginService.validateBlackIp(phoneNumber); + LoginUser loginUser = (LoginUser) apiUserDetailsService.loadUserByUsername("promo_user_" + phoneNumber); + // 超级管理员来自 promo_admin 表,其 id 与 tt_user 不同表,不能按 id 回写 tt_user 登录信息 + if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) { + recordLoginInfo(loginUser.getUserId()); + } + return tokenService.createToken(loginUser); + } + + @Override + public R register(RegisterRequest request) { + if (StringUtils.isEmpty(request.getUsername())) return R.fail("手机号不能为空"); + if (!Validator.isMobile(request.getUsername())) return R.fail("手机号格式错误,请检查手机号是否输入正确!"); + if (StringUtils.isEmpty(request.getPassword())) return R.fail("用户密码不能为空"); + if (StringUtils.isEmpty(request.getUserType())) return R.fail("注册角色不能为空"); + if (!NumberUtil.isNumber(request.getCode()) || request.getCode().trim().length() != 4) + return R.fail("验证码错误"); + + // 推广后台注册始终只允许注册主播(01)账号,任何非主播角色一律拒绝。 + // 招商(04)由后台「生成招商账号」(TtUserServiceImpl.getAccountList)创建, + // 卡商(05)由后台「生成卡商账号」(TtUserServiceImpl.getCardMerchantList)创建,均不经过本接口。 + String userType = request.getUserType(); + if (!UserType.ANCHOR.getCode().equals(userType)) { + return R.fail("推广后台仅支持注册主播账号"); + } + + // 主播必须填写上级邀请码 + if (UserType.ANCHOR.getCode().equals(userType) && StringUtils.isEmpty(request.getParentInvitationCode())) { + return R.fail("主播注册必须填写上级邀请码"); + } + + if (!userService.checkPhoneUnique(TtUser.builder().phoneNumber(request.getUsername()).build())) + return R.fail("注册失败," + "手机号'" + request.getUsername() + "'已被注册!"); + if (!userService.checkUserNameUnique(TtUser.builder().userName(request.getUsername()).build())) + return R.fail("注册失败," + "用户名'" + request.getUsername() + "'已存在!"); + + String phoneNumber = request.getUsername(); + TtUser ttUser = buildPromoUser(phoneNumber, userType, request.getPassword()); + + // 主播需要验证上级邀请码(必须是招商的邀请码) + if (UserType.ANCHOR.getCode().equals(userType)) { + var parentInvitationCode = request.getParentInvitationCode(); + TtUser parentUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase()) + .eq(TtUser::getDelFlag, "0") + .eq(TtUser::getStatus, "0").one(); + if (Objects.isNull(parentUser) || !UserType.BUSINESS_USER.getCode().equals(parentUser.getUserType())) { + return R.fail("上级邀请码填写错误!"); + } + ttUser.setParentId(parentUser.getUserId()); + } + + String validateCaptcha = apiSmsService.validateCaptcha(request.getCode().trim(), "ApiRegister_" + phoneNumber); + if (!"success".equals(validateCaptcha)) return R.fail(validateCaptcha); + + try { + var id = userIdGenerator.generateUserId(); + ttUser.setUserId(id); + ttUserMapper.insert(ttUser); + } catch (DuplicateKeyException e) { + return R.fail("手机号'" + request.getUsername() + "'已被注册!"); + } catch (Exception e) { + log.error("用户注册失败", e); + return R.fail("注册失败,请稍后重试"); + } + ttUser.setPassword(null); + return R.ok(ttUser); + } + + /** + * 构造推广后台账号(注册与自动注册共用,避免两处口径漂移) + */ + private TtUser buildPromoUser(String phoneNumber, String userType, String rawPassword) { + String nickName = RandomUtils.getRandomName(new Random().nextInt(2)); + TtUser ttUser = TtUser.builder().build(); + ttUser.setUserName(phoneNumber); + ttUser.setNickName(nickName); + ttUser.setUserType(userType); + ttUser.setPhoneNumber(phoneNumber); + List userAvatarList = new LambdaQueryChainWrapper<>(userAvatarService.getBaseMapper()) + .eq(TtUserAvatar::getIsDefault, "1").list(); + if (!userAvatarList.isEmpty()) { + ttUser.setAvatar(RandomUtil.choice(userAvatarList).getAvatar()); + } else { + ttUser.setAvatar(""); + } + ttUser.setPassword(SecurityUtils.encryptPassword(rawPassword)); + ttUser.setInvitationCode(getInvitationCode().toLowerCase()); + ttUser.setIsRealCheck("0"); + ttUser.setRemark("明文密码:" + rawPassword); + ttUser.setCreateBy(""); + ttUser.setCreateTime(DateUtils.getNowDate()); + ttUser.setAccountAmount(BigDecimal.ZERO); + ttUser.setAccountCredits(BigDecimal.ZERO); + + // 主播(01)默认返点率:邀请新人充值2、新游戏消费1、经典玩法4 + if (UserType.ANCHOR.getCode().equals(userType)) { + ttUser.setCommissionRate(BigDecimal.valueOf(0)); + ttUser.setNewGameCommissionRate(BigDecimal.valueOf(1)); + ttUser.setClassicGameCommissionRate(BigDecimal.valueOf(3.5)); + } + // 招商(04)默认返点率:邀请新人充值0、新游戏消费0.5、经典玩法0.5 + // 注:注册接口已收紧为仅允许主播(01),此分支为不可达代码,保留仅为减少改动面。 + else if (UserType.BUSINESS_USER.getCode().equals(userType)) { + ttUser.setCommissionRate(BigDecimal.valueOf(0)); + ttUser.setNewGameCommissionRate(new BigDecimal("0.5")); + ttUser.setClassicGameCommissionRate(new BigDecimal("0.5")); + } + return ttUser; + } + + @Override + public R updateInvitationCode(UpdateInvitationCodeRequest request) { + // 验证参数 + if (StringUtils.isEmpty(request.getUsername())) return R.fail("用户名不能为空"); + if (StringUtils.isEmpty(request.getInvitationCode())) return R.fail("邀请码不能为空"); + if (request.getInvitationCode().length() != 6) return R.fail("邀请码长度必须为 6 位"); + + // 从安全上下文中获取当前登录用户 ID,只允许修改自己的邀请码 + Long currentUserId = SecurityUtils.getUserId(); + + // 校验目标用户必须是当前登录用户 + TtUser user = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getUserId, currentUserId) + .eq(TtUser::getDelFlag, "0") + .one(); + + if (Objects.isNull(user)) { + return R.fail("用户不存在"); + } + + // 额外校验:如果按 username 传入,确保 username 属于当前用户 + if (!Objects.equals(user.getUserName(), request.getUsername())) { + return R.fail("用户名不匹配,无权修改他人邀请码"); + } + + // 检查是否已经修改过邀请码(只允许修改一次) + String modifiedKey = INVITATION_CODE_MODIFIED_KEY + user.getUserId(); + if (Boolean.TRUE.equals(redisCache.hasKey(modifiedKey))) { + return R.fail("邀请码只能修改一次"); + } + + // 检查邀请码是否已存在 + TtUser existingUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getInvitationCode, request.getInvitationCode().toLowerCase()) + .eq(TtUser::getDelFlag, "0") + .ne(TtUser::getUserId, user.getUserId()) // 排除当前用户 + .one(); + + if (Objects.nonNull(existingUser)) { + return R.fail("邀请码已存在,请更换其他邀请码"); + } + + // 更新邀请码 + try { + boolean updated = new LambdaUpdateChainWrapper<>(ttUserMapper) + .eq(TtUser::getUserId, user.getUserId()) + .set(TtUser::getInvitationCode, request.getInvitationCode().toLowerCase()) + .update(); + + if (!updated) { + return R.fail("更新邀请码失败"); + } + + // 标记该用户已修改过邀请码(永久标记) + redisCache.setCacheObject(modifiedKey, "1"); + + // 重新获取用户信息 + user = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getUserId, user.getUserId()) + .one(); + user.setPassword(null); // 清除密码 + return R.ok(user); + } catch (Exception e) { + log.error("更新邀请码失败", e); + return R.fail("更新邀请码失败,请稍后重试"); + } + } + + /** + * 检查用户是否已修改过邀请码 + */ + public boolean isInvitationCodeModified(Integer userId) { + String modifiedKey = INVITATION_CODE_MODIFIED_KEY + userId; + return Boolean.TRUE.equals(redisCache.hasKey(modifiedKey)); + } + + @Override + public R getShareDomain() { + GetShareDomainResponse resp = new GetShareDomainResponse(); + SysConfig zhubo = sysConfigMapper.checkConfigKeyUnique("app.zhubo.sharelink"); + SysConfig zhaoshang = sysConfigMapper.checkConfigKeyUnique("app.zhaoshang.sharelink"); + SysConfig user = sysConfigMapper.checkConfigKeyUnique("app.user.sharelink"); + if (zhubo != null) { + resp.setZhuboShareDomain(zhubo.getConfigValue()); + } + if (zhaoshang != null) { + resp.setZhaoshaoShareDomain(zhaoshang.getConfigValue()); + } + if (user != null) { + resp.setUserShareDomain(user.getConfigValue()); + } + return R.ok(resp); + } + + @Override + public List queryList() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.ge(TtRechargeProd::getPrice, BigDecimal.valueOf(100)); + return rechargeListMapper.selectList(wrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List generateCard(Integer userId, Integer rechargeListId, Integer num) { + if (num == null || num <= 0) return new ArrayList<>(); + + // 添加上限保护,防止资源耗尽攻击 + if (num > 100) { + log.warn("生成卡片数量超限:userId={}, num={}", userId, num); + return new ArrayList<>(); + } + + TtRechargeProd recharge = rechargeListMapper.selectById(rechargeListId); + if (recharge == null) return new ArrayList<>(); + + BigDecimal totalPrice = recharge.getPrice().multiply(BigDecimal.valueOf(num)); + var r = userService.updateUserCredits(userId, totalPrice.negate(), TtAccountRecordSource.ZHUBO_EXTRACT); + if (R.isError(r)) { + return new ArrayList<>(); + } + + try { + List cardList = new ArrayList<>(); + List rpList = new ArrayList<>(); + for (int i = 0; i < num; i++) { + TtRedPacket redPacket = new TtRedPacket(); + redPacket.setNum(1); + redPacket.setUserId(userId); + redPacket.setTitle("福利"); + redPacket.setOpeningTime(DateUtils.getNowDate()); + redPacket.setAmount(String.format("[%s,%s]", recharge.getPrice().toPlainString(), recharge.getPrice().toPlainString())); + redPacket.setPassword(IdUtils.fastSimpleUUID().toUpperCase()); + cardList.add(redPacket.getPassword()); + redPacket.setCreateBy(SecurityUtils.getUsername()); + redPacket.setCreateTime(DateUtils.getNowDate()); + redPacket.setUseStatus(0); + rpList.add(redPacket); + } + + redPacketMapper.insert(rpList); + return cardList; + } catch (Exception e) { + log.error("生成卡片失败:userId={}, rechargeListId={}, num={}", userId, rechargeListId, num, e); + // 扣款与发卡已在同一事务内(方法上的 @Transactional),异常时整体回滚即可, + // 不需要再手工补款:手工补款会额外写一条退还流水污染账目,且进程中途异常时补不回来 + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return new ArrayList<>(); + } + } + + public String getInvitationCode() { + while (true) { + try { + String randomInvitationCode = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(); + TtUser ttUser = new LambdaQueryChainWrapper<>(ttUserMapper) + .eq(TtUser::getInvitationCode, randomInvitationCode) + .eq(TtUser::getDelFlag, "0") + .one(); + if (StringUtils.isNull(ttUser)) { + return randomInvitationCode; + } + } catch (Exception e) { + return null; + } + } + } + + /** + * 记录登录信息 + */ + public void recordLoginInfo(Long userId) { + new LambdaUpdateChainWrapper<>(ttUserMapper).eq(TtUser::getUserId, userId) + .set(TtUser::getLoginIp, IpUtils.getIpAddr()) + .set(TtUser::getLoginDate, DateUtils.getNowDate()).update(); + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoMenuServiceImpl.java b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoMenuServiceImpl.java new file mode 100644 index 0000000..a813dc5 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoMenuServiceImpl.java @@ -0,0 +1,96 @@ +package com.ruoyi.promo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.promo.domain.entity.PromoMenuConfig; +import com.ruoyi.promo.domain.entity.PromoRoleMenu; +import com.ruoyi.promo.mapper.PromoMenuConfigMapper; +import com.ruoyi.promo.mapper.PromoRoleMenuMapper; +import com.ruoyi.promo.service.PromoMenuService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class PromoMenuServiceImpl implements PromoMenuService { + + @Autowired + private PromoRoleMenuMapper promoRoleMenuMapper; + + @Autowired + private PromoMenuConfigMapper promoMenuConfigMapper; + + @Override + public List getMenuPathsByUserType(String userType) { + // 从数据库获取该角色类型的菜单配置 + List paths = promoRoleMenuMapper.getMenuPathsByRoleType(userType); + if (paths == null || paths.isEmpty()) { + // 超级管理员(99)和超管(00)没有配置时,默认返回所有菜单 + if ("99".equals(userType) || "00".equals(userType)) { + return getAllMenuPaths(); + } + return new ArrayList<>(); + } + return paths; + } + + @Override + @Transactional + public void saveRoleMenus(String roleType, List menuPaths) { + // 先删除该角色原有的菜单配置 + promoRoleMenuMapper.delete( + new LambdaQueryWrapper().eq(PromoRoleMenu::getRoleType, roleType) + ); + + // 批量插入新的菜单配置(数组顺序即排序顺序) + if (menuPaths != null && !menuPaths.isEmpty()) { + for (int i = 0; i < menuPaths.size(); i++) { + PromoRoleMenu menu = PromoRoleMenu.builder() + .roleType(roleType) + .menuPath(menuPaths.get(i)) + .sortOrder(i) + .createTime(DateUtils.getNowDate()) + .build(); + promoRoleMenuMapper.insert(menu); + } + } + } + + @Override + public List getRoleMenuPaths(String roleType) { + return promoRoleMenuMapper.getMenuPathsByRoleType(roleType); + } + + @Override + public List getAllMenuConfigs() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByAsc(PromoMenuConfig::getSortOrder); + return promoMenuConfigMapper.selectList(wrapper); + } + + /** + * 从数据库获取所有菜单路径(带父路径展开) + */ + private List getAllMenuPaths() { + List configs = getAllMenuConfigs(); + List paths = new ArrayList<>(); + for (PromoMenuConfig config : configs) { + String menuPath = config.getMenuPath(); + // 添加父路径 + String parentPath = menuPath.substring(0, menuPath.lastIndexOf('/')); + if (!parentPath.isEmpty() && !paths.contains(parentPath)) { + paths.add(parentPath); + } + // 添加子路径 + if (!paths.contains(menuPath)) { + paths.add(menuPath); + } + } + return paths; + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoTurnoverServiceImpl.java b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoTurnoverServiceImpl.java new file mode 100644 index 0000000..55185e2 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoTurnoverServiceImpl.java @@ -0,0 +1,877 @@ +package com.ruoyi.promo.service.impl; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.utils.DateTimeUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtCommissionRecord; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.promo.contract.request.GetPurchaseByUserIdRequest; +import com.ruoyi.promo.contract.request.GetSubBranchesRequest; +import com.ruoyi.promo.contract.request.QueryPromoteWelfareRequest; +import com.ruoyi.promo.contract.response.GetSubBranchesResponse; +import com.ruoyi.promo.contract.response.QueryPromotionWelfareResponse; +import com.ruoyi.promo.contract.vo.DailyUserErcashVO; +import com.ruoyi.promo.domain.dto.AnchorDayTurnoverDTO; +import com.ruoyi.promo.domain.param.GetPlayersTurnoverParam; +import com.ruoyi.promo.domain.vo.AnchorDayTurnoverVO; +import com.ruoyi.promo.domain.vo.DayInviteVO; +import com.ruoyi.promo.domain.vo.DayTurnoverVO; +import com.ruoyi.promo.mapper.PromoTurnoverMapper; +import com.ruoyi.promo.service.PromoTurnoverService; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class PromoTurnoverServiceImpl implements PromoTurnoverService { + + @Autowired + private PromoTurnoverMapper promoTurnoverMapper; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private TtUserMapper userMapper; + + @Override + public AjaxResult getRealTimeData(Integer userId) { + // 确定当前用户类型,决定直属下级类型 + TtUser currentUser = userMapper.selectById(userId); + String childType = UserType.COMMON_USER.getCode(); // 默认查询普通用户 + if (currentUser != null) { + if (UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType())) { + childType = UserType.ANCHOR.getCode(); // 招商的下级是主播 + } else if (UserType.ANCHOR.getCode().equals(currentUser.getUserType())) { + childType = UserType.COMMON_USER.getCode(); // 主播的下级是普通用户 + } + } + + // 获取所有下级用户ID(一次递归,替代原来的 recursionAllPlayer + getAllSubUserIdsRecursive 两次遍历) + List allSubUserIds = new ArrayList<>(); + getAllSubUserIdsRecursive(userId, allSubUserIds); + + // 招商用户:游戏消费只算普通用户(不含主播),充值仍算全部下级 + boolean isBusiness = currentUser != null && UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType()); + List gameUserIds = allSubUserIds; + if (isBusiness && !allSubUserIds.isEmpty()) { + Set anchorIds = new LambdaQueryChainWrapper<>(userMapper) + .in(TtUser::getUserId, allSubUserIds) + .eq(TtUser::getUserType, UserType.ANCHOR.getCode()) + .list() + .stream().map(TtUser::getUserId).collect(Collectors.toSet()); + gameUserIds = allSubUserIds.stream() + .filter(id -> !anchorIds.contains(id)) + .collect(Collectors.toList()); + if (gameUserIds.isEmpty()) { + gameUserIds = Collections.singletonList(userId); // 避免空列表导致SQL报错 + } + } + + // 时间范围 + Date todayStart = getStartOfToday().getTime(); + Date todayEnd = new Date(todayStart.getTime() + 86400000L); + Date yesterdayStart = getStartOfYesterday().getTime(); + Date yesterdayEnd = new Date(yesterdayStart.getTime() + 86400000L); + + Map map = new HashMap<>(); + + // === 新增下级(招商→主播数/天,主播→普通用户数/天)=== + map.put("todayNewSub", promoTurnoverMapper.countNewSubByDate(userId, childType, todayStart, todayEnd)); + map.put("yesterdayNewSub", promoTurnoverMapper.countNewSubByDate(userId, childType, yesterdayStart, yesterdayEnd)); + + // === 总佣金(新人充值×充值返点比例 + 新游戏消费×新游戏返点 + 经典玩法消费×经典返点)=== + BigDecimal hundred = BigDecimal.valueOf(100); + BigDecimal commissionRate = currentUser != null && currentUser.getCommissionRate() != null + ? currentUser.getCommissionRate().divide(hundred, 6, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO; + BigDecimal newGameRate = currentUser != null && currentUser.getNewGameCommissionRate() != null + ? currentUser.getNewGameCommissionRate().divide(hundred, 6, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO; + BigDecimal classicGameRate = currentUser != null && currentUser.getClassicGameCommissionRate() != null + ? currentUser.getClassicGameCommissionRate().divide(hundred, 6, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO; + + BigDecimal todayCommission = BigDecimal.ZERO; + BigDecimal yesterdayCommission = BigDecimal.ZERO; + + if (CollectionUtils.isNotEmpty(allSubUserIds)) { + // === 佣金数据:一次查询替代原来6次(充值×2 + 新游戏×2 + 经典×2)=== + Map commData = promoTurnoverMapper.getBatchedCommissionData( + allSubUserIds, gameUserIds, todayStart, todayEnd, yesterdayStart, yesterdayEnd); + BigDecimal todaySubRecharge = toBd(commData, "todayRecharge"); + BigDecimal yesterdaySubRecharge = toBd(commData, "yesterdayRecharge"); + BigDecimal todayNewGameConsume = toBd(commData, "todayNewGame"); + BigDecimal yesterdayNewGameConsume = toBd(commData, "yesterdayNewGame"); + BigDecimal todayClassicConsume = toBd(commData, "todayClassic"); + BigDecimal yesterdayClassicConsume = toBd(commData, "yesterdayClassic"); + + todayCommission = commissionRate.multiply(todaySubRecharge) + .add(newGameRate.multiply(todayNewGameConsume)) + .add(classicGameRate.multiply(todayClassicConsume)); + yesterdayCommission = commissionRate.multiply(yesterdaySubRecharge) + .add(newGameRate.multiply(yesterdayNewGameConsume)) + .add(classicGameRate.multiply(yesterdayClassicConsume)); + + // === 下级充值(复用上面的批量结果)=== + map.put("todaySubRecharge", todaySubRecharge); + map.put("yesterdaySubRecharge", yesterdaySubRecharge); + + // === 所有下级总充值(终身累计,时间边界传 null)=== + BigDecimal totalSubRecharge = promoTurnoverMapper.getSubTotalRecharge(allSubUserIds, null, null); + map.put("totalSubRecharge", totalSubRecharge != null ? totalSubRecharge : BigDecimal.ZERO); + + // === 下级收益(所有下级用户的提货总价值)=== + BigDecimal todaySubIncome = promoTurnoverMapper.getSubDeliveryValueByDate(allSubUserIds, todayStart, todayEnd); + BigDecimal yesterdaySubIncome = promoTurnoverMapper.getSubDeliveryValueByDate(allSubUserIds, yesterdayStart, yesterdayEnd); + map.put("todaySubIncome", todaySubIncome != null ? todaySubIncome : BigDecimal.ZERO); + map.put("yesterdaySubIncome", yesterdaySubIncome != null ? yesterdaySubIncome : BigDecimal.ZERO); + + // 查询名下主播数 + map.put("anchorCount", promoTurnoverMapper.getAnchorCount(userId)); + + // === 消费流水:招商=下级主播+主播下级(普通用户);主播=下级普通用户,均取 allSubUserIds === + Map batched = promoTurnoverMapper.getBatchedTurnover( + allSubUserIds, new ArrayList<>(TtAccountRecordSource.getWelfarePrizeCodes())); + if (batched != null) { + map.put("totalTurnover", batched.get("totalTurnover")); + map.put("lastMonthTurnover", batched.get("lastMonth")); + map.put("currentMonthTurnover", batched.get("currentMonth")); + map.put("lastWeekTurnover", batched.get("lastWeek")); + map.put("currentWeekTurnover", batched.get("currentWeek")); + map.put("lastDayTurnover", batched.get("lastDay")); + map.put("currentDayTurnover", batched.get("currentDay")); + } else { + map.put("totalTurnover", BigDecimal.ZERO); + map.put("lastMonthTurnover", BigDecimal.ZERO); + map.put("currentMonthTurnover", BigDecimal.ZERO); + map.put("lastWeekTurnover", BigDecimal.ZERO); + map.put("currentWeekTurnover", BigDecimal.ZERO); + map.put("lastDayTurnover", BigDecimal.ZERO); + map.put("currentDayTurnover", BigDecimal.ZERO); + } + } else { + map.put("anchorCount", 0); + map.put("totalTurnover", BigDecimal.ZERO); + map.put("lastMonthTurnover", BigDecimal.ZERO); + map.put("currentMonthTurnover", BigDecimal.ZERO); + map.put("lastWeekTurnover", BigDecimal.ZERO); + map.put("currentWeekTurnover", BigDecimal.ZERO); + map.put("lastDayTurnover", BigDecimal.ZERO); + map.put("currentDayTurnover", BigDecimal.ZERO); + map.put("todaySubRecharge", BigDecimal.ZERO); + map.put("yesterdaySubRecharge", BigDecimal.ZERO); + map.put("totalSubRecharge", BigDecimal.ZERO); + map.put("todaySubIncome", BigDecimal.ZERO); + map.put("yesterdaySubIncome", BigDecimal.ZERO); + } + + map.put("todayCommission", todayCommission); + map.put("yesterdayCommission", yesterdayCommission); + + // === 个人消费福利:一次查询替代原来2次(today + yesterday)=== + Map welfare = promoTurnoverMapper.getBatchedWelfare( + userId, todayStart, todayEnd, yesterdayStart, yesterdayEnd); + map.put("todayPersonalWelfare", toBd(welfare, "todayWelfare")); + map.put("yesterdayPersonalWelfare", toBd(welfare, "yesterdayWelfare")); + + return AjaxResult.success(map); + } + + @Override + public AjaxResult getGameCommissionData(Integer userId) { + TtUser currentUser = userMapper.selectById(userId); + String childType = UserType.COMMON_USER.getCode(); + boolean isBusiness = false; + if (currentUser != null) { + if (UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType())) { + childType = UserType.ANCHOR.getCode(); + isBusiness = true; + } else if (UserType.ANCHOR.getCode().equals(currentUser.getUserType())) { + childType = UserType.COMMON_USER.getCode(); + } + } + + List allSubUserIds = new ArrayList<>(); + getAllSubUserIdsRecursive(userId, allSubUserIds); + + List gameUserIds = allSubUserIds; + if (isBusiness && !allSubUserIds.isEmpty()) { + Set anchorIds = new LambdaQueryChainWrapper<>(userMapper) + .in(TtUser::getUserId, allSubUserIds) + .eq(TtUser::getUserType, UserType.ANCHOR.getCode()) + .list() + .stream().map(TtUser::getUserId).collect(Collectors.toSet()); + gameUserIds = allSubUserIds.stream() + .filter(id -> !anchorIds.contains(id)) + .collect(Collectors.toList()); + } + + Date todayStart = getStartOfToday().getTime(); + Date todayEnd = new Date(todayStart.getTime() + 86400000L); + Date yesterdayStart = getStartOfYesterday().getTime(); + Date yesterdayEnd = new Date(yesterdayStart.getTime() + 86400000L); + Date[] weekRange = getCurrentWeekRange(); + Date weekStart = weekRange[0]; + Date weekEnd = weekRange[1]; + + Map result = new LinkedHashMap<>(); + + // 当前登录用户的后台返佣比例 + Map rates = new LinkedHashMap<>(); + rates.put("rechargeRate", currentUser != null && currentUser.getCommissionRate() != null + ? currentUser.getCommissionRate() : BigDecimal.ZERO); + rates.put("newGameRate", currentUser != null && currentUser.getNewGameCommissionRate() != null + ? currentUser.getNewGameCommissionRate() : BigDecimal.ZERO); + rates.put("classicGameRate", currentUser != null && currentUser.getClassicGameCommissionRate() != null + ? currentUser.getClassicGameCommissionRate() : BigDecimal.ZERO); + result.put("rates", rates); + + // 各游戏消费及充值数据 + Map data; + if (CollectionUtils.isEmpty(allSubUserIds)) { + data = createZeroGameCommissionData(); + } else { + List effectiveGameUserIds = gameUserIds.isEmpty() + ? Collections.singletonList(-1) : gameUserIds; + data = promoTurnoverMapper.getGameCommissionData( + effectiveGameUserIds, + allSubUserIds, + todayStart, todayEnd, + yesterdayStart, yesterdayEnd, + weekStart, weekEnd); + } + + // 新增下级 + int todayNewSub = promoTurnoverMapper.countNewSubByDate(userId, childType, todayStart, todayEnd); + int yesterdayNewSub = promoTurnoverMapper.countNewSubByDate(userId, childType, yesterdayStart, yesterdayEnd); + int weekNewSub = promoTurnoverMapper.countNewSubByDate(userId, childType, weekStart, weekEnd); + int totalNewSub = CollectionUtils.isEmpty(allSubUserIds) ? 0 : allSubUserIds.size(); + + BigDecimal rechargeRate = toBd(rates, "rechargeRate"); + BigDecimal newGameRate = toBd(rates, "newGameRate"); + BigDecimal classicGameRate = toBd(rates, "classicGameRate"); + + // 按周期组装结果:今日/昨日/本周/总 + List> periods = new ArrayList<>(); + periods.add(buildPeriod("今日", data, + rechargeRate, newGameRate, classicGameRate, + "todayClassicBox", "todayLuckyUpgrade", "todayBattle", "todayCrazyWheel", "todayOlympus", + "todayRecharge", todayNewSub)); + periods.add(buildPeriod("昨日", data, + rechargeRate, newGameRate, classicGameRate, + "yesterdayClassicBox", "yesterdayLuckyUpgrade", "yesterdayBattle", "yesterdayCrazyWheel", "yesterdayOlympus", + "yesterdayRecharge", yesterdayNewSub)); + periods.add(buildPeriod("本周", data, + rechargeRate, newGameRate, classicGameRate, + "weekClassicBox", "weekLuckyUpgrade", "weekBattle", "weekCrazyWheel", "weekOlympus", + "weekRecharge", weekNewSub)); + periods.add(buildPeriod("总", data, + rechargeRate, newGameRate, classicGameRate, + "totalClassicBox", "totalLuckyUpgrade", "totalBattle", "totalCrazyWheel", "totalOlympus", + "totalRecharge", totalNewSub)); + + result.put("periods", periods); + return AjaxResult.success(result); + } + + @Override + public AjaxResult getLast10DaysPromotionData(Integer userId) { + // 分离收集下级:主播(01) 用于"主播日流水/主播日邀请",普通用户(02) 用于"主播下级日流水/主播下级日邀请" + List anchorIds = new ArrayList<>(); + List subUserIds = new ArrayList<>(); + collectAnchorAndSubIds(userId, anchorIds, subUserIds); + if (CollectionUtils.isEmpty(anchorIds) && CollectionUtils.isEmpty(subUserIds)) { + return AjaxResult.success(new ArrayList<>()); + } + + List sourceCodes = new ArrayList<>(TtAccountRecordSource.getWelfarePrizeCodes()); + + Map anchorTurnoverMap = CollectionUtils.isEmpty(anchorIds) + ? Collections.emptyMap() + : toTurnoverDayMap(promoTurnoverMapper.getLast10DaysTurnover(anchorIds, sourceCodes)); + Map subTurnoverMap = CollectionUtils.isEmpty(subUserIds) + ? Collections.emptyMap() + : toTurnoverDayMap(promoTurnoverMapper.getLast10DaysTurnover(subUserIds, sourceCodes)); + Map anchorInviteMap = CollectionUtils.isEmpty(anchorIds) + ? Collections.emptyMap() + : toInviteDayMap(promoTurnoverMapper.getLast10DaysInviteByUserIds(anchorIds)); + Map subInviteMap = CollectionUtils.isEmpty(subUserIds) + ? Collections.emptyMap() + : toInviteDayMap(promoTurnoverMapper.getLast10DaysInviteByUserIds(subUserIds)); + + // 各查询均为近10天 date_range 左连接,返回同一组日期;合并后升序作为横轴 + Set dates = new TreeSet<>(); + dates.addAll(anchorTurnoverMap.keySet()); + dates.addAll(subTurnoverMap.keySet()); + dates.addAll(anchorInviteMap.keySet()); + dates.addAll(subInviteMap.keySet()); + + List> list = new ArrayList<>(); + for (String date : dates) { + Map map = new HashMap<>(); + map.put("date", date); + map.put("anchorDayTurnover", anchorTurnoverMap.getOrDefault(date, BigDecimal.ZERO)); + map.put("subDayTurnover", subTurnoverMap.getOrDefault(date, BigDecimal.ZERO)); + map.put("anchorDayInvite", anchorInviteMap.getOrDefault(date, 0)); + map.put("subDayInvite", subInviteMap.getOrDefault(date, 0)); + list.add(map); + } + return AjaxResult.success(list); + } + + /** + * 递归分离收集下级用户ID: + * 主播(01) 归入 anchorIds 并继续下钻,普通用户(02) 归入 subUserIds。 + */ + private void collectAnchorAndSubIds(int parentId, List anchorIds, List subUserIds) { + List users = promoTurnoverMapper.findByParentId(parentId); + for (TtUser user : users) { + if (UserType.ANCHOR.getCode().equals(user.getUserType())) { + anchorIds.add(user.getUserId()); + collectAnchorAndSubIds(user.getUserId(), anchorIds, subUserIds); + } else if (UserType.COMMON_USER.getCode().equals(user.getUserType())) { + subUserIds.add(user.getUserId()); + } + } + } + + private Map toTurnoverDayMap(List list) { + Map map = new HashMap<>(); + if (list == null) return map; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + for (DayTurnoverVO vo : list) { + map.put(sdf.format(vo.getDate()), vo.getAmount()); + } + return map; + } + + private Map toInviteDayMap(List list) { + Map map = new HashMap<>(); + if (list == null) return map; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + for (DayInviteVO vo : list) { + map.put(sdf.format(vo.getDate()), vo.getAnchorCount()); + } + return map; + } + + @Override + public List getAnchorDayTurnover(AnchorDayTurnoverDTO anchorDayTurnoverDTO) { + // 递归名下所有玩家 + Integer userId = anchorDayTurnoverDTO.getUserId(); + List allPlayer = recursionAllPlayer(userId); + List userIds = new ArrayList<>(); + for (TtUser ttUser : allPlayer) { + userIds.add(ttUser.getUserId()); + } + if (CollectionUtils.isEmpty(userIds)) { + return Collections.emptyList(); + } + anchorDayTurnoverDTO.setUserIds(userIds); + anchorDayTurnoverDTO.setSourceCodes(new ArrayList<>(TtAccountRecordSource.getWelfarePrizeCodes())); + + List result = promoTurnoverMapper.getAnchorDayTurnover(anchorDayTurnoverDTO); + for (AnchorDayTurnoverVO anchorDayTurnoverVO : result) { + Date startDate = anchorDayTurnoverVO.getDate(); + Date endDate = DateTimeUtils.addDays(startDate, 1); + BigDecimal dailyTotalRecharge = promoTurnoverMapper.getDailyTotalRecharge(anchorDayTurnoverVO.getUserId(), startDate, endDate); + anchorDayTurnoverVO.setDailyRechargeTotal(dailyTotalRecharge == null ? BigDecimal.ZERO : dailyTotalRecharge); + } + return result; + } + + @Override + public GetSubBranchesResponse getSubBranches(GetSubBranchesRequest request) { + if (request.getPageNum() == null || request.getPageNum() <= 0) { + request.setPageNum(1); + } + if (request.getPageSize() == null || request.getPageSize() <= 0) { + request.setPageSize(10); + } + Long userId = Long.valueOf(request.getUserId()); + var query = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getParentId, userId); + + if (request.getPlayerId() != null) { + query.eq(TtUser::getUserId, request.getPlayerId()); + } + + Page page = new Page<>(request.getPageNum(), request.getPageSize()); + page.setOptimizeCountSql(false); + + var pageData = query.page(page); + + var childrenList = pageData.getRecords(); + + if (CollectionUtils.isEmpty(childrenList)) { + GetSubBranchesResponse response = new GetSubBranchesResponse(); + response.setTotal((int) page.getTotal()); + response.setRows(new ArrayList<>()); + return response; + } + + List userids = childrenList.stream().map(TtUser::getUserId).collect(Collectors.toList()); + List ercashList = new LambdaQueryChainWrapper<>(userBlendErcashMapper) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getWelfarePrizeCodes()) + .in(TtUserBlendErcash::getUserId, userids).list(); + Map> ercashMap = ercashList.stream() + .collect(Collectors.groupingBy(TtUserBlendErcash::getUserId)); + + List resultList = childrenList.stream().map(user -> { + GetSubBranchesResponse.UserInfo userInfo = new GetSubBranchesResponse.UserInfo(); + BeanUtils.copyProperties(user, userInfo); + + userInfo.setTotalCost(BigDecimal.ZERO); + userInfo.setTotalRecharge(BigDecimal.ZERO); + + if (ercashMap.containsKey(user.getUserId())) { + var records = ercashMap.get(user.getUserId()); + + var totalRecharge = records.stream().filter(v -> { + return Objects.equals(v.getSource(), TtAccountRecordSource.RECHARGE.getCode()); + }).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add); + + var allConsumeList = records.stream().filter(v -> { + return Objects.equals(v.getType(), TtAccountRecordType.OUTPUT.getCode()); + }).toList(); + + var totalNormalGameConsume = allConsumeList.stream().filter(v -> { + return TtAccountRecordSource.isNormalGameConsume(v.getSource()); + }).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add); + + var totalSpecialGameConsume = allConsumeList.stream().filter(v -> { + return TtAccountRecordSource.isSpecialGameConsume(v.getSource()); + }).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add); + + + userInfo.setTotalNormGameCost(totalNormalGameConsume); + userInfo.setTotalSpecialGameCost(totalSpecialGameConsume); + userInfo.setTotalRecharge(totalRecharge); + } + + return userInfo; + }).collect(Collectors.toList()); + + GetSubBranchesResponse response = new GetSubBranchesResponse(); + response.setTotal((int) page.getTotal()); + response.setRows(resultList); + return response; + } + + private Set findAllChild(Integer parentId, Set result) { + + List list = promoTurnoverMapper.findByParentId(parentId); + HashSet childrens = new HashSet<>(list); + + if (childrens.isEmpty()) return result; + + result.addAll(childrens); + for (TtUser child : childrens) { + Set allChild = findAllChild(child.getUserId(), result); + result.addAll(allChild); + } + return result; + } + + @Override + public TableDataInfo getPurchaseByUserId(GetPurchaseByUserIdRequest request) { + if (request.getPageNum() == null || request.getPageNum() <= 0) { + request.setPageNum(1); + } + if (request.getPageSize() == null || request.getPageSize() <= 0) { + request.setPageSize(10); + } + if (request.getBeginTime() == null) { + LocalDateTime endTime = LocalDateTime.now(); + LocalDateTime startTime = endTime.minusDays(30).with(LocalTime.MIN); + request.setBeginTime(DateTimeUtils.date(startTime)); + } + if (request.getEndTime() == null) { + LocalDateTime endTime = LocalDateTime.now(); + request.setEndTime(DateTimeUtils.date(endTime)); + } + // Limit the time range to maximum 30 days + if (request.getBeginTime() != null && request.getEndTime() != null) { + LocalDateTime begin = DateTimeUtils.toLocalDateTime(DateTimeUtils.dateTime(DateTimeUtils.YYYY_MM_DD, request.getBeginTime())); + LocalDateTime end = DateTimeUtils.toLocalDateTime(DateTimeUtils.dateTime(DateTimeUtils.YYYY_MM_DD, request.getEndTime())); + if (ChronoUnit.DAYS.between(begin, end) > 30) { + LocalDateTime adjustedEnd = begin.plusDays(30); + request.setEndTime(DateTimeUtils.date(adjustedEnd)); + } + } + + // 权限校验:只能查询自己下级的流水 + Long currentUserId = SecurityUtils.getUserId(); + Integer queryUserId = request.getUserId(); + + // 招商账号:可以查所有下级(递归) + // 主播账号:只能查自己或自己的直属下级 + TtUser currentUser = userMapper.selectById(currentUserId.intValue()); + boolean isBusiness = currentUser != null && UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType()); + + if (!isBusiness) { + // 主播账号:只能查自己 + if (!queryUserId.equals(currentUserId.intValue())) { + var result = new TableDataInfo(); + result.setCode(HttpStatus.ERROR); + result.setMsg("无权查看他人数据"); + return result; + } + } else { + // 招商账号:需要校验 queryUserId 是否属于当前招商的下级树 + List allSubUserIds = new ArrayList<>(); + getAllSubUserIdsRecursive(currentUserId.intValue(), allSubUserIds); + if (!allSubUserIds.contains(queryUserId)) { + var result = new TableDataInfo(); + result.setCode(HttpStatus.ERROR); + result.setMsg("无权查看该用户数据"); + return result; + } + } + + List days = DateTimeUtils.generateDailyDates(request.getBeginTime(), request.getEndTime()); + days.sort(Comparator.reverseOrder()); + + int total = days.size(); + int start = Math.min((request.getPageNum() - 1) * request.getPageSize(), total - 1); + int end = Math.min(start + request.getPageSize(), total); + start = Math.max(start, 0); + end = Math.max(end, 0); + days = days.subList(start, end); + + + + List resultList = days.stream().map(d -> { + var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper) + .eq(TtUserBlendErcash::getUserId, request.getUserId()) + .apply("DATE(create_time) = {0}", d) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getWelfarePrizeCodes()) + .list(); + var totalRecharge = records.stream().filter(v -> { + return Objects.equals(v.getSource(), TtAccountRecordSource.RECHARGE.getCode()); + }).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add); + + var allConsumeList = records.stream().filter(v -> { + return Objects.equals(v.getType(), TtAccountRecordType.OUTPUT.getCode()); + }).toList(); + + var totalNormalGameConsume = allConsumeList.stream().filter(v -> { + return TtAccountRecordSource.isNormalGameConsume(v.getSource()); + }).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add); + + var totalSpecialGameConsume = allConsumeList.stream().filter(v -> { + return TtAccountRecordSource.isSpecialGameConsume(v.getSource()); + }).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add); + + DailyUserErcashVO r = new DailyUserErcashVO(); + r.setDate(d); + r.setDailyNormGameCost(totalNormalGameConsume); + r.setDailySpecialGameCost(totalSpecialGameConsume); + r.setDailyRecharge(totalRecharge); + return r; + }).collect(Collectors.toList()); + + TableDataInfo result = new TableDataInfo(); + result.setCode(200); + result.setRows(resultList); + result.setTotal(total); + return result; + + } + + @Override + public BigDecimal getCommissionRateByUserId(Integer userId) { + return promoTurnoverMapper.getCommissionRateByUserId(userId); + } + + @Override + public int updateCommissionRate(Integer userId, BigDecimal commissionRate) { + return promoTurnoverMapper.updateCommissionRate(userId, commissionRate); + } + + @Override + public List getCommissionList(Integer userId) { + return promoTurnoverMapper.getCommissionList(userId); + } + + @Override + public QueryPromotionWelfareResponse queryPromoteWelfare(Integer userId, QueryPromoteWelfareRequest request) { + Page page = new Page<>(request.getPageNum(), request.getPageSize()); + page.setOptimizeCountSql(false); + + LambdaQueryChainWrapper wrapper = new LambdaQueryChainWrapper<>(userBlendErcashMapper) + .eq(TtUserBlendErcash::getUserId, userId) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.P_WELFARE.getCode()); + + if (request.getBeginTime() != null) { + wrapper.ge(TtUserBlendErcash::getCreateTime, request.getBeginTime()); + } + if (request.getEndTime() != null) { + wrapper.le(TtUserBlendErcash::getCreateTime, request.getEndTime()); + } + wrapper.orderByDesc(TtUserBlendErcash::getCreateTime); + Page pageResult = wrapper.page(page); + + BigDecimal totalReward = promoTurnoverMapper.queryTotalRewardBySource(userId, TtAccountRecordSource.P_WELFARE.getCode()); + if (totalReward == null) { + totalReward = BigDecimal.ZERO; + } + if (CollectionUtils.isEmpty(pageResult.getRecords())) { + QueryPromotionWelfareResponse result = new QueryPromotionWelfareResponse(); + result.setRows(new ArrayList<>()); + result.setTotal(0); + result.setTotalReward(totalReward); + return result; + } + + QueryPromotionWelfareResponse result = new QueryPromotionWelfareResponse(); + result.setRows(pageResult.getRecords().stream().map(v -> { + QueryPromotionWelfareResponse.Record r = new QueryPromotionWelfareResponse.Record(); + r.setReward(v.getAmount()); + r.setDate(DateTimeUtils.dateTime(v.getCreateTime().toLocalDateTime())); + return r; + }).collect(Collectors.toList())); + result.setTotalReward(totalReward); + result.setTotal((int) pageResult.getTotal()); + return result; + } + + /** + * 递归下级分支 + */ + private void buildUserTree(TtUser ttUser) { + // 获取当前用户的子节点 + List children = promoTurnoverMapper.findByParentId(ttUser.getUserId()); + // 设置当前用户的子节点 + ttUser.setChildren(children); + for (TtUser child : children) { + // 如果子节点是主播,继续递归 + if ("01".equals(child.getUserType())) { + buildUserTree(child); + } + } + } + + @Override + public List recursionAllPlayer(Integer userId) { + List players = new ArrayList<>(); + getAllPlayersByAnchorIdRecursive(userId, players); + return players; + } + + /** + * 递归名下所有玩家(仅普通用户type=02) + */ + private void getAllPlayersByAnchorIdRecursive(int parentId, List players) { + List users = promoTurnoverMapper.findByParentId(parentId); + for (TtUser user : users) { + if ("02".equals(user.getUserType())) { // 如果是玩家 + players.add(user); + } else if ("01".equals(user.getUserType())) { // 如果是主播,继续递归 + getAllPlayersByAnchorIdRecursive(user.getUserId(), players); + } + } + } + + /** + * 递归获取所有下级用户ID(包含主播和普通用户) + * 招商→主播+普通用户,主播→普通用户 + */ + private void getAllSubUserIdsRecursive(int parentId, List subUserIds) { + List users = promoTurnoverMapper.findByParentId(parentId); + for (TtUser user : users) { + subUserIds.add(user.getUserId()); + if ("01".equals(user.getUserType())) { // 如果是主播,继续递归其下级 + getAllSubUserIdsRecursive(user.getUserId(), subUserIds); + } + } + } + + @Override + public void getAllSubIdsRecursive(Integer parentId, List subUserIds) { + getAllSubUserIdsRecursive(parentId, subUserIds); + } + + private Calendar getStartOfLastMonth() { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MONTH, -1); + calendar.set(Calendar.DAY_OF_MONTH, 1); + resetTime(calendar); + return calendar; + } + + private Calendar getEndOfLastMonth() { + Calendar calendar = getStartOfLastMonth(); + calendar.add(Calendar.MONTH, 1); + calendar.add(Calendar.DAY_OF_MONTH, -1); + setTimeToEndOfDay(calendar); + return calendar; + } + + private Calendar getStartOfThisMonth() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.DAY_OF_MONTH, 1); + resetTime(calendar); + return calendar; + } + + private Calendar getStartOfLastWeek() { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.WEEK_OF_YEAR, -1); + calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); + resetTime(calendar); + return calendar; + } + + private Calendar getEndOfLastWeek() { + Calendar calendar = getStartOfLastWeek(); + calendar.add(Calendar.DAY_OF_WEEK, 6); + setTimeToEndOfDay(calendar); + return calendar; + } + + private Calendar getStartOfThisWeek() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); + resetTime(calendar); + return calendar; + } + + private Calendar getStartOfYesterday() { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, -1); + resetTime(calendar); + return calendar; + } + + private Calendar getEndOfYesterday() { + Calendar calendar = getStartOfYesterday(); + setTimeToEndOfDay(calendar); + return calendar; + } + + private Calendar getStartOfToday() { + Calendar calendar = Calendar.getInstance(); + resetTime(calendar); + return calendar; + } + + private void resetTime(Calendar calendar) { + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + } + + private void setTimeToEndOfDay(Calendar calendar) { + calendar.set(Calendar.HOUR_OF_DAY, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 999); + } + + /** 从 Map 中安全取 BigDecimal,null 返回 ZERO */ + private BigDecimal toBd(Map map, String key) { + if (map == null) return BigDecimal.ZERO; + Object v = map.get(key); + return v instanceof BigDecimal ? (BigDecimal) v : BigDecimal.ZERO; + } + + /** + * 获取本周一 00:00:00 到下周周一 00:00:00(左闭右开),与 weeklyDailyCommission 口径一致。 + */ + private Date[] getCurrentWeekRange() { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + int daysSinceMonday = (cal.get(Calendar.DAY_OF_WEEK) + 5) % 7; + cal.add(Calendar.DAY_OF_MONTH, -daysSinceMonday); + Date start = cal.getTime(); + cal.add(Calendar.DAY_OF_MONTH, 7); + Date end = cal.getTime(); + return new Date[]{start, end}; + } + + /** + * 构造无下级时的全零看板数据。 + */ + private Map createZeroGameCommissionData() { + Map map = new HashMap<>(); + String[] keys = { + "todayClassicBox", "todayLuckyUpgrade", "todayBattle", "todayCrazyWheel", "todayOlympus", + "yesterdayClassicBox", "yesterdayLuckyUpgrade", "yesterdayBattle", "yesterdayCrazyWheel", "yesterdayOlympus", + "weekClassicBox", "weekLuckyUpgrade", "weekBattle", "weekCrazyWheel", "weekOlympus", + "totalClassicBox", "totalLuckyUpgrade", "totalBattle", "totalCrazyWheel", "totalOlympus", + "todayRecharge", "yesterdayRecharge", "weekRecharge", "totalRecharge" + }; + for (String key : keys) { + map.put(key, BigDecimal.ZERO); + } + return map; + } + + /** + * 组装单个周期的看板数据行。 + */ + private Map buildPeriod(String periodName, Map data, + BigDecimal rechargeRate, BigDecimal newGameRate, BigDecimal classicGameRate, + String classicBoxKey, String luckyUpgradeKey, String battleKey, + String crazyWheelKey, String olympusKey, String rechargeKey, int newSub) { + BigDecimal classicBox = toBd(data, classicBoxKey); + BigDecimal luckyUpgrade = toBd(data, luckyUpgradeKey); + BigDecimal battle = toBd(data, battleKey); + BigDecimal crazyWheel = toBd(data, crazyWheelKey); + BigDecimal olympus = toBd(data, olympusKey); + BigDecimal recharge = toBd(data, rechargeKey); + BigDecimal totalConsume = classicBox.add(luckyUpgrade).add(battle).add(crazyWheel).add(olympus); + BigDecimal classicConsume = classicBox.add(luckyUpgrade).add(battle).add(crazyWheel); + BigDecimal totalRebate = calcCommission(recharge, rechargeRate) + .add(calcCommission(olympus, newGameRate)) + .add(calcCommission(classicConsume, classicGameRate)); + + Map period = new LinkedHashMap<>(); + period.put("period", periodName); + period.put("classicBox", classicBox); + period.put("luckyUpgrade", luckyUpgrade); + period.put("battle", battle); + period.put("crazyWheel", crazyWheel); + period.put("olympus", olympus); + period.put("totalConsume", totalConsume); + period.put("recharge", recharge); + period.put("newSub", newSub); + period.put("totalRebate", totalRebate); + return period; + } + + /** + * 按百分比费率计算佣金:amount × rate / 100,保留两位小数。 + */ + private BigDecimal calcCommission(BigDecimal amount, BigDecimal rate) { + if (amount == null || rate == null + || amount.compareTo(BigDecimal.ZERO) <= 0 + || rate.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return amount.multiply(rate) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoUserRelationshipServiceImpl.java b/skins-promo/src/main/java/com/ruoyi/promo/service/impl/PromoUserRelationshipServiceImpl.java new file mode 100644 index 0000000..e69de29 diff --git a/skins-promo/src/main/java/com/ruoyi/promo/task/CommissionSettlementTask.java b/skins-promo/src/main/java/com/ruoyi/promo/task/CommissionSettlementTask.java new file mode 100644 index 0000000..309ab57 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/task/CommissionSettlementTask.java @@ -0,0 +1,233 @@ +package com.ruoyi.promo.task; + +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.entity.TtCommissionRecord; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.promo.mapper.PromoTurnoverMapper; +import com.ruoyi.message.service.MessageService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 招商佣金周结定时任务。 + * + * 注册到 RuoYi 系统监控-定时任务: + * - job name: 招商佣金周结 + * job group: PROMO + * invoke target: commissionSettlementTask.settleWeeklyCommission() + * cron: 0 30 0 ? * MON (每周一 00:30) + */ +@Slf4j +@Component("commissionSettlementTask") +public class CommissionSettlementTask { + + @Autowired + private PromoTurnoverMapper promoTurnoverMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private MessageService messageService; + + @Autowired + private PlatformTransactionManager transactionManager; + + /** + * 执行每周佣金结算。 + * 结算周期:上周一 00:00 ~ 本周一 00:00 + * + * 每个招商账号在独立事务中结算:结算记录插入与佣金入账同成败, + * 入账失败时回滚该用户的记录,下次任务可重试(幂等检查不会误跳), + * 同时避免单个用户异常影响其他用户结算。 + */ + public void settleWeeklyCommission() { + log.info("[招商佣金周结] 任务开始 ----------"); + + Date[] weekRange = getLastWeekRange(); + Date startTime = weekRange[0]; + Date endTime = weekRange[1]; + Date now = new Date(); + + log.info("[招商佣金周结] 结算周期: {} ~ {}", + DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", startTime), + DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", endTime)); + + List businessUsers = promoTurnoverMapper.getBusinessUsers(); + if (CollectionUtils.isEmpty(businessUsers)) { + log.info("[招商佣金周结] 没有招商账号,任务结束"); + return; + } + + int successCount = 0; + int skipCount = 0; + + TransactionTemplate txTemplate = new TransactionTemplate(transactionManager); + for (TtUser businessUser : businessUsers) { + try { + // 每个用户独立事务:失败仅回滚自己,不影响其他用户,且可在下次任务重试 + Boolean settled = txTemplate.execute(status -> + settleSingleBusinessUser(businessUser, startTime, endTime, now)); + if (Boolean.TRUE.equals(settled)) { + successCount++; + } else { + skipCount++; + } + } catch (Exception e) { + log.error("[招商佣金周结] 用户ID={} 结算异常,已回滚本用户结算", businessUser.getUserId(), e); + } + } + + log.info("[招商佣金周结] 任务完成,成功={}, 跳过={} ----------", successCount, skipCount); + } + + /** + * 结算单个招商账号。 + * + * @return true 本次产生新结算;false 因幂等已存在而跳过 + */ + private boolean settleSingleBusinessUser(TtUser businessUser, Date startTime, Date endTime, Date now) { + Integer userId = businessUser.getUserId(); + + // 幂等:同一周期已结算则跳过 + int existed = promoTurnoverMapper.countCommissionRecord(userId, startTime); + if (existed > 0) { + log.info("[招商佣金周结] userId={} 本周期已结算,跳过", userId); + return false; + } + + BigDecimal commission = BigDecimal.ZERO; + BigDecimal totalFlow = BigDecimal.ZERO; + + // 按 business_id 快照统计上周流水:改绑/解绑不影响已发生流水的招商归属。 + Map data = promoTurnoverMapper.getWeeklyCommissionDataWithSnapshot( + userId, startTime, endTime); + BigDecimal recharge = toBd(data, "recharge"); + BigDecimal newGameConsume = toBd(data, "newGameConsume"); + BigDecimal classicGameConsume = toBd(data, "classicGameConsume"); + + BigDecimal commissionRate = businessUser.getCommissionRate() != null + ? businessUser.getCommissionRate() : BigDecimal.ZERO; + BigDecimal newGameRate = businessUser.getNewGameCommissionRate() != null + ? businessUser.getNewGameCommissionRate() : BigDecimal.ZERO; + BigDecimal classicGameRate = businessUser.getClassicGameCommissionRate() != null + ? businessUser.getClassicGameCommissionRate() : BigDecimal.ZERO; + + BigDecimal rechargeCommission = calcRate(recharge, commissionRate); + BigDecimal newGameCommission = calcRate(newGameConsume, newGameRate); + BigDecimal classicGameCommission = calcRate(classicGameConsume, classicGameRate); + + commission = rechargeCommission.add(newGameCommission).add(classicGameCommission); + totalFlow = recharge.add(newGameConsume).add(classicGameConsume); + + // 保存结算记录 + TtCommissionRecord record = new TtCommissionRecord(); + record.setUserId(userId); + record.setCommission(commission); + record.setSummaryTime(now); + record.setSettlementStartTime(startTime); + record.setSettlementEndTime(endTime); + record.setTotalFlow(totalFlow); + record.setRate(calcCompositeRate(commission, totalFlow)); + record.setClaimStatus("1"); + record.setClaimTime(now); + promoTurnoverMapper.insertCommissionRecord(record); + + // 佣金到账(入账失败必须抛异常回滚结算记录,否则记录存在但钱未到账, + // 且幂等检查会导致永不补发) + if (commission.compareTo(BigDecimal.ZERO) > 0) { + R credited = userService.updateUserAccount(userId, commission, TtAccountRecordSource.BUSINESS_WEEKLY_COMMISSION); + if (R.isError(credited)) { + throw new RuntimeException("佣金入账失败: " + + (credited.getMsg() != null ? credited.getMsg() : "未知原因")); + } + + // 发送站内信通知 + String title = MessageCategoryEnum.SYS_PROMO_REBATE.getTitle(); + String content = String.format( + "上周(%s ~ %s)招商周结返利已到账,返利 %s,请注意查收。", + DateUtils.parseDateToStr("yyyy-MM-dd", startTime), + DateUtils.parseDateToStr("yyyy-MM-dd", endTime), + commission.toPlainString()); + try { + messageService.sendNotice(Long.valueOf(userId), title, content, MessageCategoryEnum.SYS_PROMO_REBATE.getCode()); + } catch (Exception e) { + log.error("[招商佣金周结] userId={} 发送到账通知失败", userId, e); + } + } + + log.info("[招商佣金周结] userId={} 结算完成,流水={}, 佣金={}", userId, totalFlow, commission); + return true; + } + + /** + * 计算上周起止时间:上周一 00:00 ~ 本周一 00:00 + */ + private Date[] getLastWeekRange() { + Calendar end = Calendar.getInstance(); + resetTime(end); + // 回到本周一 00:00 + int dayOfWeek = end.get(Calendar.DAY_OF_WEEK); + int daysSinceMonday = (dayOfWeek + 5) % 7; + end.add(Calendar.DAY_OF_MONTH, -daysSinceMonday); + + Calendar start = (Calendar) end.clone(); + start.add(Calendar.DAY_OF_MONTH, -7); + + return new Date[]{start.getTime(), end.getTime()}; + } + + private void resetTime(Calendar calendar) { + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + } + + /** + * 按百分比比例计算佣金:amount × rate / 100,setScale(2, HALF_UP)。 + */ + private BigDecimal calcRate(BigDecimal amount, BigDecimal rate) { + if (amount == null || rate == null + || amount.compareTo(BigDecimal.ZERO) <= 0 + || rate.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return amount.multiply(rate) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + } + + /** + * 计算综合费率:commission / totalFlow * 100 + */ + private BigDecimal calcCompositeRate(BigDecimal commission, BigDecimal totalFlow) { + if (totalFlow == null || totalFlow.compareTo(BigDecimal.ZERO) <= 0 + || commission == null || commission.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return commission.divide(totalFlow, 6, RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(100)); + } + + /** 从 Map 中安全取 BigDecimal,null 返回 ZERO */ + private BigDecimal toBd(Map map, String key) { + if (map == null) return BigDecimal.ZERO; + Object v = map.get(key); + return v instanceof BigDecimal ? (BigDecimal) v : BigDecimal.ZERO; + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/task/PromoCdkeyRefundTask.java b/skins-promo/src/main/java/com/ruoyi/promo/task/PromoCdkeyRefundTask.java new file mode 100644 index 0000000..ce557a1 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/task/PromoCdkeyRefundTask.java @@ -0,0 +1,92 @@ +package com.ruoyi.promo.task; + +import com.ruoyi.promo.domain.entity.PromoCdkey; +import com.ruoyi.promo.service.PromoCdkeyService; +import com.ruoyi.message.service.MessageService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * CDK 过期/兑完 精确退还定时任务。 + * + * 口径:CDK创建时按 amountMax×num 等额预扣主播金币+积分,兑换时仅随机派发 min~max 金币。 + * CDK结束(过期/兑完)后,按「预扣总额 - 实际已派发金币总额」精确退还主播金币与积分(同额)。 + * + * 注册到 RuoYi 系统监控-定时任务: + * - job name: CDK结束退还 + * job group: PROMO + * invoke target: promoCdkeyRefundTask.settleEndedCdkeyRefunds() + * cron: 0 0/10 * * * ? (每10分钟) + */ +@Slf4j +@Component("promoCdkeyRefundTask") +public class PromoCdkeyRefundTask { + + @Autowired + private PromoCdkeyService promoCdkeyService; + + @Autowired + private MessageService messageService; + + /** + * 扫描已结束(过期/兑完)且未退还的CDK,逐个精确退还,并按主播聚合发送到账通知。 + */ + public void settleEndedCdkeyRefunds() { + log.info("[CDK结束退还] 任务开始 ----------"); + + // 1. 全局补齐过期状态(validity 到期且仍进行中的置为已结束) + int expired = promoCdkeyService.markExpiredCdkeys(); + log.info("[CDK结束退还] 补齐过期状态 {} 条", expired); + + // 2. 查询待退还候选:status=1 且 refund_time IS NULL + List candidates = promoCdkeyService.selectRefundableCdkeys(); + if (CollectionUtils.isEmpty(candidates)) { + log.info("[CDK结束退还] 无待退还CDK,任务结束"); + return; + } + + // 3. 逐个退还(每个CDK独立事务),按创建者聚合本轮退还总额与数量 + Map refundByCreator = new HashMap<>(); + Map countByCreator = new HashMap<>(); + int success = 0; + for (PromoCdkey cdkey : candidates) { + try { + BigDecimal refund = promoCdkeyService.refundOne(cdkey); + Long creatorId = cdkey.getCreatorId(); + if (refund != null && refund.compareTo(BigDecimal.ZERO) > 0) { + refundByCreator.merge(creatorId, refund, BigDecimal::add); + countByCreator.merge(creatorId, 1, Integer::sum); + } + success++; + } catch (Exception e) { + log.error("[CDK结束退还] cdkeyId={} 退还异常", cdkey.getId(), e); + } + } + + // 4. 按主播聚合发送站内信(仅对有实际退还金额的主播,避免刷屏) + // for (Map.Entry entry : refundByCreator.entrySet()) { + // Long creatorId = entry.getKey(); + // BigDecimal total = entry.getValue(); + // int cnt = countByCreator.getOrDefault(creatorId, 0); + // String title = "CDKEY结束退还到账通知"; + // String content = String.format( + // "您有 %d 个CDKEY已结束(过期或兑完),共返还金币 %s、积分 %s,请注意查收。", + // cnt, total.toPlainString(), total.toPlainString()); + // try { + // messageService.sendNotice(creatorId, title, content); + // } catch (Exception e) { + // log.error("[CDK结束退还] creatorId={} 发送退还通知失败", creatorId, e); + // } + // } + + log.info("[CDK结束退还] 任务完成,处理 {} 条,退还主播 {} 个 ----------", + success, refundByCreator.size()); + } +} diff --git a/skins-promo/src/main/java/com/ruoyi/promo/task/PromoTask.java b/skins-promo/src/main/java/com/ruoyi/promo/task/PromoTask.java new file mode 100644 index 0000000..a072892 --- /dev/null +++ b/skins-promo/src/main/java/com/ruoyi/promo/task/PromoTask.java @@ -0,0 +1,57 @@ +package com.ruoyi.promo.task; + +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.promo.domain.dto.TotalUserExpenditureDTO; +import com.ruoyi.promo.mapper.PromoTurnoverMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.*; + +@Slf4j +@Component("PromoTask") +public class PromoTask { + + @Autowired + private PromoTurnoverMapper promoTurnoverMapper; + + private Map commissionMap = new HashMap<>(); + + /** + * 计算上月佣金 + */ + public void calculateLastMonthCommission() { + // 汇总所有用户上个月的充值总额 + List lastMonthTotalUserExpenditure = promoTurnoverMapper.getLastMonthTotalUserExpenditure(); + for (TotalUserExpenditureDTO totalUserExpenditureDTO : lastMonthTotalUserExpenditure) { + // 向上递归给各级代理分配佣金 + if (!Objects.isNull(totalUserExpenditureDTO.getParentId())) { + distributeCommission(totalUserExpenditureDTO.getParentId(), + totalUserExpenditureDTO.getCommissionRate(), + totalUserExpenditureDTO.getAmount()); + } + } + } + + /** + * 分配佣金 + */ + private void distributeCommission(Integer parentId, + BigDecimal commissionRate, + BigDecimal amount) { + TtUser ttUser = promoTurnoverMapper.findById(parentId); + BigDecimal parentCommissionRate = ttUser.getCommissionRate(); + BigDecimal commission = parentCommissionRate.multiply(amount).subtract(commissionRate.multiply(amount)); + System.err.println("代理:" + parentId + ",佣金比例:" + parentCommissionRate + ",获得佣金:" + commission); + // 计算并存储该级代理的佣金 + // 将相同ID的代理佣金相加 + // 判断是否还有上级代理,有继续递归 + if (!Objects.isNull(ttUser.getParentId())) { + distributeCommission(ttUser.getParentId(), + ttUser.getCommissionRate(), + amount); + } + } +} diff --git a/skins-promo/src/main/resources/mapper/promo/PromoTurnoverMapper.xml b/skins-promo/src/main/resources/mapper/promo/PromoTurnoverMapper.xml new file mode 100644 index 0000000..16fd7c5 --- /dev/null +++ b/skins-promo/src/main/resources/mapper/promo/PromoTurnoverMapper.xml @@ -0,0 +1,1021 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE tt_user SET commission_rate = #{commissionRate} WHERE user_id = #{userId} + + + + + + + + + + + + + + + + INSERT INTO tt_commission_record ( + user_id, + commission, + summary_time, + settlement_start_time, + settlement_end_time, + total_flow, + rate, + claim_status, + claim_time + ) VALUES ( + #{userId}, + #{commission}, + #{summaryTime}, + #{settlementStartTime}, + #{settlementEndTime}, + #{totalFlow}, + #{rate}, + #{claimStatus}, + #{claimTime} + ) + + + + + + + + + + + + + + SELECT create_time, user_id, source, total + FROM tt_user_blend_ercash + WHERE type = 0 AND source IN (2, 4, 38, 40, 60) + AND user_id IN + #{id} + UNION ALL + SELECT create_time, user_id, source, total + FROM tt_user_blend_ercash_archive + WHERE type = 0 AND source IN (2, 4, 38, 40, 60) + AND user_id IN + #{id} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/.gitignore b/skins-service/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/skins-service/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/skins-service/game-olympus-service/pom.xml b/skins-service/game-olympus-service/pom.xml new file mode 100644 index 0000000..7d33b8b --- /dev/null +++ b/skins-service/game-olympus-service/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + com.ruoyi + skins-service + 4.8.2 + + + game-olympus-service + + + 8 + 8 + UTF-8 + + + + + com.ruoyi + ruoyi-framework + + + com.ruoyi + service-admin + 4.8.2 + + + com.ruoyi + service-user + 4.8.2 + + + com.ruoyi + ruoyi-common + 4.8.2 + + + diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/RecordPageRequest.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/RecordPageRequest.java new file mode 100644 index 0000000..339a081 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/RecordPageRequest.java @@ -0,0 +1,17 @@ +package com.ruoyi.game.olympus.contract.request; + +import lombok.Data; + +/** 玫瑰女郎游戏记录分页请求(scope: 0全服/1我的,默认1我的;均分页) */ +@Data +public class RecordPageRequest { + + /** 记录范围:0=全服,1=我的(默认) */ + private Integer scope; + + /** 页码,默认 1 */ + private Integer pageNum; + + /** 每页条数,默认 10 */ + private Integer pageSize; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/SpinRequest.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/SpinRequest.java new file mode 100644 index 0000000..33a4ad7 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/SpinRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.game.olympus.contract.request; + +import lombok.Data; + +@Data +public class SpinRequest { + /** 非空=开始新游戏;null=继续当前游戏(下一步) */ + private Long boxId; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/BoxResponse.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/BoxResponse.java new file mode 100644 index 0000000..540fb7f --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/BoxResponse.java @@ -0,0 +1,15 @@ +package com.ruoyi.game.olympus.contract.response; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class BoxResponse { + private Long boxId; + private String boxName; + private BigDecimal basicBet; + private BigDecimal price; + private String boxType; + private String modeName; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/RecordItemResponse.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/RecordItemResponse.java new file mode 100644 index 0000000..da70aa8 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/RecordItemResponse.java @@ -0,0 +1,34 @@ +package com.ruoyi.game.olympus.contract.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** 玫瑰女郎战绩列表项(我的记录/全服记录共用,只暴露客户端展示需要的字段) */ +@Data +public class RecordItemResponse { + + /** 玩家昵称 */ + private String nickName; + + /** 玩家头像 */ + private String avatar; + + /** 本局消费(下注金额) */ + private BigDecimal cost; + + /** 本局获得饰品的价格(= 结算金额) */ + private BigDecimal settledWin; + + /** 饰品图片 */ + private String prizeImageUrl; + + /** 饰品全名:名称(磨损品质),无品质时仅名称 */ + private String prizeName; + + /** 开局时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/SpinResponse.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/SpinResponse.java new file mode 100644 index 0000000..8c020e0 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/SpinResponse.java @@ -0,0 +1,40 @@ +package com.ruoyi.game.olympus.contract.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SpinResponse { + + private String state; // "PLAYING" | "FINISH" + + private Integer spinNum; // 免费旋转序号(0=基础旋转) + private Integer tumbleNum; // 本旋内翻滚序号 + private Integer globalStep; + private Integer globalTotalSteps; + private Integer spinsRemaining; + + private Object gridState; + private Object multiplierDetail; + private Object symbolLinks; // 本旋累积连线(每波追加,tumble 标明旋内第几波;面板由它汇总) + private Object dropData; + private Object fillData; + private Object scatterTrigger; + + private BigDecimal winAmount; // 本旋累计(该旋末帧 = 这一旋的总额) + private BigDecimal spinWin; // 本局累计(末帧 = totalWin) + private BigDecimal accumulator; // 本步后炸弹累计倍数(未 ×bet;FS 跨旋传递,重连恢复累计用) + + private Object retriggerInfo; + private String bigWinLevel; // 小/中/大:small | middle | big(仅免费旋转的旋末帧可能有值,其余 null) + + // ---- 仅 FINISH ---- + private BigDecimal totalWin; + private BigDecimal totalMul; + private Boolean maxWinReached; // 1000x 封顶标识 + private Object prizeInfo; + private Long boxRecordId; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusAdminController.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusAdminController.java new file mode 100644 index 0000000..42f067e --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusAdminController.java @@ -0,0 +1,729 @@ +package com.ruoyi.game.olympus.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.game.olympus.domain.OlympusBox; +import com.ruoyi.game.olympus.domain.OlympusBoxOrnament; +import com.ruoyi.game.olympus.enums.OlympusAudience; +import com.ruoyi.game.olympus.enums.OlympusModeType; +import com.ruoyi.game.olympus.mapper.OlympusBoxMapper; +import com.ruoyi.game.olympus.mapper.OlympusBoxOrnamentMapper; +import com.ruoyi.game.olympus.mapper.OlympusRecordMapper; +import com.ruoyi.game.olympus.repository.OlympusRewardPoolCacheRepository; +import com.ruoyi.game.olympus.domain.OlympusOrnament; +import com.ruoyi.game.olympus.domain.OlympusPrizeOrnament; +import com.ruoyi.game.olympus.domain.OlympusPrizeRange; +import com.ruoyi.game.olympus.domain.OlympusRound; +import com.ruoyi.game.olympus.mapper.OlympusOrnamentMapper; +import com.ruoyi.game.olympus.mapper.OlympusPrizeOrnamentMapper; +import com.ruoyi.game.olympus.mapper.OlympusPrizeRangeMapper; +import com.ruoyi.game.olympus.mapper.OlympusRoundMapper; +import com.ruoyi.game.olympus.domain.OlympusBoxTemplate; +import com.ruoyi.game.olympus.mapper.OlympusBoxTemplateMapper; +import com.ruoyi.game.olympus.repository.OlympusCycleRepository; +import com.ruoyi.game.olympus.service.OlympusDistributionService; +import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import com.ruoyi.game.olympus.domain.OlympusRewardLog; +import com.ruoyi.game.olympus.mapper.OlympusRewardLogMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +/** 奥林匹斯管理端:箱子配置 / 桶分布 / 水位(按箱注水抽水)。 */ +@RestController +@RequestMapping("/admin/olympus") +public class OlympusAdminController extends BaseController { + + @Autowired private OlympusBoxMapper boxMapper; + @Autowired private OlympusBoxOrnamentMapper boxOrnamentMapper; + @Autowired private OlympusRecordMapper recordMapper; + @Autowired private OlympusRewardPoolCacheRepository rewardPoolRepository; + @Autowired private OlympusOrnamentMapper ornamentMapper; + @Autowired private OlympusRoundMapper roundMapper; + @Autowired private OlympusPrizeOrnamentMapper prizeOrnamentMapper; + @Autowired private OlympusPrizeRangeMapper prizeRangeMapper; + @Autowired private OlympusBoxTemplateMapper boxTemplateMapper; + @Autowired private OlympusCycleRepository cycleRepository; + @Autowired private OlympusDistributionService distributionService; + @Autowired(required = false) private TtOrnamentsLevelMapper ornamentsLevelMapper; + @Autowired private TtUserService userService; + @Autowired private OlympusRewardLogMapper rewardLogMapper; + + @GetMapping("/box/list") + public R boxList(@RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize, + @RequestParam(required = false) String boxType) { + LambdaQueryWrapper qw = new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS"); + if (boxType != null && !boxType.isEmpty()) qw.eq(OlympusBox::getBoxType, boxType); + qw.orderByAsc(OlympusBox::getBoxId); + Page page = boxMapper.selectPage(new Page<>(pageNum, pageSize), qw); + + // 各身份分布一次全量查出(30箱×4身份量小),内存分组,避免逐身份 N+1 + // 桶期望按"这个箱子的池子实际会赔多少"取(每个箱子查一次,缓存复用) + Map> expectByBox = new HashMap<>(); + Map> distByKey = new HashMap<>(); + for (OlympusBoxOrnament bo : boxOrnamentMapper.selectList(null)) { + if (bo.getBoxId() == null || bo.getAudience() == null) continue; + distByKey.computeIfAbsent(bo.getBoxId() + ":" + bo.getAudience(), k -> new ArrayList<>()).add(bo); + } + + for (OlympusBox box : page.getRecords()) { + try { + OlympusModeType m = OlympusModeType.fromBoxType(box.getBoxType()); + double cm = m.getCostMultiplier(); + Map ornMul = expectByBox.computeIfAbsent(box.getBoxId(), + k -> distributionService.loadPoolExpect(k)); + box.setModeName(m.getName()); + box.setCostMultiplier(BigDecimal.valueOf(cm)); + Map targetMap = new HashMap<>(); + Map actualMap = new HashMap<>(); + Map totalMap = new HashMap<>(); + for (OlympusAudience aud : OlympusAudience.values()) { + // 目标RTP:从该身份分布反推 = Σ(count×expect_mul) / Σ(count) / CM × 100 + List list = distByKey.get(box.getBoxId() + ":" + aud.getCode()); + if (list != null && !list.isEmpty()) { + double sumMul = 0; int totalCount = 0; + for (OlympusBoxOrnament bo : list) { + int c = bo.getCount() != null ? bo.getCount() : 0; + if (c <= 0) continue; + sumMul += c * ornMul.getOrDefault(bo.getOrnamentId(), 0.0); + totalCount += c; + } + if (totalCount > 0) { + targetMap.put(aud.getCode(), BigDecimal.valueOf(sumMul / totalCount / cm * 100) + .setScale(2, RoundingMode.HALF_UP)); + // 该身份一轮总回合 = 固定池 count 和反推(身份级独立) + double cr = box.getRtpConvergeRatio() != null ? box.getRtpConvergeRatio().doubleValue() : 0.15; + totalMap.put(aud.getCode(), BigDecimal.valueOf( + OlympusDistributionService.deriveTotalRounds(totalCount, cr))); + } + } + // 实时RTP:按 (boxId, audience) 循环账本各查一次 + BigDecimal r = cycleRepository.realtimeRtp(box.getBoxId(), aud.getCode(), cm); + if (r != null) actualMap.put(aud.getCode(), r); + } + box.setTargetRtpByAudience(targetMap); + box.setActualRtpByAudience(actualMap); + box.setTotalRoundsByAudience(totalMap); + box.setActualRtp(actualMap.get("common")); // 兼容保留 common 口径 + } catch (Exception ignore) {} + } + return R.ok(Map.of("rows", page.getRecords(), "total", page.getTotal())); + } + + @Log(title = "箱子配置", action = "编辑", businessType = BusinessType.UPDATE) + @PostMapping("/box/save") + @Transactional(rollbackFor = Exception.class) + public R boxSave(@RequestBody OlympusBox box) { + if (box.getBet() == null || box.getBoxType() == null) return R.fail("下注与模式不能为空"); + try { + OlympusModeType m = OlympusModeType.fromBoxType(box.getBoxType()); + box.setGameType("OLYMPUS"); + box.setPrice(box.getBet().multiply(BigDecimal.valueOf(m.getCostMultiplier()))); + } catch (IllegalArgumentException e) { + return R.fail("无效模式: " + box.getBoxType()); + } + if (box.getBoxId() != null && boxMapper.selectById(box.getBoxId()) != null) boxMapper.updateById(box); + else boxMapper.insert(box); + return R.ok(Map.of("boxId", box.getBoxId())); + } + + /** 某箱某身份的桶分布:含 convergeCount(收敛账本) + fixedRounds/convergeRounds(按该身份分布反推) */ + @GetMapping("/box/ornaments/{boxId}") + public R boxOrnaments(@PathVariable Long boxId, + @RequestParam(required = false, defaultValue = "common") String audience) { + List list = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); + for (OlympusBoxOrnament bo : list) { + bo.setConvergeCount(cycleRepository.convergeCount(boxId, audience, bo.getOrnamentId())); + } + OlympusBox box = boxMapper.selectById(boxId); + // 固定/收敛回合按该身份分布 count 之和反推(身份级独立),不使用箱子级 total_ornaments + int fixedSum = list.stream().mapToInt(bo -> bo.getCount() != null ? bo.getCount() : 0).sum(); + double cr = (box != null && box.getRtpConvergeRatio() != null) ? box.getRtpConvergeRatio().doubleValue() : 0.15; + int fixedRounds = fixedSum; + int convergeRounds = fixedSum > 0 ? Math.max(0, OlympusDistributionService.deriveTotalRounds(fixedSum, cr) - fixedSum) : 0; + // poolExpect:该箱每个桶"池子里实际会赔多少"的均值(= 列表页「目标RTP」用的同一口径)。 + // 前端「计算RTP」必须拿它算,否则会拿桶定义的名义 expect_mul(区间中点)算出跟目标RTP不一致的数。 + return R.ok(Map.of("list", list, "audience", audience, "fixedRounds", fixedRounds, + "convergeRounds", convergeRounds, "poolExpect", distributionService.loadPoolExpect(boxId))); + } + + @Log(title = "箱子配置", action = "保存", businessType = BusinessType.UPDATE) + /** 保存某箱某身份的饰品回合分布(items 为固定池回合,反推回写总回合) */ + @PostMapping("/box/ornaments/save") + @Transactional(rollbackFor = Exception.class) + public R boxOrnamentsSave(@RequestBody Map params) { + Long boxId = asLong(params.get("boxId")); + if (boxId == null) return R.fail("boxId 不能为空"); + String audience = params.get("audience") != null ? params.get("audience").toString() : "common"; + @SuppressWarnings("unchecked") + List> items = (List>) params.get("items"); + distributionService.writeDistribution(boxId, audience, items); + cycleRepository.resetCycle(boxId, audience); + return R.ok(); + } + + /** 按目标 RTP 重算某箱某身份分布(只写 box_ornament) */ + @PostMapping("/box/ornaments/calc") + public R calcDistribution(@RequestBody Map params) { + Long boxId = asLong(params.get("boxId")); + String audience = params.get("audience") != null ? params.get("audience").toString() : "common"; + Integer totalRounds = asInt(params.get("totalRounds")); + Double rtp = params.get("rtp") != null ? Double.parseDouble(params.get("rtp").toString()) : 0.8; + Double convergeRatio = params.get("convergeRatio") != null ? Double.parseDouble(params.get("convergeRatio").toString()) : null; + OlympusBox box = boxMapper.selectById(boxId); + if (box == null) return R.fail("箱子不存在"); + OlympusModeType mode = OlympusModeType.fromBoxType(box.getBoxType()); + double cr = convergeRatio != null ? convergeRatio + : (box.getRtpConvergeRatio() != null ? box.getRtpConvergeRatio().doubleValue() : 0.15); + int fixedRoundsN = OlympusDistributionService.deriveFixedRounds(totalRounds != null ? totalRounds : 200, cr); + int canonicalN = OlympusDistributionService.deriveTotalRounds(fixedRoundsN, cr); + box.setTotalOrnaments(canonicalN); + box.setRtp(BigDecimal.valueOf(rtp)); + if (convergeRatio != null) box.setRtpConvergeRatio(BigDecimal.valueOf(convergeRatio)); + boxMapper.updateById(box); + distributionService.reset(boxId, audience); + cycleRepository.resetCycle(boxId, audience); + List saved = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); + Map ornMul = distributionService.loadPoolExpect(boxId); + List> result = new ArrayList<>(); + int total = 0; double contrib = 0; + for (OlympusBoxOrnament bo : saved) { + int c = bo.getCount() != null ? bo.getCount() : 0; + result.add(Map.of("ornamentId", bo.getOrnamentId(), "count", c)); + total += c; + contrib += c * ornMul.getOrDefault(bo.getOrnamentId(), 0.0); + } + double cm = mode.getCostMultiplier(); + double calcRtpValue = total > 0 ? contrib / total / cm * 100 : 0; + return R.ok(Map.of("items", result, "totalRounds", total, "boxTotal", canonicalN, + "convergeRounds", Math.max(0, canonicalN - total), + "calcRtp", String.format("%.1f%%", calcRtpValue))); + } + + /** 模拟 payoutCap 约束下的预估实际 RTP */ + @PostMapping("/box/ornaments/simulate-rtp") + public R simulateRtp(@RequestBody Map params) { + @SuppressWarnings("unchecked") + List> items = (List>) params.get("items"); + Long boxId = asLong(params.get("boxId")); + double rtp = params.get("rtp") != null ? Double.parseDouble(params.get("rtp").toString()) : 0.8; + if (items == null || items.isEmpty()) return R.ok(Map.of("estimatedRtp", "0.0%")); + Map dist = new LinkedHashMap<>(); + for (Map item : items) { + Integer ornamentId = asInt(item.get("ornamentId")); + Integer count = asInt(item.get("count")); + if (ornamentId != null && count != null && count > 0) dist.put(ornamentId, count); + } + double est = distributionService.simulateActualRtp(dist, boxId, rtp, 10); + return R.ok(Map.of("estimatedRtp", String.format("%.1f%%", est))); + } + + // ==================== 分布模板(按身份) ==================== + + @GetMapping("/box/templates/{boxId}") + public R templateList(@PathVariable Long boxId, + @RequestParam(required = false, defaultValue = "common") String audience) { + List list = boxTemplateMapper.selectList( + new LambdaQueryWrapper() + .eq(OlympusBoxTemplate::getBoxId, boxId) + .eq(OlympusBoxTemplate::getAudience, audience) + .orderByDesc(OlympusBoxTemplate::getId)); + for (OlympusBoxTemplate t : list) { + t.setItems(distributionService.parseItems(t.getItemsJson())); + t.setItemsJson(null); + } + return R.ok(Map.of("list", list)); + } + + @Log(title = "箱子配置", action = "保存为模板", businessType = BusinessType.INSERT) + @PostMapping("/box/template/save") + @Transactional(rollbackFor = Exception.class) + public R templateSave(@RequestBody Map params) { + Long boxId = asLong(params.get("boxId")); + if (boxId == null) return R.fail("boxId 不能为空"); + String audience = params.get("audience") != null ? params.get("audience").toString() : "common"; + String name = params.get("name") != null ? params.get("name").toString().trim() : ""; + if (name.isEmpty()) return R.fail("模板名称不能为空"); + @SuppressWarnings("unchecked") + List> items = (List>) params.get("items"); + if (items == null || items.isEmpty()) return R.fail("模板饰品分布不能为空"); + Long dup = boxTemplateMapper.selectCount(new LambdaQueryWrapper() + .eq(OlympusBoxTemplate::getBoxId, boxId).eq(OlympusBoxTemplate::getAudience, audience) + .eq(OlympusBoxTemplate::getName, name)); + if (dup != null && dup > 0) return R.fail("模板名称已存在,请使用不同的名称"); + int total = 0; + for (Map item : items) { Integer c = asInt(item.get("count")); if (c != null && c > 0) total += c; } + OlympusBoxTemplate tpl = new OlympusBoxTemplate(); + tpl.setBoxId(boxId); + tpl.setAudience(audience); + tpl.setName(name); + tpl.setTotalRounds(total); + tpl.setItemsJson(distributionService.toItemsJson(items)); + tpl.setIsActive(0); + boxTemplateMapper.insert(tpl); + return R.ok(Map.of("id", tpl.getId())); + } + + @Log(title = "箱子配置", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/box/template/{id}") + public R templateDelete(@PathVariable Long id) { + boxTemplateMapper.deleteById(id); + return R.ok(); + } + + @Log(title = "箱子配置", action = "应用选中模板", businessType = BusinessType.UPDATE) + @PostMapping("/box/template/apply") + @Transactional(rollbackFor = Exception.class) + public R templateApply(@RequestBody Map params) { + Long templateId = asLong(params.get("templateId")); + if (templateId == null) return R.fail("templateId 不能为空"); + OlympusBoxTemplate tpl = boxTemplateMapper.selectById(templateId); + if (tpl == null) return R.fail("模板不存在"); + distributionService.writeDistribution(tpl.getBoxId(), tpl.getAudience(), distributionService.parseItems(tpl.getItemsJson())); + cycleRepository.resetCycle(tpl.getBoxId(), tpl.getAudience()); + List siblings = boxTemplateMapper.selectList(new LambdaQueryWrapper() + .eq(OlympusBoxTemplate::getBoxId, tpl.getBoxId()).eq(OlympusBoxTemplate::getAudience, tpl.getAudience())); + for (OlympusBoxTemplate s : siblings) { + int active = s.getId().equals(templateId) ? 1 : 0; + if (!Integer.valueOf(active).equals(s.getIsActive())) { s.setIsActive(active); boxTemplateMapper.updateById(s); } + } + return R.ok(); + } + + /** 一键重算所有箱子 × 所有身份分布 */ + @PostMapping("/box/recalc_all") + public R recalcAll() { + List boxes = boxMapper.selectList( + new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); + int done = 0; + for (OlympusBox box : boxes) { + for (OlympusAudience aud : OlympusAudience.values()) { + try { + distributionService.reset(box.getBoxId(), aud.getCode()); + cycleRepository.resetCycle(box.getBoxId(), aud.getCode()); + done++; + } catch (Exception ignore) {} + } + } + return R.ok(Map.of("recalculated", done)); + } + + @Log(title = "箱子配置", action = "一键重算分布", businessType = BusinessType.CLEAN) + /** + * 重算分布(弹窗):按统一参数批量重算。 + * 入参:boxType 可选(空=全部模式);audience 可选(空=全部身份);rtp/totalRounds/convergeRatio 非空则统一设置到箱,再按最新配置重算分布。 + */ + @PostMapping("/box/recalcBatch") + @Transactional(rollbackFor = Exception.class) + public R recalcBatch(@RequestBody Map params) { + String boxType = params.get("boxType") != null && !params.get("boxType").toString().isEmpty() + ? params.get("boxType").toString() : null; + String audience = params.get("audience") != null && !params.get("audience").toString().isEmpty() + ? params.get("audience").toString() : null; + Double rtp = params.get("rtp") != null ? Double.parseDouble(params.get("rtp").toString()) : null; + Integer totalRounds = asInt(params.get("totalRounds")); + Double convergeRatio = params.get("convergeRatio") != null + ? Double.parseDouble(params.get("convergeRatio").toString()) : null; + + LambdaQueryWrapper qw = new LambdaQueryWrapper() + .eq(OlympusBox::getGameType, "OLYMPUS"); + if (boxType != null) qw.eq(OlympusBox::getBoxType, boxType); + List boxes = boxMapper.selectList(qw); + if (boxes.isEmpty()) return R.fail("没有匹配的箱子"); + + List audiences = audience != null ? Collections.singletonList(audience) + : Arrays.stream(OlympusAudience.values()).map(OlympusAudience::getCode).collect(Collectors.toList()); + + int done = 0; + for (OlympusBox box : boxes) { + boolean changed = false; + if (rtp != null) { box.setRtp(BigDecimal.valueOf(rtp)); changed = true; } + if (totalRounds != null && totalRounds > 0) { box.setTotalOrnaments(totalRounds); changed = true; } + if (convergeRatio != null) { box.setRtpConvergeRatio(BigDecimal.valueOf(convergeRatio)); changed = true; } + if (changed) boxMapper.updateById(box); + for (String aud : audiences) { + try { + distributionService.reset(box.getBoxId(), aud); + cycleRepository.resetCycle(box.getBoxId(), aud); + done++; + } catch (Exception ignore) {} + } + } + return R.ok(Map.of("recalculated", done, "boxes", boxes.size())); + } + + /** 玩家记录列表:可按玩家ID + 身份筛选;行内回填 audience(默认全部身份,含白号/机器人) */ + @GetMapping("/record/list") + public R recordList(@RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize, + @RequestParam(required = false) Long userId, + @RequestParam(required = false) String audience) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + // 空 = 全部身份(与奖池页"未选身份默认普通"不同:这里空是不过滤) + if (audience != null && !audience.trim().isEmpty() && !POOL_AUDIENCES.contains(audience.trim())) { + return R.fail("身份无效"); + } + qw.inSql(com.ruoyi.game.olympus.domain.OlympusRecord::getUserId, audienceUserSubQuery(audience)); + if (userId != null) qw.eq(com.ruoyi.game.olympus.domain.OlympusRecord::getUserId, userId); + qw.orderByDesc(com.ruoyi.game.olympus.domain.OlympusRecord::getCreateTime); + Page page = recordMapper.selectPage(new Page<>(pageNum, pageSize), qw); + fillAudience(page.getRecords()); + return R.ok(Map.of("rows", page.getRecords(), "total", page.getTotal())); + } + + /** + * 身份 → tt_user 子查询。奖池 4 池口径(common/anchor/robot/white),另附封禁/逻辑删除兜底: + * 封禁(status=1)和已删除用户任何身份都不展示;身份为空=全部身份(不再无条件排除白号/机器人)。 + */ + private static String audienceUserSubQuery(String audience) { + String base = "SELECT user_id FROM tt_user WHERE status != '1' AND del_flag = '0' AND "; + if ("white".equals(audience)) return base + "is_blank_account = 1"; + if ("anchor".equals(audience)) return base + "COALESCE(is_blank_account,0) != 1 AND user_type = '01'"; + if ("robot".equals(audience)) return base + "COALESCE(is_blank_account,0) != 1 AND user_type = '03'"; + if ("common".equals(audience)) return base + "COALESCE(is_blank_account,0) != 1 AND (user_type IS NULL OR user_type NOT IN ('01','03'))"; + return base + "1 = 1"; + } + + /** 批量回填每行身份:复用 OlympusAudience.resolve,勿另写一套判定 */ + private void fillAudience(List rows) { + if (rows == null || rows.isEmpty()) return; + Set ids = rows.stream().map(com.ruoyi.game.olympus.domain.OlympusRecord::getUserId) + .filter(Objects::nonNull).collect(Collectors.toSet()); + if (ids.isEmpty()) return; + Map audMap = new HashMap<>(); + for (TtUser u : userService.listByIds(ids)) { + if (u.getUserId() != null) audMap.put(u.getUserId().longValue(), OlympusAudience.resolve(u).getCode()); + } + for (com.ruoyi.game.olympus.domain.OlympusRecord r : rows) { + r.setAudience(audMap.getOrDefault(r.getUserId(), "common")); + } + } + + @GetMapping("/win/summary") + public R winSummary(@RequestParam(required = false) String dateStart, + @RequestParam(required = false) String dateEnd) { + Map row = recordMapper.winSummary(dateStart, dateEnd); + return R.ok(row); + } + + // ==================== 饰品桶定义(饰品列表) ==================== + + @GetMapping("/ornament/list") + public R ornamentList() { + List list = ornamentMapper.selectList( + new LambdaQueryWrapper().orderByAsc(OlympusOrnament::getId)); + return R.ok(Map.of("list", list)); + } + + // ==================== 预生成回合(回合管理) ==================== + + @GetMapping("/round/list") + public R roundList(@RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize, + @RequestParam(required = false) Long roundId, + @RequestParam(required = false) String boxType) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (roundId != null) qw.eq(OlympusRound::getRoundId, roundId); + // 收集箱子 id→模式,供筛选与回填 boxType + List boxes = boxMapper.selectList( + new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); + Map boxTypeMap = new HashMap<>(); + List filterBoxIds = new ArrayList<>(); + for (OlympusBox b : boxes) { + boxTypeMap.put(b.getBoxId(), b.getBoxType()); + if (boxType != null && !boxType.isEmpty() && boxType.equals(b.getBoxType())) filterBoxIds.add(b.getBoxId()); + } + if (boxType != null && !boxType.isEmpty()) { + if (filterBoxIds.isEmpty()) return R.ok(Map.of("rows", Collections.emptyList(), "total", 0)); + qw.in(OlympusRound::getBoxId, filterBoxIds); + } + qw.orderByAsc(OlympusRound::getRoundId); + Page page = roundMapper.selectPage(new Page<>(pageNum, pageSize), qw); + for (OlympusRound r : page.getRecords()) r.setBoxType(boxTypeMap.get(r.getBoxId())); + return R.ok(Map.of("rows", page.getRecords(), "total", page.getTotal())); + } + + // ==================== 每日统计 ==================== + + /** 每日统计列表:按天聚合 game_olympus_record(与 winSummary 同源同口径,支持日期筛选) */ + @GetMapping("/win/list") + public R winList(@RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize, + @RequestParam(required = false) String dateStart, + @RequestParam(required = false) String dateEnd) { + List> all = recordMapper.dailyList(dateStart, dateEnd); + int from = Math.max(0, (pageNum - 1) * pageSize); + if (from >= all.size()) return R.ok(Map.of("rows", Collections.emptyList(), "total", all.size())); + return R.ok(Map.of("rows", all.subList(from, Math.min(all.size(), from + pageSize)), "total", all.size())); + } + + // ==================== 返奖饰品(奖品管理) ==================== + + @GetMapping("/prize/list") + public R prizeList() { + List list = prizeOrnamentMapper.selectList( + new LambdaQueryWrapper().orderByAsc(OlympusPrizeOrnament::getSort)); + List ranges = prizeRangeMapper.selectList(new LambdaQueryWrapper<>()); + Map rangeMap = new HashMap<>(); + for (OlympusPrizeRange r : ranges) rangeMap.put(r.getPrizeId(), r); + for (OlympusPrizeOrnament o : list) { + OlympusPrizeRange r = rangeMap.get(o.getId()); + if (r != null) { o.setWinLow(r.getWinLow()); o.setWinHigh(r.getWinHigh()); } + if (o.getGrade() != null && ornamentsLevelMapper != null) { + TtOrnamentsLevel lv = ornamentsLevelMapper.selectById(o.getGrade()); + if (lv != null) o.setLevelImg(lv.getLevelImg()); + } + } + return R.ok(Map.of("list", list)); + } + + @Log(title = "奖品管理", action = "编辑", businessType = BusinessType.UPDATE) + @PostMapping("/prize/save") + @Transactional(rollbackFor = Exception.class) + public R prizeSave(@RequestBody OlympusPrizeOrnament prize) { + if (prize.getName() == null || prize.getName().trim().isEmpty()) return R.fail("饰品名称不能为空"); + prize.setGameType("OLYMPUS"); + if (prize.getStatus() == null) prize.setStatus("1"); + if (prize.getId() != null && prizeOrnamentMapper.selectById(prize.getId()) != null) prizeOrnamentMapper.updateById(prize); + else prizeOrnamentMapper.insert(prize); + // 同步区间(按 prizeId 唯一) + if (prize.getWinLow() != null && prize.getWinHigh() != null) { + OlympusPrizeRange r = prizeRangeMapper.selectOne( + new LambdaQueryWrapper().eq(OlympusPrizeRange::getPrizeId, prize.getId()).last("LIMIT 1")); + if (r == null) { r = new OlympusPrizeRange(); r.setPrizeId(prize.getId()); } + r.setWinLow(prize.getWinLow()); + r.setWinHigh(prize.getWinHigh()); + if (r.getId() != null) prizeRangeMapper.updateById(r); else prizeRangeMapper.insert(r); + } + return R.ok(Map.of("id", prize.getId())); + } + + // ==================== 水位(按【箱 × 身份】注水/抽水) ==================== + + /** 奖池身份维度,与 OlympusAudience 对齐 */ + private static final List POOL_AUDIENCES = Arrays.asList("common", "anchor", "robot", "white"); + + /** 身份入参归一化:空=common;非法返回 null(调用方报错,不静默兜底) */ + private static String audienceOf(Object v) { + if (v == null || v.toString().trim().isEmpty()) return "common"; + String s = v.toString().trim(); + return POOL_AUDIENCES.contains(s) ? s : null; + } + + /** 奖池配置快照:某身份各箱水位/RTP/本轮进度/收敛占比 + 一轮花费基数 */ + @GetMapping("/rewardPool/config") + public R rewardPoolConfig(@RequestParam(required = false) String audience) { + String aud = audienceOf(audience); + if (aud == null) return R.fail("身份无效"); + return R.ok(poolConfigMap(aud)); + } + + /** 组装奖池配置(total + boxes),供 config 与 adjust/batch/reset 复用 */ + private Map poolConfigMap(String audience) { + List boxes = boxMapper.selectList( + new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS").orderByAsc(OlympusBox::getBoxId)); + List> boxList = new ArrayList<>(); + BigDecimal totalBet = BigDecimal.ZERO, totalWin = BigDecimal.ZERO, totalBalance = BigDecimal.ZERO; + for (OlympusBox box : boxes) { + Long boxId = box.getBoxId(); + com.ruoyi.game.olympus.model.cache.OlympusRewardPoolCache c = rewardPoolRepository.getBoxCache(boxId, audience); + BigDecimal balance = rewardPoolRepository.getBoxBalance(boxId, audience); + double cm = 1.0; + try { cm = OlympusModeType.fromBoxType(box.getBoxType()).getCostMultiplier(); } catch (Exception ignore) {} + int rounds = box.getTotalOrnaments() != null ? box.getTotalOrnaments() : 0; + BigDecimal costPerRound = coalesce(box.getBet()).multiply(BigDecimal.valueOf(cm)); + BigDecimal cycleCost = costPerRound.multiply(BigDecimal.valueOf(rounds)); + long drawn = cycleRepository.drawn(boxId, audience); + Double cycleRtp = null; + if (drawn > 0 && cm > 0) cycleRtp = cycleRepository.mulSum(boxId, audience) / drawn / cm; + Map m = new LinkedHashMap<>(); + m.put("boxId", boxId); + m.put("name", box.getName()); + m.put("audience", audience); + m.put("boxType", box.getBoxType()); + m.put("bet", box.getBet()); + m.put("totalBet", c.getTotalBet()); + m.put("totalWin", c.getTotalWin()); + m.put("adjust", c.getAdjust()); + m.put("balance", balance); + m.put("costPerRound", costPerRound.setScale(2, RoundingMode.HALF_UP)); + m.put("cycleRounds", rounds); + m.put("cycleCost", cycleCost.setScale(2, RoundingMode.HALF_UP)); + m.put("totalRounds", rounds); + m.put("convergeRatio", box.getRtpConvergeRatio()); + m.put("cycleDrawn", drawn); + m.put("cycleRtp", cycleRtp); + m.put("rtp", c.getTotalBet().signum() > 0 + ? c.getTotalWin().divide(c.getTotalBet(), 4, RoundingMode.HALF_UP) : null); + boxList.add(m); + totalBet = totalBet.add(c.getTotalBet()); + totalWin = totalWin.add(c.getTotalWin()); + totalBalance = totalBalance.add(balance); + } + Map total = new LinkedHashMap<>(); + total.put("audience", audience); + total.put("totalBet", totalBet); + total.put("totalWin", totalWin); + total.put("adjust", rewardPoolRepository.getGlobalAdjust(audience)); + total.put("balance", totalBalance); + total.put("rtp", totalBet.signum() > 0 ? totalWin.divide(totalBet, 4, RoundingMode.HALF_UP) : null); + Map result = new LinkedHashMap<>(); + result.put("audience", audience); + result.put("total", total); + result.put("boxes", boxList); + return result; + } + + @Log(title = "奖池水位", action = "注水/抽水", businessType = BusinessType.GRANT) + /** 逐箱注水/抽水:type=1注水/2抽水,记审计日志 */ + @PostMapping("/rewardPool/adjust") + public R rewardPoolAdjust(@RequestBody Map body) { + String audience = audienceOf(body.get("audience")); + if (audience == null) return R.fail("身份无效"); + Long boxId = asLong(body.get("boxId")); + Integer type = asInt(body.get("type")); + BigDecimal amount = asBd(body.get("amount")); + String remark = body.get("remark") != null ? body.get("remark").toString() : null; + OlympusBox box = boxId != null ? boxMapper.selectById(boxId) : null; + if (box == null) return R.fail("箱子不存在"); + if (type == null || (type != 1 && type != 2)) return R.fail("调整类型无效"); + if (amount.signum() <= 0) return R.fail("金额必须大于 0"); + BigDecimal before = rewardPoolRepository.getBoxBalance(boxId, audience); + rewardPoolRepository.adjustBox(boxId, audience, type == 2 ? amount.negate() : amount); + BigDecimal after = rewardPoolRepository.getBoxBalance(boxId, audience); + writeLog(boxId, box.getName(), audience, type, amount, before, after, remark); + return R.ok(poolConfigMap(audience)); + } + + @Log(title = "奖池水位", action = "批量注水/抽水", businessType = BusinessType.GRANT) + /** 批量按一轮花费比例注水/抽水:ratio 百分比(0~1000],记一条汇总日志 */ + @PostMapping("/rewardPool/adjustBatch") + public R rewardPoolAdjustBatch(@RequestBody Map body) { + String audience = audienceOf(body.get("audience")); + if (audience == null) return R.fail("身份无效"); + Integer type = asInt(body.get("type")); + BigDecimal ratio = asBd(body.get("ratio")); + String remark = body.get("remark") != null ? body.get("remark").toString() : null; + if (type == null || (type != 1 && type != 2)) return R.fail("调整类型无效"); + if (ratio.signum() <= 0 || ratio.compareTo(new BigDecimal("1000")) > 0) return R.fail("比例须在 0~1000"); + List boxes = boxMapper.selectList( + new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); + int adjusted = 0; + BigDecimal totalAmount = BigDecimal.ZERO; + for (OlympusBox box : boxes) { + double cm = 1.0; + try { cm = OlympusModeType.fromBoxType(box.getBoxType()).getCostMultiplier(); } catch (Exception ignore) {} + BigDecimal base = coalesce(box.getBet()).multiply(BigDecimal.valueOf(cm)) + .multiply(BigDecimal.valueOf(box.getTotalOrnaments() != null ? box.getTotalOrnaments() : 0)); + if (base.signum() <= 0) continue; + BigDecimal amount = base.multiply(ratio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + if (amount.signum() <= 0) continue; + rewardPoolRepository.adjustBox(box.getBoxId(), audience, type == 2 ? amount.negate() : amount); + totalAmount = totalAmount.add(amount); + adjusted++; + } + writeLog(null, "批量(" + adjusted + "箱 " + ratio.stripTrailingZeros().toPlainString() + "%)", + audience, type, totalAmount, null, null, remark); + Map r = poolConfigMap(audience); + r.put("adjusted", adjusted); + r.put("totalAmount", totalAmount); + return R.ok(r); + } + + @Log(title = "奖池水位", action = "重置调整", businessType = BusinessType.CLEAN) + /** 重置人工调整:该身份所有箱子 adjust 归零,仅保留真实水位;记一条汇总日志 */ + @PostMapping("/rewardPool/adjustReset") + public R rewardPoolAdjustReset(@RequestBody(required = false) Map body) { + String audience = audienceOf(body != null ? body.get("audience") : null); + if (audience == null) return R.fail("身份无效"); + String remark = body != null && body.get("remark") != null ? body.get("remark").toString() : null; + List boxes = boxMapper.selectList( + new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); + int cleared = 0; + BigDecimal totalCleared = BigDecimal.ZERO; + for (OlympusBox box : boxes) { + BigDecimal adjust = rewardPoolRepository.getBoxAdjust(box.getBoxId(), audience); + if (adjust.signum() == 0) continue; + rewardPoolRepository.adjustBox(box.getBoxId(), audience, adjust.negate()); + totalCleared = totalCleared.add(adjust.abs()); + cleared++; + } + BigDecimal residual = rewardPoolRepository.getGlobalAdjust(audience); + if (residual.signum() != 0) rewardPoolRepository.adjustGlobal(audience, residual.negate()); + writeLog(null, "全部(" + cleared + "箱)", audience, 3, totalCleared, null, null, remark); + Map r = poolConfigMap(audience); + r.put("cleared", cleared); + r.put("totalCleared", totalCleared); + return R.ok(r); + } + + /** 奖池调整历史(分页,可按身份筛选) */ + @GetMapping("/rewardPool/adjustLog") + public R rewardPoolAdjustLog(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String audience) { + String aud = audienceOf(audience); + if (aud == null) return R.fail("身份无效"); + Page p = rewardLogMapper.selectPage(new Page<>(page, size), + new LambdaQueryWrapper() + .eq(OlympusRewardLog::getAudience, aud) + .orderByDesc(OlympusRewardLog::getId)); + return R.ok(Map.of("rows", p.getRecords(), "total", p.getTotal())); + } + + private void writeLog(Long boxId, String boxName, String audience, Integer type, BigDecimal amount, + BigDecimal before, BigDecimal after, String remark) { + try { + OlympusRewardLog rlog = new OlympusRewardLog(); + rlog.setBoxId(boxId); + rlog.setBoxName(boxName); + rlog.setAudience(audience); + rlog.setType(type); + rlog.setAmount(amount); + rlog.setBalanceBefore(before); + rlog.setBalanceAfter(after); + rlog.setPoolBalance(totalPoolBalance(audience)); + rlog.setOperator(getUsername()); + rlog.setRemark(remark); + rlog.setCreateTime(new java.util.Date()); + rewardLogMapper.insert(rlog); + } catch (Exception ignore) {} + } + + /** 该身份全站总池余额 = 各箱水位之和(写日志时取操作后快照) */ + private BigDecimal totalPoolBalance(String audience) { + List list = boxMapper.selectList( + new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); + BigDecimal sum = BigDecimal.ZERO; + for (OlympusBox b : list) sum = sum.add(rewardPoolRepository.getBoxBalance(b.getBoxId(), audience)); + return sum; + } + + private static BigDecimal coalesce(BigDecimal v) { return v != null ? v : BigDecimal.ZERO; } + + private static Long asLong(Object v) { + if (v == null) return null; + try { return Long.valueOf(v.toString().trim()); } catch (Exception e) { return null; } + } + private static Integer asInt(Object v) { + if (v == null) return null; + try { return Integer.valueOf(v.toString().trim()); } catch (Exception e) { return null; } + } + private static BigDecimal asBd(Object v) { + if (v == null) return BigDecimal.ZERO; + try { return new BigDecimal(v.toString().trim()); } catch (Exception e) { return BigDecimal.ZERO; } + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusClientController.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusClientController.java new file mode 100644 index 0000000..f7220bf --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusClientController.java @@ -0,0 +1,175 @@ +package com.ruoyi.game.olympus.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.game.olympus.contract.request.SpinRequest; +import com.ruoyi.game.olympus.contract.request.RecordPageRequest; +import com.ruoyi.game.olympus.contract.response.BoxResponse; +import com.ruoyi.game.olympus.contract.response.RecordItemResponse; +import com.ruoyi.game.olympus.contract.response.SpinResponse; +import com.ruoyi.game.olympus.domain.OlympusBox; +import com.ruoyi.game.olympus.domain.OlympusPrizeOrnament; +import com.ruoyi.game.olympus.domain.OlympusRecord; +import com.ruoyi.game.olympus.enums.OlympusModeType; +import com.ruoyi.game.olympus.mapper.OlympusBoxMapper; +import com.ruoyi.game.olympus.mapper.OlympusPrizeOrnamentMapper; +import com.ruoyi.game.olympus.mapper.OlympusRecordMapper; +import com.ruoyi.game.olympus.service.OlympusGameService; +import com.ruoyi.system.service.ISysConfigService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Api(tags = "玫瑰女郎游戏") +@RestController +@RequestMapping("/api/olympus") +public class OlympusClientController extends BaseController { + + @Autowired private OlympusGameService gameService; + @Autowired private OlympusBoxMapper boxMapper; + @Autowired private OlympusRecordMapper recordMapper; + @Autowired private OlympusPrizeOrnamentMapper prizeOrnamentMapper; + @Autowired private ISysConfigService sysConfigService; + + /** 进行中的局不展示:结算时 settle_source=start,播完末帧才改 finish;超过单局缓存 TTL(30分钟) 视为已结束兜底 */ + private static final long IN_PROGRESS_MS = 30 * 60 * 1000L; + + @ApiOperation("获取箱子列表") + @PostMapping("/boxes") + public R boxes() { + List list = boxMapper.selectList(new LambdaQueryWrapper() + .eq(OlympusBox::getGameType, "OLYMPUS").eq(OlympusBox::getStatus, "1") + .orderByAsc(OlympusBox::getBoxId)); + List boxes = list.stream().map(b -> { + BoxResponse r = new BoxResponse(); + r.setBoxId(b.getBoxId()); + r.setBoxName(b.getName()); + r.setBasicBet(b.getBet()); + r.setPrice(b.getPrice()); + r.setBoxType(b.getBoxType()); + try { r.setModeName(OlympusModeType.fromBoxType(b.getBoxType()).getName()); } + catch (Exception e) { r.setModeName(b.getBoxType()); } + return r; + }).collect(Collectors.toList()); + return R.ok(Map.of("list", boxes)); + } + + @ApiOperation("执行旋转(新游戏或继续游戏)") + @PostMapping("/dospin") + public R dospin(@RequestBody SpinRequest req) { + Long userId = getUserId(); + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + String olympusMaintenance = sysConfigService.selectConfigByKey("olympusMaintenance"); + boolean isMaintenance = "1".equals(websiteMaintenance) || "1".equals(olympusMaintenance); + + // 仅“新开一局”(boxId 非空)受维护限制;继续当前对局(boxId 为空)不受影响,可正常播完 + boolean isNewGame = req != null && req.getBoxId() != null; + if (isMaintenance && isNewGame) { + if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......"); + if ("1".equals(olympusMaintenance)) return R.fail("玫瑰女郎维护中......"); + } + try { + SpinResponse resp = gameService.doSpin(userId, req); + return R.ok(resp); + } catch (ServiceException se) { + return se.getCode() != null ? R.fail(se.getCode().intValue(), se.getMessage()) : R.fail(se.getMessage()); + } catch (Exception e) { + return R.fail(e.getMessage()); + } + } + + @ApiOperation("查询游戏记录(分页;scope: 0全服/1我的,默认1我的;只含已播完的局)") + @PostMapping("/records") + public R records(@RequestBody(required = false) RecordPageRequest req) { + Long userId = getUserId(); + int scope = (req != null && req.getScope() != null) ? req.getScope() : 1; + int pageNum = (req != null && req.getPageNum() != null) ? req.getPageNum() : 1; + int pageSize = (req != null && req.getPageSize() != null) ? req.getPageSize() : 10; + + // 用 Java 时间算 cutoff(勿改成 SQL NOW():DB 会话时区是 UTC,create_time 存的是本地时间) + Date cutoff = new Date(System.currentTimeMillis() - IN_PROGRESS_MS); + LambdaQueryWrapper qw = new LambdaQueryWrapper() + .eq(OlympusRecord::getSettled, 1) + // 只展示已结束的局:还在播动画的那局 settle_source 仍为 start,等播完(或超时兜底)才进列表 + .and(w -> w.ne(OlympusRecord::getSettleSource, "start") + .or().lt(OlympusRecord::getCreateTime, cutoff)); + if (scope == 1) { + qw.eq(OlympusRecord::getUserId, userId); + } else { + // 全服:排除机器人/封禁/已删除用户(保留白号,白号可上榜) + qw.inSql(OlympusRecord::getUserId, + "SELECT user_id FROM tt_user WHERE user_type != '03' AND status != '1' AND del_flag = '0'"); + } + qw.orderByDesc(OlympusRecord::getCreateTime); + + Page page = recordMapper.selectPage(new Page<>(pageNum, pageSize), qw); + List records = page.getRecords(); + + // 昵称/头像(两种 scope 都返回) + Map> userMap = new HashMap<>(); + Set userIds = new HashSet<>(); + for (OlympusRecord r : records) userIds.add(r.getUserId()); + if (!userIds.isEmpty()) { + for (Map m : recordMapper.getUserInfos(userIds)) { + Object uid = m.get("user_id") != null ? m.get("user_id") : m.get("userId"); + if (uid != null) userMap.put(Long.valueOf(uid.toString()), m); + } + } + + // 饰品全名 = 名称(磨损品质):新记录按 prize_id 关联,老记录(prize_id 为空)按名称兜底 + Map exteriorById = new HashMap<>(); + Map exteriorByName = new HashMap<>(); + for (OlympusPrizeOrnament o : prizeOrnamentMapper.selectList(null)) { + if (o.getExteriorName() == null || o.getExteriorName().isEmpty()) continue; + exteriorById.putIfAbsent(o.getId(), o.getExteriorName()); + if (o.getName() != null) exteriorByName.putIfAbsent(o.getName(), o.getExteriorName()); + } + + List rows = new ArrayList<>(); + for (OlympusRecord r : records) { + RecordItemResponse vo = new RecordItemResponse(); + Map u = userMap.get(r.getUserId()); + if (u != null) { + vo.setNickName(str(u.get("nick_name"))); + vo.setAvatar(str(u.get("avatar"))); + } + vo.setCost(r.getCost()); + vo.setSettledWin(r.getSettledWin()); + vo.setPrizeImageUrl(r.getPrizeImageUrl()); + vo.setPrizeName(fullPrizeName(r.getPrizeName(), r.getPrizeId(), exteriorById, exteriorByName)); + vo.setCreateTime(r.getCreateTime()); + rows.add(vo); + } + return R.ok(Map.of("rows", rows, "total", page.getTotal())); + } + + private static String str(Object v) { + return v != null ? v.toString() : null; + } + + /** 饰品全名:名称已是全名,有磨损品质时追加 " (品质)" */ + private static String fullPrizeName(String name, Long prizeId, + Map exteriorById, Map exteriorByName) { + if (name == null) return null; + String exterior = prizeId != null ? exteriorById.get(prizeId) : null; + if (exterior == null) exterior = exteriorByName.get(name); + return exterior == null || exterior.isEmpty() ? name : name + " (" + exterior + ")"; + } + + @ApiOperation("查询是否有进行中的游戏(断线重连)") + @PostMapping("/hasActive") + public R hasActive() { + Long userId = getUserId(); + // 维护期间不强制结算:进行中的对局可正常重连并播完(“当前不受影响”,仅拦新开一局) + return R.ok(gameService.hasActive(userId)); + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBox.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBox.java new file mode 100644 index 0000000..0a96cbc --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBox.java @@ -0,0 +1,86 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.Map; + +/** 箱子配置表 game_olympus_box —— 每种模式×下注档位一个箱子 */ +@Data +@TableName("game_olympus_box") +public class OlympusBox { + + @TableId(value = "box_id", type = IdType.INPUT) + private Long boxId; + + @TableField("game_type") + private String gameType; + + /** 模式:normal | ante | buy_fs */ + @TableField("box_type") + private String boxType; + + private BigDecimal bet; + + private String name; + + /** 实际扣费 = bet × CM (normal=1/ante=2/buy_fs=20) */ + private BigDecimal price; + + @TableField("total_ornaments") + private Integer totalOrnaments; + + private BigDecimal rtp; + + @TableField("rtp_converge_ratio") + private BigDecimal rtpConvergeRatio; + + private Integer sort; + + private String status; + + @TableField("del_flag") + private String delFlag; + + private String remark; + + @TableField("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + @TableField("update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; + + // ======== transient ======== + + /** 模式中文名,运行时由 OlympusModeType 填充 */ + @TableField(exist = false) + private String modeName; + + /** 费用倍率 CM,运行时填充 */ + @TableField(exist = false) + private BigDecimal costMultiplier; + + /** 实时RTP,从 game_olympus_record 统计 */ + @TableField(exist = false) + private BigDecimal actualRtp; + + /** 各身份分布反推目标RTP(%),运行时填充 {audience: percent} */ + @TableField(exist = false) + private Map targetRtpByAudience; + + /** 各身份实时RTP(%),运行时填充 {audience: percent} */ + @TableField(exist = false) + private Map actualRtpByAudience; + + /** 各身份一轮总回合(按各自固定池分布反推),运行时填充 {audience: n} */ + @TableField(exist = false) + private Map totalRoundsByAudience; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxOrnament.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxOrnament.java new file mode 100644 index 0000000..a2ae923 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxOrnament.java @@ -0,0 +1,51 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 箱子-饰品桶分布表 game_olympus_box_ornament(多身份) + * 唯一键 (box_id, ornament_id, audience):每种身份一套独立的 count/used 分布。 + * 白号(audience=white)独立库存,抽取只递减自己的 used,永不消耗其他身份。 + */ +@Data +@TableName("game_olympus_box_ornament") +public class OlympusBoxOrnament { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("box_id") + private Long boxId; + + @TableField("ornament_id") + private Integer ornamentId; + + /** 身份维度 common/anchor/robot/white */ + private String audience; + + /** 分配回合数(加权随机权重) */ + private Integer count; + + /** 已消耗回合数:剩余 = count - used */ + private Integer used; + + // ======== transient(SQL JOIN 填充)======== + + @TableField(exist = false) + private String ornamentName; + + @TableField(exist = false) + private java.math.BigDecimal expectMul; + + /** 剩余 = count - used,SQL 计算填充 */ + @TableField(exist = false) + private Integer remaining; + + /** 本轮收敛阶段该桶已动态选取回合数(轮次账本) */ + @TableField(exist = false) + private Long convergeCount; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxTemplate.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxTemplate.java new file mode 100644 index 0000000..19fd212 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxTemplate.java @@ -0,0 +1,45 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** 箱子饰品分布模板 game_olympus_box_template(按身份 audience,抽干补桶依据) */ +@Data +@TableName("game_olympus_box_template") +public class OlympusBoxTemplate { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("box_id") + private Long boxId; + + private String audience; + + private String name; + + @TableField("total_rounds") + private Integer totalRounds; + + @TableField("items_json") + private String itemsJson; + + @TableField("is_active") + private Integer isActive; + + @TableField("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + /** 反序列化后的分布,回传前端用 */ + @TableField(exist = false) + private List> items; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusOrnament.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusOrnament.java new file mode 100644 index 0000000..a7f2e61 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusOrnament.java @@ -0,0 +1,30 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; + +/** 饰品桶定义表 game_olympus_ornament(17 桶:0x~1000x,展示与落桶区间参考) */ +@Data +@TableName("game_olympus_ornament") +public class OlympusOrnament { + + @TableId(type = IdType.INPUT) + private Long id; + + private String gameType; + + /** 期望倍率(区间均值,最高 1000) */ + private BigDecimal expectMul; + + private String name; + + /** 落桶区间下限(含) */ + private BigDecimal rangeLow; + + /** 落桶区间上限(含) */ + private BigDecimal rangeHigh; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeOrnament.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeOrnament.java new file mode 100644 index 0000000..8a20e5a --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeOrnament.java @@ -0,0 +1,36 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** 返奖饰品定义表 game_olympus_prize_ornament(展示用,按赢额区间匹配) */ +@Data +@TableName("game_olympus_prize_ornament") +public class OlympusPrizeOrnament { + + @TableId(type = IdType.AUTO) + private Long id; + + private String gameType; + private String name; + private String imageUrl; + private Integer grade; + private String exteriorName; + private Integer sort; + private String status; + private Integer isFallback; + + // ======== transient ======== + + @TableField(exist = false) + private java.math.BigDecimal winLow; + + @TableField(exist = false) + private java.math.BigDecimal winHigh; + + @TableField(exist = false) + private String levelImg; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeRange.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeRange.java new file mode 100644 index 0000000..d707787 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeRange.java @@ -0,0 +1,29 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; + +/** 返奖饰品区间表 game_olympus_prize_range */ +@Data +@TableName("game_olympus_prize_range") +public class OlympusPrizeRange { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long prizeId; + private BigDecimal winLow; + private BigDecimal winHigh; + + // 兼容 sweet 的字段命名(部分查询用 gameType/ornamentId/sort,olympus 表无这些列时忽略) + @com.baomidou.mybatisplus.annotation.TableField(exist = false) + private String gameType; + @com.baomidou.mybatisplus.annotation.TableField(exist = false) + private Long ornamentId; + @com.baomidou.mybatisplus.annotation.TableField(exist = false) + private Integer sort; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRecord.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRecord.java new file mode 100644 index 0000000..dd262c6 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRecord.java @@ -0,0 +1,43 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** 玩家游玩记录表 game_olympus_record —— 每次开局一条 */ +@Data +@TableName("game_olympus_record") +public class OlympusRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long userId; + private Long boxId; + private Long roundId; + private BigDecimal cost; + private BigDecimal totalWin; + + private Integer settled; // 0未结算 1已结算(幂等守卫) + private String modeType; // normal/ante/buy_fs + private String rewardType; // gold / ornament + private BigDecimal settledWin; + private Long prizeId; + private String prizeName; + private String prizeImageUrl; + private String prizeLevelImg; + private String settleSource; // start=开局已结算(动画还在播) / finish=末帧播完(已结束,进战绩列表);reconnect/scheduled/force_settle 为历史值 + + /** 身份 common/anchor/robot/white:不入库,列表查询时按 tt_user 运行期回填(与奖池 4 池同一口径) */ + @TableField(exist = false) + private String audience; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRewardLog.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRewardLog.java new file mode 100644 index 0000000..dc65b3f --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRewardLog.java @@ -0,0 +1,52 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** 奖池水位调整日志 game_olympus_reward_log(注水/抽水/重置审计) */ +@Data +@TableName("game_olympus_reward_log") +public class OlympusRewardLog { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("box_id") + private Long boxId; + + @TableField("box_name") + private String boxName; + + /** 身份:common/anchor/robot/white(奖池按 箱子×身份 拆池) */ + private String audience; + + /** 1注水 2抽水 3重置 */ + private Integer type; + + private BigDecimal amount; + + @TableField("balance_before") + private BigDecimal balanceBefore; + + @TableField("balance_after") + private BigDecimal balanceAfter; + + /** 操作后全站总池余额快照 */ + @TableField("pool_balance") + private BigDecimal poolBalance; + + private String operator; + + private String remark; + + @TableField("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRound.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRound.java new file mode 100644 index 0000000..a82d7cb --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRound.java @@ -0,0 +1,40 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** 预生成回合表 game_olympus_round */ +@Data +@TableName("game_olympus_round") +public class OlympusRound { + + @TableId(value = "round_id", type = IdType.INPUT) + private Long roundId; + + private Long boxId; + + /** 落入的饰品桶ID (1-17) */ + private Integer ornamentId; + + /** 实际总倍率 = 所有步骤赢额之和 / bet(≤1000,已封顶) */ + private BigDecimal actualMul; + + /** 免费旋转总次数(0=未进FS) */ + private Integer freeSpinCount; + + /** 总 cascade 步数 */ + private Integer totalCascades; + + private Date createTime; + + // ======== transient ======== + + @TableField(exist = false) + private String boxType; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusStep.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusStep.java new file mode 100644 index 0000000..cb22c97 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusStep.java @@ -0,0 +1,79 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; + +/** 回合步骤表 game_olympus_step —— 前端逐步播放的每一帧 */ +@Data +@TableName("game_olympus_step") +public class OlympusStep { + + @TableId(type = IdType.INPUT) + private Long id; + + private Long roundId; + + /** 免费旋转序号 (0=基础旋转, >0=第N次免费旋转) */ + private Integer freeSpinId; + + /** 本旋转内 cascade 序号 */ + private Integer cascadeSeq; + + /** 网格状态 JSON 6×5 */ + private String gridState; + + /** 炸弹符号 JSON [{"pos","val","quality"}] */ + private String multiplierDetail; + + /** 本步中奖明细 JSON */ + private String symbolLinks; + + /** 本旋累计消除符号 JSON {"CROWN":9,...} */ + private String accumulatedSymbols; + + /** 获胜者面板 JSON:本旋逐波追加 [{"symbol","count","win"}] */ + private String spinWinners; + + /** 消除位置 JSON */ + private String removedPositions; + + /** 下落数据 JSON */ + private String dropData; + + /** 填充数据 JSON */ + private String fillData; + + /** 基础盘触发免费旋转 JSON */ + private String scatterTrigger; + + /** 免费旋转重触发 JSON */ + private String retriggerInfo; + + /** 本步基础分(未乘炸弹) */ + private BigDecimal baseScore; + + /** 本步赢额(回合末帧含炸弹结算) */ + private BigDecimal winAmount; + + /** 本步前累计炸弹倍数 */ + private BigDecimal accumulatorBefore; + + /** 本步后累计炸弹倍数(跨旋传递) */ + private BigDecimal accumulatorAfter; + + /** 整局累计赢额 */ + private BigDecimal spinWin; + + /** 本步赢额被 1000x 封顶截断 0否1是 */ + private Integer winCapped; + + /** 爆奖档位(小/中/大:small/middle/big;只在免费旋转里判档,按单旋赢额) */ + private String bigwinLevel; + + /** 本旋末帧 */ + private Integer isFinal; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusUser.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusUser.java new file mode 100644 index 0000000..ee84a8f --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusUser.java @@ -0,0 +1,26 @@ +package com.ruoyi.game.olympus.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** 玩家统计表 game_olympus_user —— 每次结算累加 */ +@Data +@TableName("game_olympus_user") +public class OlympusUser { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long userId; + private Long totalSpins; + private BigDecimal totalCost; + private BigDecimal totalWin; + private Integer totalFreeSpins; + private BigDecimal maxWinMul; + private Date updateTime; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusAudience.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusAudience.java new file mode 100644 index 0000000..ae84a74 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusAudience.java @@ -0,0 +1,43 @@ +package com.ruoyi.game.olympus.enums; + +import com.ruoyi.domain.entity.sys.TtUser; + +/** + * 身份维度(audience):每种身份一套独立的饰品桶数量分布,实现差异化 RTP。 + * 与 olympus-py config.AUDIENCES 对齐。 + * 解析优先级(白号最高,压过 user_type): + * is_blank_account==1 → white;user_type=01 → anchor;03 → robot;其余 → common。 + */ +public enum OlympusAudience { + COMMON("common"), // 普通用户(user_type=02 及招商/卡商/超管等其他) + ANCHOR("anchor"), // 主播(user_type=01) + ROBOT("robot"), // 机器人(user_type=03) + WHITE("white"); // 白号(is_blank_account=1,压过 user_type) + + private final String code; + + OlympusAudience(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + /** 白号:纯概率隔离池,不计水位、不消耗其他身份库存 */ + public boolean isWhite() { + return this == WHITE; + } + + /** + * 按玩家解析 audience:白号最高优先,压过 user_type。 + */ + public static OlympusAudience resolve(TtUser player) { + if (player == null) return COMMON; + if (Integer.valueOf(1).equals(player.getIsBlankAccount())) return WHITE; + String userType = player.getUserType(); + if ("01".equals(userType)) return ANCHOR; + if ("03".equals(userType)) return ROBOT; + return COMMON; + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusModeType.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusModeType.java new file mode 100644 index 0000000..0d37e29 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusModeType.java @@ -0,0 +1,58 @@ +package com.ruoyi.game.olympus.enums; + +/** + * Olympus 三种互斥模式(对齐 olympus-py config.MODES): + * normal(1x) / ante(2x, 底注) / buy_fs(50x, 购买免费旋转)。 + */ +public enum OlympusModeType { + NORMAL("normal", "普通旋转", 1.0), + ANTE("ante", "底注", 2.0), + // 售价 50 倍(原 20 倍):局均赔付 = 50×0.8 = 40 倍底注 → 配比落在 25x/40x/60x 桶 + // (FS 可用预算 22~57),每局才演得出 6~10 次消除 + 4~6 次炸弹累计; + // 20 倍时局均只有 16 倍,配比被迫压在 10x/16x 桶(预算 7~13),动画"钱不够"。 + BUY_FS("buy_fs", "购买免费旋转", 50.0); + + private final String boxType; + private final String name; + private final double costMultiplier; + + OlympusModeType(String boxType, String name, double costMultiplier) { + this.boxType = boxType; + this.name = name; + this.costMultiplier = costMultiplier; + } + + public String getBoxType() { return boxType; } + public String getName() { return name; } + public double getCostMultiplier() { return costMultiplier; } + + /** 购买模式:保证直接进入免费旋转 */ + public boolean isBuyMode() { + return this == BUY_FS; + } + + /** 底注效果:SCATTER 概率提升(仅 ante) */ + public boolean anteBoost() { + return this == ANTE; + } + + /** 该模式可用饰品桶 ID 范围 [minId, maxId],对齐 py 端 MODE_ORNAMENT_IDS */ + public int[] getOrnamentRange() { + switch (this) { + case NORMAL: return new int[]{1, 13}; // 0x-150x + case ANTE: return new int[]{1, 14}; // 0x-200x + // buy_fs 从 10x 起(原 5 = 4x 起):4x/6.5x 桶的整局预算只有 1~3.5 倍底注, + // 演不出"15 旋里 10 旋都在消 + 炸弹累计",玩家看到的一直是空转动画。 + // 10x 起后每局预算 7~13 倍底注,能装 5~8 次消除 + 1~2 次炸弹累计。 + case BUY_FS: return new int[]{7, 17}; // 10x-1000x + default: return new int[]{1, 17}; + } + } + + public static OlympusModeType fromBoxType(String boxType) { + for (OlympusModeType t : values()) { + if (t.boxType.equals(boxType)) return t; + } + throw new IllegalArgumentException("Unknown boxType: " + boxType); + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxMapper.java new file mode 100644 index 0000000..eee1ae2 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusBox; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OlympusBoxMapper extends BaseMapper { +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxOrnamentMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxOrnamentMapper.java new file mode 100644 index 0000000..5d7c608 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxOrnamentMapper.java @@ -0,0 +1,32 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusBoxOrnament; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 箱子-饰品桶分布 Mapper(多身份 audience)。 + * 所有查询/消耗均按 (boxId, audience) 维度隔离,白号不碰其他身份库存。 + */ +@Mapper +public interface OlympusBoxOrnamentMapper extends BaseMapper { + + /** 查某箱某身份的桶分布(含 expectMul/remaining 连表填充) */ + List selectByBoxIdAndAudience(@Param("boxId") Long boxId, + @Param("audience") String audience); + + int batchInsert(@Param("list") List list); + + /** 删除某箱某身份的分布(重算/应用模板前清空该身份) */ + int deleteByBoxIdAndAudience(@Param("boxId") Long boxId, @Param("audience") String audience); + + /** 该身份某桶 used+1(仅固定阶段消耗) */ + int incrUsed(@Param("boxId") Long boxId, @Param("ornamentId") Integer ornamentId, + @Param("audience") String audience); + + /** 该身份 used 归 0(补桶) */ + int resetUsed(@Param("boxId") Long boxId, @Param("audience") String audience); +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxTemplateMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxTemplateMapper.java new file mode 100644 index 0000000..e9ed706 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxTemplateMapper.java @@ -0,0 +1,10 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusBoxTemplate; +import org.apache.ibatis.annotations.Mapper; + +/** 箱子饰品分布模板 game_olympus_box_template(按身份 audience)。 */ +@Mapper +public interface OlympusBoxTemplateMapper extends BaseMapper { +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusOrnamentMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusOrnamentMapper.java new file mode 100644 index 0000000..2f0e847 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusOrnamentMapper.java @@ -0,0 +1,10 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusOrnament; +import org.apache.ibatis.annotations.Mapper; + +/** 饰品桶定义 game_olympus_ornament(17 桶)。 */ +@Mapper +public interface OlympusOrnamentMapper extends BaseMapper { +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeOrnamentMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeOrnamentMapper.java new file mode 100644 index 0000000..76f944c --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeOrnamentMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusPrizeOrnament; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OlympusPrizeOrnamentMapper extends BaseMapper { +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeRangeMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeRangeMapper.java new file mode 100644 index 0000000..cb95bed --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeRangeMapper.java @@ -0,0 +1,50 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusPrizeRange; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 返奖饰品区间:按赢额匹配展示饰品。联表 game_olympus_prize_ornament 取 name/imageUrl/grade, + * 再联 tt_ornaments_level 取 level_img AS levelImageUrl(品质/等级图片,前端 prizeInfo.levelImageUrl 用这个)。 + */ +@Mapper +public interface OlympusPrizeRangeMapper extends BaseMapper { + + /** 精确区间匹配 win_low < win <= win_high */ + @Select("SELECT o.id, o.name, o.image_url AS imageUrl, o.grade, o.exterior_name AS exteriorName, " + + "l.level_img AS levelImageUrl " + + "FROM game_olympus_prize_range r JOIN game_olympus_prize_ornament o ON o.id = r.prize_id " + + "LEFT JOIN tt_ornaments_level l ON l.id = o.grade " + + "WHERE #{win} > r.win_low AND #{win} <= r.win_high AND o.status = '1' " + + "ORDER BY r.win_low DESC LIMIT 1") + Map findPrizeByWin(@Param("win") BigDecimal win); + + /** 兜底饰品(is_fallback=1) */ + @Select("SELECT o.id, o.name, o.image_url AS imageUrl, o.grade, o.exterior_name AS exteriorName, " + + "l.level_img AS levelImageUrl " + + "FROM game_olympus_prize_ornament o LEFT JOIN tt_ornaments_level l ON l.id = o.grade " + + "WHERE o.is_fallback = 1 AND o.status = '1' ORDER BY o.sort ASC LIMIT 1") + Map findFallbackPrize(); + + /** 最低区间饰品 */ + @Select("SELECT o.id, o.name, o.image_url AS imageUrl, o.grade, o.exterior_name AS exteriorName, " + + "l.level_img AS levelImageUrl " + + "FROM game_olympus_prize_range r JOIN game_olympus_prize_ornament o ON o.id = r.prize_id " + + "LEFT JOIN tt_ornaments_level l ON l.id = o.grade " + + "WHERE o.status = '1' ORDER BY r.win_low ASC LIMIT 1") + Map findLowestPrize(); + + /** 最高区间饰品 */ + @Select("SELECT o.id, o.name, o.image_url AS imageUrl, o.grade, o.exterior_name AS exteriorName, " + + "l.level_img AS levelImageUrl " + + "FROM game_olympus_prize_range r JOIN game_olympus_prize_ornament o ON o.id = r.prize_id " + + "LEFT JOIN tt_ornaments_level l ON l.id = o.grade " + + "WHERE o.status = '1' ORDER BY r.win_high DESC LIMIT 1") + Map findHighestPrize(); +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRecordMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRecordMapper.java new file mode 100644 index 0000000..0ad1b7f --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRecordMapper.java @@ -0,0 +1,67 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Mapper +public interface OlympusRecordMapper extends BaseMapper { + + /** 批量查各箱实时 RTP(排除白号/机器人/封禁/删除,与首页口径一致) */ + @Select("") + List> selectBoxRtpList(@Param("boxIds") List boxIds); + + @Select("") + List> getUserInfos(@Param("userIds") Set userIds); + + /** 每日统计汇总(排除白号/机器人/封禁/删除) */ + @Select("") + Map winSummary(@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd); + + /** 每日统计列表:按天聚合(口径与 winSummary 一致,排除白号/机器人/封禁/删除) */ + @Select("") + List> dailyList(@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd); + + /** + * 某箱 + 某身份奖池真实累计(按已结算记录派生)。身份口径同 {@link com.ruoyi.game.olympus.enums.OlympusAudience#resolve}, + * 四个身份互斥且并集 = 全部已结算记录(拆分口径,总量与旧"非白号一个池"一致)。 + * totalBet=Σcost,totalWin=Σsettled_win(实际发放价值)。 + */ + @Select("") + Map sumPool(@Param("boxId") Long boxId, @Param("audience") String audience); +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRewardLogMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRewardLogMapper.java new file mode 100644 index 0000000..483b4cb --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRewardLogMapper.java @@ -0,0 +1,10 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusRewardLog; +import org.apache.ibatis.annotations.Mapper; + +/** 奖池水位调整日志 game_olympus_reward_log。 */ +@Mapper +public interface OlympusRewardLogMapper extends BaseMapper { +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRoundMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRoundMapper.java new file mode 100644 index 0000000..c5dfba6 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRoundMapper.java @@ -0,0 +1,28 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusRound; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@Mapper +public interface OlympusRoundMapper extends BaseMapper { + + OlympusRound pickByOrnament(@Param("boxId") Long boxId, @Param("ornamentId") Integer ornamentId, + @Param("needFreeSpins") Boolean needFreeSpins); + + OlympusRound pickAnyByBox(@Param("boxId") Long boxId, @Param("needFreeSpins") Boolean needFreeSpins); + + /** 收敛阶段:桶内取 actual_mul 最接近 desiredMul 的回合,优先 actual_mul ≤ capMul */ + OlympusRound pickClosestMul(@Param("boxId") Long boxId, @Param("ornamentId") Integer ornamentId, + @Param("needFreeSpins") Boolean needFreeSpins, + @Param("desiredMul") BigDecimal desiredMul, + @Param("capMul") BigDecimal capMul); + + /** 统计某箱池内各桶实际均值:[{ornamentId, sampleCount, avgMul}](配比/RTP 按它算) */ + List> avgMulByOrnament(@Param("boxId") Long boxId); +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusStepMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusStepMapper.java new file mode 100644 index 0000000..0240957 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusStepMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusStep; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface OlympusStepMapper extends BaseMapper { + + @Select("SELECT * FROM game_olympus_step WHERE round_id = #{roundId} ORDER BY id ASC") + List selectByRoundId(@Param("roundId") Long roundId); +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusUserMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusUserMapper.java new file mode 100644 index 0000000..d367b16 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusUserMapper.java @@ -0,0 +1,14 @@ +package com.ruoyi.game.olympus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.olympus.domain.OlympusUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface OlympusUserMapper extends BaseMapper { + + @Select("SELECT * FROM game_olympus_user WHERE user_id = #{userId} LIMIT 1") + OlympusUser selectByUserId(@Param("userId") Long userId); +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/PickedRound.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/PickedRound.java new file mode 100644 index 0000000..3e954e0 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/PickedRound.java @@ -0,0 +1,17 @@ +package com.ruoyi.game.olympus.model; + +import com.ruoyi.game.olympus.domain.OlympusRound; +import com.ruoyi.game.olympus.domain.OlympusStep; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +/** 选中回合 + 其步骤 + 阶段标记(固定/收敛,决定是否 incrUsed 与 conv 记账) */ +@Data +@AllArgsConstructor +public class PickedRound { + private OlympusRound round; + private List steps; + private boolean converge; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusGameCache.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusGameCache.java new file mode 100644 index 0000000..3e20200 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusGameCache.java @@ -0,0 +1,54 @@ +package com.ruoyi.game.olympus.model.cache; + +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** Olympus 单局进行中缓存(Redis,TTL 30 分钟) */ +@Data +public class OlympusGameCache implements Serializable { + + private Long boxId; + private String boxType; // normal|ante|buy_fs + private BigDecimal bet; + private BigDecimal cost; + + private Long roundId; + private BigDecimal totalMul; // round.actualMul + private Integer freeSpinCount; + + private int stepIndex; // 当前已返回步数(1-based) + private List steps; + + private BigDecimal totalWin; + + private Long recordId; + private Long boxRecordId; + + /** 身份维度 common/anchor/robot/white,白号隔离依据 */ + private String audience; + /** 是否白号:白号不注水/不扣水/不受水位约束 */ + private boolean blankAccount; + + @Data + public static class CachedStep implements Serializable { + private int freeSpinId; + private int cascadeSeq; + private String gridState; + private String multiplierDetail; + private String symbolLinks; // 本旋累积连线(含 tumble) + private BigDecimal winAmount; // 本旋累计(该旋末帧 = 这一旋的总额) + private BigDecimal spinWin; // 本局累计(末帧 = totalWin) + /** 本步后炸弹累计倍数(FS 跨旋传递);断线重连时前端靠它恢复累计倍数 */ + private BigDecimal accumulatorAfter; + private int winCapped; + private String bigwinLevel; + private int isFinal; + private String retriggerInfo; + private String dropData; + private String fillData; + private String scatterTrigger; + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusRewardPoolCache.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusRewardPoolCache.java new file mode 100644 index 0000000..ed791b9 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusRewardPoolCache.java @@ -0,0 +1,16 @@ +package com.ruoyi.game.olympus.model.cache; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * Olympus 按箱奖池(水位)缓存。 + * 水位 = totalBet - totalWin + adjust(人工注水/抽水),仅真实玩家(白号隔离)。 + */ +@Data +public class OlympusRewardPoolCache { + private BigDecimal totalBet; + private BigDecimal totalWin; + private BigDecimal adjust; +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCacheRepository.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCacheRepository.java new file mode 100644 index 0000000..c8b151c --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCacheRepository.java @@ -0,0 +1,30 @@ +package com.ruoyi.game.olympus.repository; + +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.game.olympus.model.cache.OlympusGameCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +import static com.ruoyi.admin.config.RedisConstants.OLYMPUS_GAME_CACHE; + +/** 单局进行中缓存(Redis,TTL 30 分钟) */ +@Component +public class OlympusCacheRepository { + + @Autowired + private RedisCache redisCache; + + public OlympusGameCache getCache(Long userId) { + return redisCache.getCacheObject(OLYMPUS_GAME_CACHE + userId); + } + + public void setCache(Long userId, OlympusGameCache cache) { + redisCache.setCacheObject(OLYMPUS_GAME_CACHE + userId, cache, 30, TimeUnit.MINUTES); + } + + public void clearCache(Long userId) { + redisCache.deleteObject(OLYMPUS_GAME_CACHE + userId); + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCycleRepository.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCycleRepository.java new file mode 100644 index 0000000..bf96b9a --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCycleRepository.java @@ -0,0 +1,81 @@ +package com.ruoyi.game.olympus.repository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.concurrent.TimeUnit; + +import static com.ruoyi.admin.config.RedisConstants.OLYMPUS_GAME_CYCLE; + +/** + * 一轮内 RTP 动态收敛账本,按 (boxId, audience) 隔离。 + * Hash key = ruoyi:olympus:cycle:{boxId}:{audience},字段 drawn/mulSum/conv:{ornamentId}。 + * 白号(audience=white)也有独立账本,与其他身份完全隔离。 + */ +@Component +public class OlympusCycleRepository { + + @Autowired + public RedisTemplate redisTemplate; + + private static final String F_DRAWN = "drawn"; + private static final String F_MUL_SUM = "mulSum"; + private static final String F_CONV_PREFIX = "conv:"; + private static final long TTL_DAYS = 30; + + private String key(Long boxId, String audience) { + return OLYMPUS_GAME_CYCLE + boxId + ":" + audience; + } + + public long drawn(Long boxId, String audience) { + Object v = redisTemplate.opsForHash().get(key(boxId, audience), F_DRAWN); + return v == null ? 0L : toLong(v); + } + + public double mulSum(Long boxId, String audience) { + Object v = redisTemplate.opsForHash().get(key(boxId, audience), F_MUL_SUM); + return v == null ? 0d : toDouble(v); + } + + /** 记一抽:drawn+1、mulSum+actualMul;收敛阶段额外 conv:{ornamentId}+1。 */ + public void recordDraw(Long boxId, String audience, Integer ornamentId, BigDecimal actualMul, boolean converge) { + String k = key(boxId, audience); + redisTemplate.opsForHash().increment(k, F_DRAWN, 1L); + double mul = actualMul != null ? actualMul.doubleValue() : 0d; + if (mul != 0d) redisTemplate.opsForHash().increment(k, F_MUL_SUM, mul); + if (converge && ornamentId != null) { + redisTemplate.opsForHash().increment(k, F_CONV_PREFIX + ornamentId, 1L); + } + redisTemplate.expire(k, TTL_DAYS, TimeUnit.DAYS); + } + + public void resetCycle(Long boxId, String audience) { + redisTemplate.delete(key(boxId, audience)); + } + + public long convergeCount(Long boxId, String audience, Integer ornamentId) { + Object v = redisTemplate.opsForHash().get(key(boxId, audience), F_CONV_PREFIX + ornamentId); + return v == null ? 0L : toLong(v); + } + + /** 本轮实时 RTP(%) = mulSum / drawn / cm × 100(common 身份口径,供后台展示)。 */ + public BigDecimal realtimeRtp(Long boxId, String audience, double cm) { + long d = drawn(boxId, audience); + if (d <= 0 || cm <= 0) return null; + double rtp = mulSum(boxId, audience) / d / cm * 100; + return BigDecimal.valueOf(rtp).setScale(2, RoundingMode.HALF_UP); + } + + private static long toLong(Object v) { + if (v instanceof Number) return ((Number) v).longValue(); + try { return Long.parseLong(v.toString().trim()); } catch (Exception e) { return 0L; } + } + + private static double toDouble(Object v) { + if (v instanceof Number) return ((Number) v).doubleValue(); + try { return Double.parseDouble(v.toString().trim()); } catch (Exception e) { return 0d; } + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusRewardPoolCacheRepository.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusRewardPoolCacheRepository.java new file mode 100644 index 0000000..6aa960c --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusRewardPoolCacheRepository.java @@ -0,0 +1,115 @@ +package com.ruoyi.game.olympus.repository; + +import com.ruoyi.game.olympus.model.cache.OlympusRewardPoolCache; +import com.ruoyi.game.olympus.mapper.OlympusRecordMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.ruoyi.admin.config.RedisConstants.OLYMPUS_GAME_REWARD_POOL_CACHE; + +/** + * Olympus 按【箱子 × 身份】奖池(水位)仓储 —— 对齐荒野争锋 bullet 的按箱水位模型,再按身份(audience)拆池。 + * 每箱每身份独立键::box:{boxId}:{audience}:dbbet / :dbwin / :adjust;全局 adjust 也按身份一份。 + * 水位 = box:bet − box:win + box:adjust(可能为负)。 + *

+ * 记账口径(方案B''):bet/win **由 DB 已结算记录按身份派生**(game_olympus_record + tt_user), + * Redis 仅做短缓存供 payoutCap/展示快速读取;adjust 为管理员人工注水/抽水(Redis)。 + * 因 record 与发奖同一事务提交,池值恒等于已提交的真实账,无"提交后记账崩溃窗口"。 + */ +@Component +public class OlympusRewardPoolCacheRepository { + + @Autowired + public RedisTemplate redisTemplate; + + @Autowired + private OlympusRecordMapper recordMapper; + + private static final String KEY_BOX_PREFIX = OLYMPUS_GAME_REWARD_POOL_CACHE + ":box:"; + /** 全局人工调整累计(各箱 adjust 之和,仅展示),按身份一份 */ + private static final String KEY_ADJUST = OLYMPUS_GAME_REWARD_POOL_CACHE + ":adjust:"; + + private static final String KEY_DB_BET = ":dbbet"; + private static final String KEY_DB_WIN = ":dbwin"; + /** ponytail: DB 派生结果短缓存;DB 为真相,TTL 内只是短暂滞后、自愈,无永久漂移 */ + private static final long DB_CACHE_TTL_SECONDS = 5; + + private static String boxKey(Long boxId, String audience, String suffix) { + return KEY_BOX_PREFIX + boxId + ":" + audience + suffix; + } + + private static String adjustKey(String audience) { + return KEY_ADJUST + audience; + } + + /** 管理员逐箱注水/抽水:delta 为正注水、为负抽水;同时累加该身份的全局 adjust 键 */ + public void adjustBox(Long boxId, String audience, BigDecimal delta) { + if (boxId == null || delta == null || delta.signum() == 0) return; + redisTemplate.opsForValue().increment(boxKey(boxId, audience, ":adjust"), delta.doubleValue()); + redisTemplate.opsForValue().increment(adjustKey(audience), delta.doubleValue()); + } + + /** 仅调整全局 adjust 键(重置已删除箱子遗留残余),不动箱子键 */ + public void adjustGlobal(String audience, BigDecimal delta) { + if (delta == null || delta.signum() == 0) return; + redisTemplate.opsForValue().increment(adjustKey(audience), delta.doubleValue()); + } + + public BigDecimal getGlobalAdjust(String audience) { + return readValue(adjustKey(audience)); + } + + public OlympusRewardPoolCache getBoxCache(Long boxId, String audience) { + OlympusRewardPoolCache cache = new OlympusRewardPoolCache(); + cache.setTotalBet(dbPoolTotal(boxId, audience, KEY_DB_BET)); + cache.setTotalWin(dbPoolTotal(boxId, audience, KEY_DB_WIN)); + cache.setAdjust(readValue(boxKey(boxId, audience, ":adjust"))); + return cache; + } + + public BigDecimal getBoxAdjust(Long boxId, String audience) { + return readValue(boxKey(boxId, audience, ":adjust")); + } + + /** 箱子×身份水位 = 该身份箱内累计投注 − 累计返奖 + 人工调整(可能为负) */ + public BigDecimal getBoxBalance(Long boxId, String audience) { + OlympusRewardPoolCache c = getBoxCache(boxId, audience); + return c.getTotalBet().subtract(c.getTotalWin()).add(c.getAdjust()) + .setScale(2, java.math.RoundingMode.HALF_UP); + } + + /** 读取某箱某身份 bet/win 的 DB 派生值(短缓存;缓存缺失时按 record 聚合重算) */ + private BigDecimal dbPoolTotal(Long boxId, String audience, String suffix) { + String key = boxKey(boxId, audience, suffix); + Object v = redisTemplate.opsForValue().get(key); + if (v != null) return toBd(v); + refreshDbPool(boxId, audience); + return toBd(redisTemplate.opsForValue().get(key)); + } + + private void refreshDbPool(Long boxId, String audience) { + Map row = boxId != null ? recordMapper.sumPool(boxId, audience) : null; + BigDecimal bet = toBd(row != null ? row.get("totalBet") : null); + BigDecimal win = toBd(row != null ? row.get("totalWin") : null); + redisTemplate.opsForValue().set(boxKey(boxId, audience, KEY_DB_BET), bet.doubleValue(), DB_CACHE_TTL_SECONDS, TimeUnit.SECONDS); + redisTemplate.opsForValue().set(boxKey(boxId, audience, KEY_DB_WIN), win.doubleValue(), DB_CACHE_TTL_SECONDS, TimeUnit.SECONDS); + } + + private BigDecimal readValue(String key) { + return toBd(redisTemplate.opsForValue().get(key)); + } + + private static BigDecimal toBd(Object v) { + if (v == null) return BigDecimal.ZERO; + try { + return new BigDecimal(v.toString()); + } catch (Exception e) { + return BigDecimal.ZERO; + } + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusDistributionService.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusDistributionService.java new file mode 100644 index 0000000..464ac0b --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusDistributionService.java @@ -0,0 +1,354 @@ +package com.ruoyi.game.olympus.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.game.olympus.domain.OlympusBox; +import com.ruoyi.game.olympus.domain.OlympusBoxOrnament; +import com.ruoyi.game.olympus.domain.OlympusBoxTemplate; +import com.ruoyi.game.olympus.domain.OlympusOrnament; +import com.ruoyi.game.olympus.enums.OlympusModeType; +import com.ruoyi.game.olympus.mapper.OlympusBoxMapper; +import com.ruoyi.game.olympus.mapper.OlympusBoxOrnamentMapper; +import com.ruoyi.game.olympus.mapper.OlympusBoxTemplateMapper; +import com.ruoyi.game.olympus.mapper.OlympusOrnamentMapper; +import com.ruoyi.game.olympus.mapper.OlympusRoundMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +/** + * Olympus 饰品桶分布计算 / 重置服务(按身份 audience)。 + * 算法移植自 sweet:目标 RTP → 幂律权重二分 → 单调约束 → 微调逼近; + * 差异:expect_mul 从 game_olympus_ornament 读,基础权重按模式桶区间通用生成(30×0.6^k)。 + */ +@Service +public class OlympusDistributionService { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Autowired private OlympusBoxMapper boxMapper; + @Autowired private OlympusBoxOrnamentMapper boxOrnamentMapper; + @Autowired private OlympusBoxTemplateMapper boxTemplateMapper; + @Autowired private OlympusOrnamentMapper ornamentMapper; + @Autowired private OlympusRoundMapper roundMapper; + + /** + * 桶"实际发出期望"覆盖:桶 17 展示 1000x(区间 630~1370),但单局封顶 1000, + * 真正发出去的是 min(原始分, 1000)。理论值(原始分均匀分布)E[min(x,1000)] = 907.5; + * 实测(buy_fs 10 箱 × 桶17 全部回合 = 200 个)均值 834.6、中位 821.3、27.5% 触顶, + * 因为生成端种的大奖目标集中在 1000 附近而不是均匀分布。 + * 配比计算(calcRounds)与后台「目标RTP」都按"实际会发出去多少钱"算,所以用实测值: + * 用名义 1000 会把 RTP 高估约 0.5~1.6 个百分点。 + * 注:抽回合时的 payoutCap 过滤(OlympusRoundPicker)仍读 DB 的名义 expect_mul,偏保守、保留安全余量。 + */ + private static final Map PAYOUT_EXPECT_OVERRIDE = Map.of(17, 835.0); + + /** 饰品桶 ID → 实际发出期望(受封顶影响的桶用覆盖值,其余用 DB 的 expect_mul)。 */ + public Map loadOrnamentMul() { + Map m = new HashMap<>(); + for (OlympusOrnament o : ornamentMapper.selectList(null)) { + if (o.getId() == null) continue; + int id = o.getId().intValue(); + Double override = PAYOUT_EXPECT_OVERRIDE.get(id); + if (override != null) { m.put(id, override); continue; } + m.put(id, o.getExpectMul() != null ? o.getExpectMul().doubleValue() : 0.0); + } + return m; + } + + /** 该桶池子里一条回合都没有时才回退名义期望:只要池子有回合,它的均值就是这个箱子的真实期望 */ + private static final int MIN_POOL_SAMPLE = 1; + + /** + * 桶 → **这个箱子的池子实际会赔出去多少钱**的期望(AVG(actual_mul),按 boxId 统计)。 + * + * 为什么不用名义 expect_mul:桶内实际均值与区间中点有偏差(实测 -18% ~ +11%,桶 17 还受 1000x 封顶 + * 影响只有 ~835),用名义值算配比 → 「设 80% 实际赔 81.8%」这种系统性偏差;而且每个箱子的池子是 + * 独立生成的一批回合,只有按它自己的池内均值算,算出来的配比才真的等于该箱的期望赔付。 + * 样本不足的桶回退名义值(池子还没生成时也能算)。 + */ + public Map loadPoolExpect(Long boxId) { + Map expect = new HashMap<>(loadOrnamentMul()); + if (boxId == null) return expect; + try { + for (Map row : roundMapper.avgMulByOrnament(boxId)) { + Object idObj = row.get("ornamentId"); + Object nObj = row.get("sampleCount"); + Object avgObj = row.get("avgMul"); + if (idObj == null || avgObj == null) continue; + int id = ((Number) idObj).intValue(); + long n = nObj instanceof Number ? ((Number) nObj).longValue() : 0L; + double avg = ((Number) avgObj).doubleValue(); + if (!expect.containsKey(id)) continue; + if (n < MIN_POOL_SAMPLE || avg <= 0) continue; + expect.put(id, avg); + } + } catch (Exception ignore) { + // 池子还没生成/查询异常 → 用名义值兜底 + } + return expect; + } + + /** + * 按目标 RTP 动态计算各饰品桶回合数(幂律 power 二分搜索 + 单调 + 微调)。 + * 期望值用 {@link #loadPoolExpect}(**该箱**池内实际均值),所以算出来的配比实际赔付就等于 targetRtp。 + * @param mode 模式 @param boxId 箱子(决定用哪个池子的实际均值) @param totalRounds 固定池回合数 @param targetRtp 目标 RTP(如 0.8) + */ + public Map calcRounds(OlympusModeType mode, Long boxId, int totalRounds, double targetRtp) { + double cm = mode.getCostMultiplier(); + double targetAvg = targetRtp * cm; + Map ornMul = loadPoolExpect(boxId); + + // 基础权重:模式桶区间 [lo,hi] 内 30×0.6^k 等比递减 + int[] range = mode.getOrnamentRange(); + int lo = range[0], hi = range[1]; + int n = Math.max(1, hi - lo + 1); + double[][] baseWeights = new double[n][2]; + for (int i = 0; i < n; i++) { + baseWeights[i][0] = lo + i; + baseWeights[i][1] = 30.0 * Math.pow(0.6, i); + } + + int nOrns = baseWeights.length; + // 回合数不足:每桶至少 1,均摊 + if (totalRounds <= nOrns) { + Map flat = new LinkedHashMap<>(); + for (double[] w : baseWeights) flat.put((int) w[0], 1); + return flat; + } + int remainingRounds = totalRounds - nOrns; + + double guaranteedMulSum = 0; + for (double[] w : baseWeights) guaranteedMulSum += ornMul.getOrDefault((int) w[0], 0.0); + double targetTotalMul = targetAvg * totalRounds; + double remainingTargetMul = targetTotalMul - guaranteedMulSum; + double remainingTargetAvg = remainingTargetMul / remainingRounds; + + // 二分 power + double power = 1.0, plo = 0.2, phi = 10.0; + for (int iter = 0; iter < 30; iter++) { + double sumW = 0, sumWMul = 0; + for (double[] w : baseWeights) { + double weight = Math.pow(w[1], power); + sumW += weight; + sumWMul += weight * ornMul.getOrDefault((int) w[0], 0.0); + } + double avg = sumW > 0 ? sumWMul / sumW : 0; + if (Math.abs(avg - remainingTargetAvg) < Math.max(remainingTargetAvg * 0.01, 0.001)) break; + if (avg < remainingTargetAvg) phi = power; else plo = power; + power = (plo + phi) / 2; + } + + Map result = new LinkedHashMap<>(); + for (double[] w : baseWeights) result.put((int) w[0], 1); + double totalW = 0; + Map adjWeights = new LinkedHashMap<>(); + for (double[] w : baseWeights) { + double weight = Math.pow(w[1], power); + adjWeights.put((int) w[0], weight); + totalW += weight; + } + int rem = remainingRounds; + for (double[] w : baseWeights) { + int ornId = (int) w[0]; + int extra = totalW > 0 ? (int) Math.round(adjWeights.get(ornId) / totalW * remainingRounds) : 0; + int put = Math.min(extra, rem); + if (put > 0) { result.put(ornId, result.get(ornId) + put); rem -= put; } + } + if (rem > 0) { int first = (int) baseWeights[0][0]; result.put(first, result.get(first) + rem); } + + // 单调:count[i] >= count[i+1] + List sortedIds = new ArrayList<>(result.keySet()); + sortedIds.sort(Integer::compareTo); + for (int i = sortedIds.size() - 2; i >= 0; i--) { + int cur = sortedIds.get(i), next = sortedIds.get(i + 1); + if (result.get(cur) < result.get(next)) result.put(cur, result.get(next)); + } + int curTotal = result.values().stream().mapToInt(Integer::intValue).sum(); + int diff = totalRounds - curTotal; + if (diff != 0) { int first = sortedIds.get(0); result.put(first, result.get(first) + diff); } + + // 微调:任取两个桶搬 1 个(保持单调),只要更接近目标 + // (原来只在"最大的 3 个桶"之间搬,池内均值口径下根本走不到目标,会差 0.5~1 个百分点) + for (int iter = 0; iter < 400; iter++) { + double currentMul = 0; + for (Map.Entry e : result.entrySet()) + currentMul += e.getValue() * ornMul.getOrDefault(e.getKey(), 0.0); + double deltaMul = targetAvg * totalRounds - currentMul; + if (Math.abs(deltaMul) < 0.01) break; + int bestLo = -1, bestHi = -1; + double bestDiff = Math.abs(deltaMul); + for (int loId : sortedIds) { + if (result.get(loId) <= 1) continue; + for (int hiId : sortedIds) { + if (loId == hiId) continue; + double change = ornMul.getOrDefault(hiId, 0.0) - ornMul.getOrDefault(loId, 0.0); + if (Math.abs(change) < 0.001) continue; + double nd = Math.abs(deltaMul - change); + if (nd < bestDiff) { + boolean ok = true; + int ca = result.get(loId) - 1, idxLo = sortedIds.indexOf(loId); + if (idxLo < sortedIds.size() - 1 && ca < result.get(sortedIds.get(idxLo + 1))) ok = false; + int ca2 = result.get(hiId) + 1, idxHi = sortedIds.indexOf(hiId); + if (idxHi > 0 && ca2 > result.get(sortedIds.get(idxHi - 1))) ok = false; + if (sortedIds.indexOf(loId) < sortedIds.indexOf(hiId) && ca < ca2) ok = false; + if (ok) { bestDiff = nd; bestLo = loId; bestHi = hiId; } + } + } + } + if (bestLo < 0) break; + result.put(bestLo, result.get(bestLo) - 1); + result.put(bestHi, result.get(bestHi) + 1); + } + return result; + } + + /** 模拟 payoutCap 约束下的实际 RTP(%):桶的赔付期望按该箱池内实际均值算。 */ + public double simulateActualRtp(Map distribution, Long boxId, double rtp, int simRounds) { + if (distribution == null || distribution.isEmpty() || simRounds <= 0) return 0; + Map ornMul = loadPoolExpect(boxId); + double cm = 1.0; + if (boxId != null) { + OlympusBox box = boxMapper.selectById(boxId); + if (box != null) cm = OlympusModeType.fromBoxType(box.getBoxType()).getCostMultiplier(); + } + if (cm <= 0) return 0; + List mulList = new ArrayList<>(); + for (Map.Entry e : distribution.entrySet()) { + if (e.getValue() == null || e.getValue() <= 0) continue; + double mul = ornMul.getOrDefault(e.getKey(), 0.0); + for (int i = 0; i < e.getValue(); i++) mulList.add(mul); + } + if (mulList.isEmpty()) return 0; + Random rnd = new Random(); + double totalRtpSum = 0; + for (int round = 0; round < simRounds; round++) { + Collections.shuffle(mulList, rnd); + double balance = 0, totalPayout = 0, totalCost = 0; + for (double mul : mulList) { + double payoutCap = Math.max(0, balance + cm) * rtp; + double actualMul = (mul <= payoutCap) ? mul : 0; + balance += cm - actualMul; + totalPayout += actualMul; + totalCost += cm; + } + totalRtpSum += totalCost > 0 ? totalPayout / totalCost : 0; + } + return (totalRtpSum / simRounds) * 100; + } + + /** 由固定池回合数反推总回合 N = round(fixed/(1-cr))。 */ + public static int deriveTotalRounds(int fixedRounds, double convergeRatio) { + double cr = convergeRatio < 0 ? 0 : (convergeRatio > 0.9 ? 0.9 : convergeRatio); + return (int) Math.round(fixedRounds / (1.0 - cr)); + } + + /** 由总回合 N 算固定池回合数 fixed = round(N*(1-cr))。 */ + public static int deriveFixedRounds(int totalRounds, double convergeRatio) { + double cr = convergeRatio < 0 ? 0 : (convergeRatio > 0.9 ? 0.9 : convergeRatio); + return (int) Math.round(totalRounds * (1.0 - cr)); + } + + private static double convergeRatioOf(OlympusBox box) { + double cr = (box != null && box.getRtpConvergeRatio() != null) ? box.getRtpConvergeRatio().doubleValue() : 0.15; + if (cr < 0) cr = 0; + if (cr > 0.9) cr = 0.9; + return cr; + } + + /** 重算某箱某身份分布:按目标 RTP 分配固定池 count,used 归 0。 */ + @Transactional(rollbackFor = Exception.class) + public void reset(Long boxId, String audience) { + OlympusBox box = boxMapper.selectById(boxId); + if (box == null) return; + OlympusModeType mode = OlympusModeType.fromBoxType(box.getBoxType()); + int totalRounds = box.getTotalOrnaments() != null ? box.getTotalOrnaments() : 200; + double targetRtp = box.getRtp() != null ? box.getRtp().doubleValue() : 0.8; + int fixedRounds = deriveFixedRounds(totalRounds, convergeRatioOf(box)); + if (fixedRounds < 1) fixedRounds = totalRounds; + Map dist = calcRounds(mode, boxId, fixedRounds, targetRtp); + List list = new ArrayList<>(); + for (Map.Entry e : dist.entrySet()) { + if (e.getValue() == null || e.getValue() <= 0) continue; + OlympusBoxOrnament bo = new OlympusBoxOrnament(); + bo.setBoxId(boxId); + bo.setOrnamentId(e.getKey()); + bo.setAudience(audience); + bo.setCount(e.getValue()); + bo.setUsed(0); + list.add(bo); + } + boxOrnamentMapper.deleteByBoxIdAndAudience(boxId, audience); + if (!list.isEmpty()) boxOrnamentMapper.batchInsert(list); + } + + /** 应用模板分布到某箱某身份(count 取模板、used 归 0),并按固定池反推回写总回合。 */ + @Transactional(rollbackFor = Exception.class) + public void writeDistribution(Long boxId, String audience, List> items) { + boxOrnamentMapper.deleteByBoxIdAndAudience(boxId, audience); + int total = 0; + List list = new ArrayList<>(); + if (items != null) { + for (Map item : items) { + Integer ornamentId = toInt(item.get("ornamentId")); + Integer count = toInt(item.get("count")); + if (ornamentId == null || count == null || count <= 0) continue; + OlympusBoxOrnament bo = new OlympusBoxOrnament(); + bo.setBoxId(boxId); + bo.setOrnamentId(ornamentId); + bo.setAudience(audience); + bo.setCount(count); + bo.setUsed(0); + list.add(bo); + total += count; + } + } + if (!list.isEmpty()) boxOrnamentMapper.batchInsert(list); + OlympusBox box = boxMapper.selectById(boxId); + if (box != null) { + box.setTotalOrnaments(deriveTotalRounds(total, convergeRatioOf(box))); + boxMapper.updateById(box); + } + } + + /** 某箱某身份抽干补桶:优先按该身份生效模板重写;否则仅 used 归 0。 */ + @Transactional(rollbackFor = Exception.class) + public void refillFromConfig(Long boxId, String audience) { + OlympusBoxTemplate active = boxTemplateMapper.selectOne( + new LambdaQueryWrapper() + .eq(OlympusBoxTemplate::getBoxId, boxId) + .eq(OlympusBoxTemplate::getAudience, audience) + .eq(OlympusBoxTemplate::getIsActive, 1) + .orderByDesc(OlympusBoxTemplate::getId).last("LIMIT 1")); + if (active != null && active.getItemsJson() != null) { + List> items = parseItems(active.getItemsJson()); + if (!items.isEmpty()) { writeDistribution(boxId, audience, items); return; } + } + boxOrnamentMapper.resetUsed(boxId, audience); + } + + public List distribution(Long boxId, String audience) { + return boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); + } + + public List> parseItems(String itemsJson) { + try { + return OBJECT_MAPPER.readValue(itemsJson, new TypeReference>>() {}); + } catch (Exception e) { + return new ArrayList<>(); + } + } + + public String toItemsJson(Object items) { + try { return OBJECT_MAPPER.writeValueAsString(items); } catch (Exception e) { return "[]"; } + } + + private static Integer toInt(Object v) { + if (v == null) return null; + if (v instanceof Number) return ((Number) v).intValue(); + try { return Integer.valueOf(v.toString().trim()); } catch (Exception e) { return null; } + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusGameService.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusGameService.java new file mode 100644 index 0000000..3714425 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusGameService.java @@ -0,0 +1,526 @@ +package com.ruoyi.game.olympus.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.message.service.MessageService; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import com.ruoyi.game.olympus.contract.request.SpinRequest; +import com.ruoyi.game.olympus.contract.response.SpinResponse; +import com.ruoyi.game.olympus.domain.*; +import com.ruoyi.game.olympus.enums.OlympusAudience; +import com.ruoyi.game.olympus.enums.OlympusModeType; +import com.ruoyi.game.olympus.mapper.*; +import com.ruoyi.game.olympus.model.PickedRound; +import com.ruoyi.game.olympus.model.cache.OlympusGameCache; +import com.ruoyi.game.olympus.repository.OlympusCacheRepository; +import com.ruoyi.game.olympus.repository.OlympusCycleRepository; +import com.ruoyi.game.olympus.repository.OlympusRewardPoolCacheRepository; +import com.ruoyi.user.service.ApiUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionTemplate; + +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static com.ruoyi.admin.config.RedisConstants.COMMON_LOCK; + +/** + * Olympus 核心游戏服务:dospin 开局/播放/结算 + 多身份选池 + 按箱水位 + 白号隔离。 + */ +@Slf4j +@Service +public class OlympusGameService { + + @Autowired private OlympusBoxMapper boxMapper; + @Autowired private OlympusRoundPicker roundPicker; + @Autowired private OlympusBoxOrnamentMapper boxOrnamentMapper; + @Autowired private OlympusRecordMapper recordMapper; + @Autowired private OlympusUserMapper userMapper; + @Autowired private OlympusRoundMapper roundMapper; + @Autowired private OlympusPrizeRangeMapper prizeRangeMapper; + @Autowired private OlympusDistributionService distributionService; + @Autowired private OlympusCacheRepository cacheRepository; + @Autowired private OlympusCycleRepository cycleRepository; + @Autowired private OlympusRewardPoolCacheRepository rewardPoolRepository; + + @Autowired private TtUserService userService; + @Autowired private ApiUserService apiUserService; + @Autowired private RedisLock redisLock; + @Autowired private TransactionTemplate transactionTemplate; + + @Autowired(required = false) private TtBoxRecordsMapper boxRecordsMapper; + @Autowired(required = false) private TtOrnamentsLevelMapper ornamentsLevelMapper; + @Autowired private MessageService messageService; + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = + new com.fasterxml.jackson.databind.ObjectMapper(); + + /** 单局封顶:总倍率≥此值视为已 1000x 封顶 */ + private static final BigDecimal MAX_WIN_CAP_MUL = new BigDecimal("1000"); + + /** 免费旋转总旋数上限:15 起,每次重触发 +5,封顶 30 */ + private static final int MAX_FREE_SPINS = 30; + + // ============================================================ + // dospin 主入口 + // ============================================================ + + public SpinResponse doSpin(Long userId, SpinRequest req) throws Exception { + Boolean lock = redisLock.tryLock(COMMON_LOCK + userId, 3L, 7L, TimeUnit.SECONDS); + if (!Boolean.TRUE.equals(lock)) throw new RuntimeException("请求频繁,请稍后再试"); + try { + OlympusGameCache cache = cacheRepository.getCache(userId); + return cache == null ? startNewGame(userId, req != null ? req.getBoxId() : null) : continueGame(userId, cache); + } finally { + redisLock.unlock(COMMON_LOCK + userId); + } + } + + // ============================================================ + // 新游戏:扣费 + 多身份选池 + 缓存步骤 + 返回第一步 + // ============================================================ + + private SpinResponse startNewGame(Long userId, Long boxId) { + if (boxId == null) throw new IllegalArgumentException("boxId 不能为空"); + OlympusBox box = boxMapper.selectById(boxId); + if (box == null || !"1".equals(box.getStatus())) throw new IllegalArgumentException("箱子不可用"); + + OlympusModeType mode = OlympusModeType.fromBoxType(box.getBoxType()); + BigDecimal bet = coalesce(box.getBet()); + BigDecimal cost = bet.multiply(BigDecimal.valueOf(mode.getCostMultiplier())); + + R rechargeCheck = apiUserService.checkRecharge(userId.intValue()); + if (R.isError(rechargeCheck)) throw new ServiceException("玩家未充值不能游玩任何游戏", 703); + + TtUser player = userService.getById(userId); + if (player == null || player.getAccountAmount() == null || player.getAccountAmount().compareTo(cost) < 0) { + throw new ServiceException("账户余额不足", 702); + } + + // 身份解析:白号最高优先,压过 user_type + OlympusAudience audience = OlympusAudience.resolve(player); + boolean isBlank = audience.isWhite(); + + // 购买模式必须选有免费旋转的回合 + Boolean needFreeSpins = mode.isBuyMode() ? Boolean.TRUE : null; + + // 水位赔付上限:真实玩家按 (本箱水位 + 本局成本) × rtp 约束;白号 payoutCap=null(纯概率、不受水位) + BigDecimal payoutCap = null; + if (!isBlank) { + double ratio = box.getRtp() != null ? box.getRtp().doubleValue() : 0.8; + BigDecimal balance = rewardPoolRepository.getBoxBalance(boxId, audience.getCode()).add(cost); + if (balance.signum() < 0) balance = BigDecimal.ZERO; + payoutCap = scale2(balance.multiply(BigDecimal.valueOf(ratio))); + } + + PickedRound picked = roundPicker.pick(boxId, audience.getCode(), needFreeSpins, bet, payoutCap); + if (picked == null || picked.getRound() == null) throw new RuntimeException("该箱子暂无可用回合"); + + BigDecimal totalMul = coalesce(picked.getRound().getActualMul()); + BigDecimal totalWin = scale2(totalMul.multiply(bet)); + + List cachedSteps = new ArrayList<>(); + for (OlympusStep s : picked.getSteps()) { + OlympusGameCache.CachedStep cs = new OlympusGameCache.CachedStep(); + cs.setFreeSpinId(coalesce0(s.getFreeSpinId())); + cs.setCascadeSeq(coalesce0(s.getCascadeSeq())); + cs.setGridState(s.getGridState()); + cs.setMultiplierDetail(s.getMultiplierDetail()); + cs.setSymbolLinks(s.getSymbolLinks()); + cs.setWinAmount(s.getWinAmount()); + cs.setSpinWin(coalesce(s.getSpinWin())); + cs.setAccumulatorAfter(coalesce(s.getAccumulatorAfter())); + cs.setWinCapped(coalesce0(s.getWinCapped())); + cs.setBigwinLevel(s.getBigwinLevel()); + cs.setIsFinal(coalesce0(s.getIsFinal())); + cs.setRetriggerInfo(s.getRetriggerInfo()); + cs.setDropData(s.getDropData()); + cs.setFillData(s.getFillData()); + cs.setScatterTrigger(s.getScatterTrigger()); + cachedSteps.add(cs); + } + + OlympusGameCache cache = new OlympusGameCache(); + cache.setBoxId(boxId); + cache.setBoxType(box.getBoxType()); + cache.setBet(bet); + cache.setCost(cost); + cache.setRoundId(picked.getRound().getRoundId()); + cache.setTotalMul(totalMul); + cache.setFreeSpinCount(coalesce0(picked.getRound().getFreeSpinCount())); + cache.setStepIndex(1); + cache.setSteps(cachedSteps); + cache.setTotalWin(totalWin); + cache.setAudience(audience.getCode()); + cache.setBlankAccount(isBlank); + + boolean firstIsFinal = cachedSteps.size() == 1; + + transactionTemplate.executeWithoutResult(status -> { + R deductR = userService.updateUserAccount(userId.intValue(), cost.negate(), TtAccountRecordSource.OLYMPUS_SPIN); + if (R.isError(deductR)) throw new RuntimeException(deductR.getMsg()); + + OlympusRecord record = new OlympusRecord(); + record.setUserId(userId); + record.setBoxId(boxId); + record.setRoundId(picked.getRound().getRoundId()); + record.setCost(cost); + record.setTotalWin(BigDecimal.ZERO); + record.setModeType(box.getBoxType()); + record.setSettled(0); + record.setCreateTime(new Date()); + recordMapper.insert(record); + cache.setRecordId(record.getId()); + + // 仅固定阶段消耗对应 audience 一个回合(白号消耗自己的 white 库存,不碰其他身份) + if (!picked.isConverge()) { + boxOrnamentMapper.incrUsed(boxId, picked.getRound().getOrnamentId(), audience.getCode()); + } + // 开局即结算:回合结果已确定,立即将饰品写入背包(与扣费同事务,中途退出/刷新无损) + settleAtStart(userId, cache); + }); + + // 事务已提交(钱已扣、奖已落库)。以下全是 Redis 缓存/节奏账本/补桶,非资金操作: + // 一旦失败必须吞掉——否则接口返回 500,玩家以为没开局而重试,会再开一局、再扣一次钱。 + try { + cycleRepository.recordDraw(boxId, audience.getCode(), picked.getRound().getOrnamentId(), + coalesce(picked.getRound().getActualMul()), picked.isConverge()); + // 一轮 N 按该身份固定池分布反推(与 RoundPicker 一致,身份级独立) + int fixedSum = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience.getCode()).stream() + .mapToInt(bo -> bo.getCount() != null ? bo.getCount() : 0).sum(); + double cr = box.getRtpConvergeRatio() != null ? box.getRtpConvergeRatio().doubleValue() : 0.15; + int cycleN = fixedSum > 0 ? OlympusDistributionService.deriveTotalRounds(fixedSum, cr) : coalesce0(box.getTotalOrnaments()); + if (cycleN > 0 && cycleRepository.drawn(boxId, audience.getCode()) >= cycleN) { + cycleRepository.resetCycle(boxId, audience.getCode()); + distributionService.refillFromConfig(boxId, audience.getCode()); + } + + if (firstIsFinal) { + if (cache.getTotalWin().compareTo(BigDecimal.ZERO) <= 0) cache.setTotalWin(new BigDecimal("0.01")); + cacheRepository.clearCache(userId); + } else { + cacheRepository.setCache(userId, cache); + } + } catch (Exception e) { + log.error("olympus 开局后置处理失败(扣费与发奖已提交,不影响本局结果)userId={} boxId={}", userId, boxId, e); + } + // 单步局开局即播完,通知此刻发;多步局等末帧(continueGame) + if (firstIsFinal) sendWinNotice(userId, cache); + return buildStepResponse(cache, cachedSteps.get(0), 1, firstIsFinal); + } + + // ============================================================ + // 继续游戏:取下一步 + // ============================================================ + + private SpinResponse continueGame(Long userId, OlympusGameCache cache) { + int nextIdx = cache.getStepIndex() + 1; + if (nextIdx > cache.getSteps().size()) { + cacheRepository.clearCache(userId); + throw new RuntimeException("游戏已结束"); + } + OlympusGameCache.CachedStep step = cache.getSteps().get(nextIdx - 1); + cache.setStepIndex(nextIdx); + + if (nextIdx == cache.getSteps().size()) { + if (cache.getTotalWin().compareTo(BigDecimal.ZERO) <= 0) cache.setTotalWin(new BigDecimal("0.01")); + // 末帧已播完:这一局才算结束,战绩列表(我的/全服)才会展示 + recordMapper.update(null, new LambdaUpdateWrapper() + .eq(OlympusRecord::getId, cache.getRecordId()) + .set(OlympusRecord::getSettleSource, "finish")); + // 末帧到了才发结算通知(开局发会在动画播完前剧透),失败只记日志不影响本局 + sendWinNotice(userId, cache); + // 已在开局结算入背包,末帧仅揭晓;回填 boxRecordId 供前端选择保留/立即分解 + SpinResponse resp = buildStepResponse(cache, step, nextIdx, true); + cacheRepository.clearCache(userId); + return resp; + } + cacheRepository.setCache(userId, cache); + return buildStepResponse(cache, step, nextIdx, false); + } + + /** 结算通知(站内信):只在"这一局播完"的时刻发——单步局=开局,多步局=末帧。 + * 开局就发会在动画播完前把结果剧透给玩家(饰品开局已入背包,通知是唯一会提前泄露的通道)。 + * 多步局中途退出/断线未播完的局不发通知,饰品仍在背包,战绩按 30 分钟兜底照常展示。 */ + private void sendWinNotice(Long userId, OlympusGameCache cache) { + if (cache.getRecordId() == null) return; + try { + OlympusRecord rec = recordMapper.selectById(cache.getRecordId()); + if (rec == null || rec.getPrizeName() == null) return; + String priceStr = rec.getSettledWin() == null ? "0" : rec.getSettledWin().stripTrailingZeros().toPlainString(); + messageService.sendNotice( + userId, + MessageCategoryEnum.GAME_OLYMPUS.getTitle(), + "玫瑰女郎挑战结算:您本局获得饰品 " + rec.getPrizeName() + "(价值 ¥" + priceStr + ")", + MessageCategoryEnum.GAME_OLYMPUS.getCode()); + } catch (Exception e) { + log.error("olympus 结算通知发送失败 userId={} recordId={}", userId, cache.getRecordId(), e); + } + } + + // ============================================================ + // 开局即结算(事务中,饰品直接写入背包,中途退出/刷新无损) + // ============================================================ + + private void settleAtStart(Long userId, OlympusGameCache cache) { + BigDecimal totalWin = cache.getTotalWin(); + if (totalWin == null || totalWin.compareTo(BigDecimal.ZERO) <= 0) totalWin = new BigDecimal("0.01"); + BigDecimal win = scale2(totalWin); + + Map prize = findPrizeWithFallback(win); + Long prizeId = asLong(prize != null ? prize.get("id") : null); + String prizeName = prize != null ? (String) prize.get("name") : null; + Integer prizeGrade = prize != null && prize.get("grade") instanceof Integer ? (Integer) prize.get("grade") : 1; + String prizeImageUrl = prize != null ? (String) prize.get("imageUrl") : null; + String prizeExteriorName = prize != null ? (String) prize.get("exteriorName") : null; + String prizeLevelImg = prize != null ? (String) prize.get("levelImageUrl") : null; + + // 幂等守卫。settle_source:开局写 start,客户端播完末帧才改 finish(战绩列表只展示已结束的局); + // 单步局开局即播完,直接写 finish。 + boolean playedOut = cache.getSteps() == null || cache.getSteps().size() <= 1; + int guard = recordMapper.update(null, new LambdaUpdateWrapper() + .eq(OlympusRecord::getId, cache.getRecordId()) + .eq(OlympusRecord::getSettled, 0) + .set(OlympusRecord::getSettled, 1) + .set(OlympusRecord::getRewardType, "ornament") + .set(OlympusRecord::getTotalWin, totalWin) + .set(OlympusRecord::getSettledWin, win) + .set(OlympusRecord::getPrizeId, prizeId) + .set(OlympusRecord::getSettleSource, playedOut ? "finish" : "start") + .set(OlympusRecord::getPrizeName, prizeName) + .set(OlympusRecord::getPrizeImageUrl, prizeImageUrl)); + if (guard == 0) { cacheRepository.clearCache(userId); return; } + + // 写入背包 + Long boxRecordId = null; + if (boxRecordsMapper != null && prizeId != null) { + if (prizeLevelImg == null && ornamentsLevelMapper != null) { + TtOrnamentsLevel lv = ornamentsLevelMapper.selectById(prizeGrade); + if (lv != null) prizeLevelImg = lv.getLevelImg(); + } + TtBoxRecords bi = TtBoxRecords.builder() + .userId(userId.intValue()).holderUserId(userId.intValue()) + .boxId(cache.getBoxId() != null ? cache.getBoxId().intValue() : 0) + .boxName("OLYMPUS").boxPrice(cache.getCost()) + .ornamentId(prizeId).ornamentsPrice(win) + .ornamentName(prizeName).imageUrl(prizeImageUrl) + .marketHashName(prizeExteriorName != null ? prizeName + " (" + prizeExteriorName + ")" : prizeName) + .ornamentsLevelId(prizeGrade != null ? prizeGrade : 1) + .ornamentLevelImg(prizeLevelImg) + .source(TtboxRecordSource.OLYMPUS_BOX.getCode()) + .status(0).isShow(1).createTime(new Date()) + .build(); + boxRecordsMapper.insert(bi); + boxRecordId = bi.getId(); + cache.setBoxRecordId(bi.getId()); + // 注:结算通知不在这里发——开局就发会在动画播完前把结果剧透给玩家,改由 startNewGame/continueGame 在末帧发 + } + + // 注:饰品一律以 ornament 写入背包(开局即结算,断网也不丢)。自动分解由前端开关控制: + // 末帧前端调 /api/userPackSack/decompose 即按玫瑰女郎奖励(61)入账;调不动也只是留在背包,无资金风险。 + + // 更新用户统计 + OlympusUser user = userMapper.selectByUserId(userId); + if (user == null) { + user = new OlympusUser(); + user.setUserId(userId); + user.setTotalSpins(1L); + user.setTotalCost(cache.getCost()); + user.setTotalWin(totalWin); + user.setTotalFreeSpins(cache.getFreeSpinCount() != null && cache.getFreeSpinCount() > 0 ? 1 : 0); + user.setMaxWinMul(cache.getTotalMul()); + userMapper.insert(user); + } else { + user.setTotalSpins(coalesceL(user.getTotalSpins()) + 1L); + user.setTotalCost(coalesce(user.getTotalCost()).add(cache.getCost())); + user.setTotalWin(coalesce(user.getTotalWin()).add(totalWin)); + if (cache.getFreeSpinCount() != null && cache.getFreeSpinCount() > 0) + user.setTotalFreeSpins(coalesce0(user.getTotalFreeSpins()) + 1); + if (cache.getTotalMul() != null && cache.getTotalMul().compareTo(coalesce(user.getMaxWinMul())) > 0) + user.setMaxWinMul(cache.getTotalMul()); + userMapper.updateById(user); + } + + // 注:奖池注水与返奖记入由 startNewGame 事务提交后统一执行(开局即结算,白号隔离) + } + + // ============================================================ + // 断线重连 + // ============================================================ + + public Map hasActive(Long userId) { + OlympusGameCache cache = cacheRepository.getCache(userId); + // 开局即结算模型:无缓存即无进行中动画可恢复(饰品已在背包),直接返回 + if (cache == null) return Map.of("hasActive", false); + List steps = cache.getSteps(); + int played = Math.min(Math.max(cache.getStepIndex(), 0), steps.size()); + BigDecimal bet = coalesce(cache.getBet()); + // 已播累计直接取最后一步的 spin_win(本局累计),不能逐帧加 win_amount: + // win_amount 是"本旋内累计",同一旋的每一波都是同一个累计量的快照,相加会把中间波重复计入。 + BigDecimal playedWin = played > 0 + ? scale2(coalesce(steps.get(played - 1).getSpinWin()).multiply(bet)) + : BigDecimal.ZERO; + Map result = new HashMap<>(); + result.put("hasActive", true); + result.put("boxId", cache.getBoxId()); + result.put("boxType", cache.getBoxType()); + result.put("bet", cache.getBet()); + result.put("cost", cache.getCost()); + result.put("stepIndex", cache.getStepIndex()); + result.put("totalSteps", steps.size()); + result.put("freeSpinCount", cache.getFreeSpinCount()); + result.put("spinsRemaining", spinsRemainingAfter(steps, played)); + result.put("totalWin", scale2(playedWin)); + result.put("roundId", cache.getRoundId()); + if (played > 0) result.put("lastStep", buildStepResponse(cache, steps.get(played - 1), played, false)); + return result; + } + + // ============================================================ + // 构建单步响应 + // ============================================================ + + private SpinResponse buildStepResponse(OlympusGameCache cache, OlympusGameCache.CachedStep cs, int idx, boolean isFinal) { + BigDecimal bet = coalesce(cache.getBet()); + SpinResponse resp = new SpinResponse(); + resp.setState(isFinal ? "FINISH" : "PLAYING"); + resp.setTumbleNum(cs.getCascadeSeq()); + resp.setSpinNum(cs.getFreeSpinId()); + resp.setGridState(toJson(cs.getGridState())); + resp.setMultiplierDetail(toJson(cs.getMultiplierDetail())); + resp.setSymbolLinks(scaleMoneyList(cs.getSymbolLinks(), bet, new String[]{"base_score", "score", "win"})); + resp.setDropData(toJson(cs.getDropData())); + resp.setFillData(toJson(cs.getFillData())); + resp.setScatterTrigger(scaleMoneyMap(cs.getScatterTrigger(), bet, new String[]{"scatterWin", "startAmount"})); + resp.setWinAmount(scale2(coalesce(cs.getWinAmount()).multiply(bet))); + resp.setSpinWin(scale2(coalesce(cs.getSpinWin()).multiply(bet))); + // 炸弹累计倍数(未 ×bet):跨旋传递的累计器,断线重连只回最后一步,前端靠这一步的值接回累计 + resp.setAccumulator(scale2(coalesce(cs.getAccumulatorAfter()))); + if (cs.getRetriggerInfo() != null && !cs.getRetriggerInfo().isEmpty()) + resp.setRetriggerInfo(scaleMoneyMap(cs.getRetriggerInfo(), bet, new String[]{"scatterWin"})); + // 爆奖动画:py 已按单旋赢额判档写入 bigwin_level,直接透传 + if (cs.getBigwinLevel() != null && !cs.getBigwinLevel().isEmpty()) resp.setBigWinLevel(cs.getBigwinLevel()); + + resp.setGlobalStep(idx); + resp.setGlobalTotalSteps(cache.getSteps().size()); + // 剩余免费旋转:每帧都给(前端直接显示倒计,不用自己加减);1000x 封顶时下面覆盖成 0 + resp.setSpinsRemaining(spinsRemainingAfter(cache.getSteps(), idx)); + + if (isFinal) { + resp.setTotalWin(scale2(cache.getTotalWin())); + resp.setTotalMul(scale2(cache.getTotalMul())); + if (coalesce(cache.getTotalMul()).compareTo(MAX_WIN_CAP_MUL) >= 0) { + resp.setMaxWinReached(true); + resp.setSpinsRemaining(0); + } + // 开局已结算入背包:末帧揭晓饰品 + 回填 boxRecordId 供前端选择保留/立即分解 + try { + BigDecimal win = scale2(cache.getTotalWin()); + if (win.compareTo(BigDecimal.ZERO) <= 0) win = new BigDecimal("0.01"); + Map prize = findPrizeWithFallback(win); + if (prize != null && !prize.isEmpty()) resp.setPrizeInfo(prize); + } catch (Exception ignore) {} + resp.setBoxRecordId(cache.getBoxRecordId()); + } + return resp; + } + + // ============================================================ + // 工具 + // ============================================================ + + private Map findPrizeWithFallback(BigDecimal totalWin) { + Map prize = prizeRangeMapper.findPrizeByWin(totalWin); + if (prize != null && !prize.isEmpty()) { prize.put("isFallback", false); return prize; } + prize = prizeRangeMapper.findFallbackPrize(); + if (prize == null || prize.isEmpty()) prize = prizeRangeMapper.findLowestPrize(); + if (prize == null || prize.isEmpty()) prize = prizeRangeMapper.findHighestPrize(); + if (prize != null) prize.put("isFallback", true); + return prize; + } + + private static BigDecimal scale2(BigDecimal v) { return v == null ? BigDecimal.ZERO : v.setScale(2, java.math.RoundingMode.HALF_UP); } + private static BigDecimal coalesce(BigDecimal v) { return v != null ? v : BigDecimal.ZERO; } + private static int coalesce0(Integer v) { return v != null ? v : 0; } + private static long coalesceL(Long v) { return v != null ? v : 0L; } + + private static Long asLong(Object v) { + if (v == null) return null; + if (v instanceof Long) return (Long) v; + if (v instanceof Number) return ((Number) v).longValue(); + try { return Long.valueOf(v.toString()); } catch (Exception e) { return null; } + } + + private static Object toJson(String s) { + if (s == null || s.isEmpty()) return null; + try { return MAPPER.readTree(s); } catch (Exception e) { return s; } + } + + /** 剩余免费旋转 = 已发放 − 已播完的 FS 旋数(played = 算到第几步,含这一步;中间波帧不减,旋末帧才 −1)。 + * 已发放只算玩家**已经看到**的:触发帧的 freeSpins(=15) + 已播步里的 retriggerInfo.extraSpins(=5),封顶 30 + * (封顶后 3+ 个 S 的旋仍会写 retrigger_info=5 但不再加旋,所以这里必须夹 30)。 + * 例:第 2 旋末帧出 3 个 S → 15+5−2 = 18(那一帧 −1 与 +5 同时结算,净 +4)。*/ + private static int spinsRemainingAfter(List steps, int played) { + int awarded = 0, completed = 0; + for (int i = 0; i < played && i < steps.size(); i++) { + OlympusGameCache.CachedStep s = steps.get(i); + awarded += jsonInt(s.getScatterTrigger(), "freeSpins") + jsonInt(s.getRetriggerInfo(), "extraSpins"); + if (s.getIsFinal() == 1 && s.getFreeSpinId() > 0) completed++; + } + return Math.max(0, Math.min(awarded, MAX_FREE_SPINS) - completed); + } + + /** 取 JSON 对象里的整数字段(缺失 / null / 解析失败一律按 0) */ + private static int jsonInt(String json, String key) { + if (json == null || json.isEmpty()) return 0; + try { + com.fasterxml.jackson.databind.JsonNode node = MAPPER.readTree(json).get(key); + return node == null ? 0 : node.asInt(); + } catch (Exception e) { return 0; } + } + + /** 解析 JSON 数组,把指定金额字段 ×bet */ + @SuppressWarnings("unchecked") + private static Object scaleMoneyList(String json, BigDecimal bet, String[] keys) { + if (json == null || json.isEmpty()) return null; + try { + List> list = MAPPER.readValue(json, List.class); + for (Map m : list) scaleKeys(m, bet, keys); + return list; + } catch (Exception e) { return toJson(json); } + } + + @SuppressWarnings("unchecked") + private static Object scaleMoneyMap(String json, BigDecimal bet, String[] keys) { + if (json == null || json.isEmpty()) return null; + try { + Map m = MAPPER.readValue(json, Map.class); + scaleKeys(m, bet, keys); + return m; + } catch (Exception e) { return toJson(json); } + } + + private static void scaleKeys(Map m, BigDecimal bet, String[] keys) { + for (String key : keys) { + Object v = m.get(key); + if (v instanceof Number) { + m.put(key, BigDecimal.valueOf(((Number) v).doubleValue()) + .multiply(coalesce(bet)).setScale(2, java.math.RoundingMode.HALF_UP)); + } + } + } +} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusRoundPicker.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusRoundPicker.java new file mode 100644 index 0000000..82679f2 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusRoundPicker.java @@ -0,0 +1,279 @@ +package com.ruoyi.game.olympus.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.game.olympus.domain.*; +import com.ruoyi.game.olympus.enums.OlympusModeType; +import com.ruoyi.game.olympus.mapper.*; +import com.ruoyi.game.olympus.model.PickedRound; +import com.ruoyi.game.olympus.repository.OlympusCycleRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Olympus 回合选择器(多身份 + 两阶段 + 白号隔离)。 + * + * 每个身份(audience)读自己的桶分布与轮次账本: + * - 白号(payoutCap==null):纯概率原逻辑,不进两阶段、不受水位约束,但消耗自己的 white 库存(isConverge=false); + * - 真实身份(payoutCap!=null):固定阶段(payoutCap 过滤+剩余加权) → 收敛阶段(按 desiredNextMul 在水位内选桶)。 + * + * 消耗(used+1,仅固定阶段)与账本 recordDraw 由 OlympusGameService 在扣费事务内/提交后执行。 + */ +@Service +public class OlympusRoundPicker { + + @Autowired + private OlympusBoxOrnamentMapper boxOrnamentMapper; + @Autowired + private OlympusRoundMapper roundMapper; + @Autowired + private OlympusStepMapper stepMapper; + @Autowired + private OlympusDistributionService distributionService; + @Autowired + private OlympusBoxMapper boxMapper; + @Autowired + private OlympusCycleRepository cycleRepository; + + private static final double DEFAULT_CONVERGE_RATIO = 0.15; + + public PickedRound pick(Long boxId, String audience, Boolean needFreeSpins, BigDecimal bet, BigDecimal payoutCap) { + List dist = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); + if (dist.isEmpty()) throw new RuntimeException("Box " + boxId + " audience=" + audience + " 暂无饰品分布数据"); + + // 白号 / 无水位约束:纯概率原逻辑(消耗自己的库存,不受水位) + if (payoutCap == null) { + int totalRemaining = dist.stream().mapToInt(bo -> safe(bo.getRemaining())).sum(); + if (totalRemaining <= 0) { + distributionService.refillFromConfig(boxId, audience); + dist = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); + } + Integer ornId = weightedPickByRemaining(dist); + if (ornId == null) ornId = dist.get(0).getOrnamentId(); + OlympusRound r = resolveRound(boxId, ornId, needFreeSpins, bet, null); + return new PickedRound(r, stepMapper.selectByRoundId(r.getRoundId()), false); + } + + // 真实玩家:两阶段 + OlympusBox box = boxMapper.selectById(boxId); + double cm = 1.0, rtp = 0.8, convergeRatio = DEFAULT_CONVERGE_RATIO; + if (box != null) { + try { cm = OlympusModeType.fromBoxType(box.getBoxType()).getCostMultiplier(); } catch (Exception ignore) {} + if (box.getRtp() != null) rtp = box.getRtp().doubleValue(); + if (box.getRtpConvergeRatio() != null) convergeRatio = box.getRtpConvergeRatio().doubleValue(); + } + if (convergeRatio < 0) convergeRatio = 0; + if (convergeRatio > 0.9) convergeRatio = 0.9; + // 一轮总回合 N 按该身份固定池分布反推(身份级独立,各身份可不同),不使用箱子级 total_ornaments; + // 无分布时回退箱子级值兜底 + int fixedSum = dist.stream().mapToInt(bo -> safe(bo.getCount())).sum(); + int N; + if (fixedSum > 0) { + N = (int) Math.round(fixedSum / (1.0 - convergeRatio)); + } else { + N = (box != null && box.getTotalOrnaments() != null && box.getTotalOrnaments() > 0) + ? box.getTotalOrnaments() : fixedSum; + } + int fixedRounds = (int) Math.round(N * (1 - convergeRatio)); + long drawn = cycleRepository.drawn(boxId, audience); + int totalRemaining = dist.stream().mapToInt(bo -> safe(bo.getRemaining())).sum(); + + // 固定阶段 + if (drawn < fixedRounds && totalRemaining > 0) { + Integer ornId = pickFixedOrnId(dist, bet, payoutCap); + if (ornId == null) ornId = dist.get(0).getOrnamentId(); + OlympusRound r = resolveRound(boxId, ornId, needFreeSpins, bet, payoutCap); + return new PickedRound(r, stepMapper.selectByRoundId(r.getRoundId()), false); + } + + // 收敛阶段 + double targetMul = rtp * cm; + long remainingDraws = Math.max(1, N - drawn); + double remainingBudget = N * targetMul - cycleRepository.mulSum(boxId, audience); + double desiredNextMul = Math.max(0, remainingBudget / remainingDraws); + OlympusRound r = pickConverge(boxId, dist, needFreeSpins, bet, payoutCap, desiredNextMul); + return new PickedRound(r, stepMapper.selectByRoundId(r.getRoundId()), true); + } + + private Integer pickFixedOrnId(List dist, BigDecimal bet, BigDecimal payoutCap) { + List eligible = new ArrayList<>(); + for (OlympusBoxOrnament bo : dist) { + if (Math.max(0, safe(bo.getRemaining())) <= 0) continue; + if (expectPayout(bo, bet).compareTo(payoutCap) <= 0) eligible.add(bo); + } + return !eligible.isEmpty() ? weightedPickByRemaining(eligible) : minPayoutOrnId(dist, bet); + } + + private OlympusRound resolveRound(Long boxId, Integer ornId, Boolean needFreeSpins, BigDecimal bet, BigDecimal payoutCap) { + OlympusRound round = pickRoundWithCap(boxId, ornId, needFreeSpins, bet, payoutCap); + if (round == null) round = copyFromSameBucket(boxId, ornId, needFreeSpins); + if (round == null && needFreeSpins != null) { + round = roundMapper.pickByOrnament(boxId, ornId, null); + if (round == null) round = copyFromSameBucket(boxId, ornId, null); + } + if (round == null) throw new RuntimeException("暂无可用回合数据"); + return round; + } + + /** 桶内随机取一条,真实玩家还要 actual_mul×bet ≤ payoutCap(最多重抽 5 次,抽不到就退回原条) */ + private OlympusRound pickRoundWithCap(Long boxId, Integer ornId, Boolean needFreeSpins, BigDecimal bet, BigDecimal payoutCap) { + OlympusRound round = roundMapper.pickByOrnament(boxId, ornId, needFreeSpins); + if (round == null || payoutCap == null) return round; + BigDecimal b = bet != null ? bet : BigDecimal.ZERO; + if (withinCap(round, b, payoutCap)) return round; + for (int i = 0; i < 5; i++) { + OlympusRound r = roundMapper.pickByOrnament(boxId, ornId, needFreeSpins); + if (r != null && withinCap(r, b, payoutCap)) return r; + } + return round; + } + + private OlympusRound pickConverge(Long boxId, List dist, Boolean needFreeSpins, + BigDecimal bet, BigDecimal payoutCap, double desiredNextMul) { + BigDecimal b = (bet != null && bet.signum() > 0) ? bet : BigDecimal.ONE; + List allowed = new ArrayList<>(); + for (OlympusBoxOrnament bo : dist) { + if (expectPayout(bo, b).compareTo(payoutCap) <= 0) allowed.add(bo); + } + OlympusBoxOrnament chosen = allowed.isEmpty() ? minExpectMul(dist) : closestExpectMul(allowed, desiredNextMul); + Integer ornId = chosen.getOrnamentId(); + + double capMul = payoutCap.doubleValue() / b.doubleValue(); + double effectiveDesired = Math.min(desiredNextMul, capMul); + BigDecimal desiredBd = BigDecimal.valueOf(effectiveDesired).setScale(4, RoundingMode.HALF_UP); + BigDecimal capBd = BigDecimal.valueOf(capMul).setScale(4, RoundingMode.HALF_UP); + + OlympusRound round = roundMapper.pickClosestMul(boxId, ornId, needFreeSpins, desiredBd, capBd); + if (round == null && needFreeSpins != null) round = roundMapper.pickClosestMul(boxId, ornId, null, desiredBd, capBd); + if (round == null) round = copyFromSameBucket(boxId, ornId, needFreeSpins); + if (round == null) round = copyFromSameBucket(boxId, ornId, null); + if (round == null) throw new RuntimeException("收敛阶段暂无可用回合数据"); + return round; + } + + private OlympusBoxOrnament closestExpectMul(List list, double desired) { + List sorted = new ArrayList<>(list); + sorted.sort(Comparator.comparingDouble(bo -> { + double mul = bo.getExpectMul() != null ? bo.getExpectMul().doubleValue() : 0; + return Math.abs(mul - desired); + })); + int pickCount = Math.min(3, sorted.size()); + return sorted.get(ThreadLocalRandom.current().nextInt(pickCount)); + } + + private OlympusBoxOrnament minExpectMul(List list) { + OlympusBoxOrnament best = list.get(0); + double bestMul = Double.MAX_VALUE; + for (OlympusBoxOrnament bo : list) { + double mul = bo.getExpectMul() != null ? bo.getExpectMul().doubleValue() : 0; + if (mul < bestMul) { bestMul = mul; best = bo; } + } + return best; + } + + /** 同桶复制已有回合(新ID,动画数据原样拷贝),保证一定有回合可用。 */ + private OlympusRound copyFromSameBucket(Long boxId, Integer ornamentId, Boolean needFreeSpins) { + OlympusRound template = null; + for (int i = 0; i < 10; i++) { + template = roundMapper.pickByOrnament(boxId, ornamentId, needFreeSpins); + if (template != null && nz(template.getActualMul()).signum() > 0) break; + } + if (template == null) { + for (int i = 0; i < 10; i++) { + template = roundMapper.pickAnyByBox(boxId, needFreeSpins); + if (template != null && nz(template.getActualMul()).signum() > 0) break; + } + if (template == null) return null; + } + List templateSteps = stepMapper.selectByRoundId(template.getRoundId()); + Long newRoundId = nextRoundId(); + long newStepId = nextStepId(); + for (OlympusStep ts : templateSteps) { + OlympusStep ns = new OlympusStep(); + ns.setId(newStepId++); + ns.setRoundId(newRoundId); + ns.setFreeSpinId(ts.getFreeSpinId()); + ns.setCascadeSeq(ts.getCascadeSeq()); + ns.setGridState(ts.getGridState()); + ns.setMultiplierDetail(ts.getMultiplierDetail()); + ns.setSymbolLinks(ts.getSymbolLinks()); + ns.setAccumulatedSymbols(ts.getAccumulatedSymbols()); + ns.setSpinWinners(ts.getSpinWinners()); + ns.setRemovedPositions(ts.getRemovedPositions()); + ns.setDropData(ts.getDropData()); + ns.setFillData(ts.getFillData()); + ns.setScatterTrigger(ts.getScatterTrigger()); + ns.setRetriggerInfo(ts.getRetriggerInfo()); + ns.setBaseScore(ts.getBaseScore()); + ns.setWinAmount(ts.getWinAmount()); + ns.setAccumulatorBefore(ts.getAccumulatorBefore()); + ns.setAccumulatorAfter(ts.getAccumulatorAfter()); + ns.setSpinWin(ts.getSpinWin()); + ns.setWinCapped(ts.getWinCapped()); + ns.setBigwinLevel(ts.getBigwinLevel()); + ns.setIsFinal(ts.getIsFinal()); + stepMapper.insert(ns); + } + OlympusRound copy = new OlympusRound(); + copy.setRoundId(newRoundId); + copy.setBoxId(boxId); + copy.setOrnamentId(ornamentId); + copy.setActualMul(template.getActualMul()); + copy.setFreeSpinCount(template.getFreeSpinCount()); + copy.setTotalCascades(template.getTotalCascades()); + copy.setCreateTime(new Date()); + roundMapper.insert(copy); + return copy; + } + + private Long nextRoundId() { + List max = roundMapper.selectList( + new LambdaQueryWrapper().orderByDesc(OlympusRound::getRoundId).last("limit 1")); + return max.isEmpty() ? 1L : max.get(0).getRoundId() + 1; + } + + private Long nextStepId() { + List max = stepMapper.selectList( + new LambdaQueryWrapper().orderByDesc(OlympusStep::getId).last("limit 1")); + return max.isEmpty() ? 1L : max.get(0).getId() + 1; + } + + private BigDecimal expectPayout(OlympusBoxOrnament bo, BigDecimal bet) { + BigDecimal mul = bo.getExpectMul() != null ? bo.getExpectMul() : BigDecimal.ZERO; + return mul.multiply(bet != null ? bet : BigDecimal.ZERO); + } + + private Integer weightedPickByRemaining(List list) { + if (list.isEmpty()) return null; + int totalWeight = list.stream().mapToInt(bo -> Math.max(0, safe(bo.getRemaining()))).sum(); + if (totalWeight <= 0) return list.get(0).getOrnamentId(); + int r = ThreadLocalRandom.current().nextInt(totalWeight); + int acc = 0; + for (OlympusBoxOrnament bo : list) { + acc += Math.max(0, safe(bo.getRemaining())); + if (r < acc) return bo.getOrnamentId(); + } + return list.get(0).getOrnamentId(); + } + + private Integer minPayoutOrnId(List dist, BigDecimal bet) { + OlympusBoxOrnament best = null; + for (OlympusBoxOrnament bo : dist) { + if (Math.max(0, safe(bo.getRemaining())) <= 0) continue; + if (best == null || expectPayout(bo, bet).compareTo(expectPayout(best, bet)) < 0) best = bo; + } + return best != null ? best.getOrnamentId() : dist.get(0).getOrnamentId(); + } + + private boolean withinCap(OlympusRound round, BigDecimal bet, BigDecimal payoutCap) { + BigDecimal mul = nz(round.getActualMul()); + return mul.signum() > 0 && mul.multiply(bet).compareTo(payoutCap) <= 0; + } + + private static BigDecimal nz(BigDecimal v) { return v != null ? v : BigDecimal.ZERO; } + private static int safe(Integer v) { return v != null ? v : 0; } +} diff --git a/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusBoxOrnamentMapper.xml b/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusBoxOrnamentMapper.xml new file mode 100644 index 0000000..83dee2a --- /dev/null +++ b/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusBoxOrnamentMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + + INSERT INTO game_olympus_box_ornament (box_id, ornament_id, audience, count, used) VALUES + + (#{it.boxId}, #{it.ornamentId}, #{it.audience}, #{it.count}, #{it.used}) + + + + + DELETE FROM game_olympus_box_ornament WHERE box_id = #{boxId} AND audience = #{audience} + + + + UPDATE game_olympus_box_ornament SET used = used + 1 + WHERE box_id = #{boxId} AND ornament_id = #{ornamentId} AND audience = #{audience} + + + + UPDATE game_olympus_box_ornament SET used = 0 + WHERE box_id = #{boxId} AND audience = #{audience} + + + diff --git a/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusRoundMapper.xml b/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusRoundMapper.xml new file mode 100644 index 0000000..2300ef5 --- /dev/null +++ b/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusRoundMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/skins-service/game-wheel-service/doc/data.sql b/skins-service/game-wheel-service/doc/data.sql new file mode 100644 index 0000000..4d94406 --- /dev/null +++ b/skins-service/game-wheel-service/doc/data.sql @@ -0,0 +1 @@ +todo \ No newline at end of file diff --git a/skins-service/game-wheel-service/doc/init.sql b/skins-service/game-wheel-service/doc/init.sql new file mode 100644 index 0000000..b54c54e --- /dev/null +++ b/skins-service/game-wheel-service/doc/init.sql @@ -0,0 +1,62 @@ +CREATE TABLE `game_wheel_round` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '唯一自增id', + `date` varchar(10) NOT NULL COMMENT '所属日期', + `round_id` bigint NOT NULL COMMENT 'round id', + `start_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间', + `end_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '结束时间', + `phase_times` varchar(128) NOT NULL DEFAULT '' COMMENT '每个阶段的时间点', + `total_bet` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '本期总下注数', + `total_win` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '本期玩家总赢数', + `symbols` varchar(64) NOT NULL DEFAULT '' COMMENT '转盘上的符号', + `result` varchar(64) NOT NULL COMMENT '开出的奖的集合', + `is_manual` int NOT NULL DEFAULT 0 COMMENT '是否后台开奖', + `status` varchar(16) NOT NULL DEFAULT '' COMMENT '状态', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_date_round_id` (`date`, `round_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='转盘每期表'; + +CREATE TABLE `game_wheel_round_bet` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '唯一自增id', + `round_id` bigint NOT NULL COMMENT 'round id', + `date` varchar(10) NOT NULL DEFAULT '' COMMENT '所属日期yyyyMMdd(roundId每日重置,需与date联合定位回合)', + `user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户id', + `bet` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '下注数', + `bet_symbol` varchar(4) NOT NULL DEFAULT '' COMMENT '压注目标', + `win` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '赔付结果', + `settled` int NOT NULL DEFAULT 0 COMMENT '是否已结算 0=未结算 1=已结算', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_date_round_user_symbol` (`date`, `round_id`, `user_id`, `bet_symbol`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='转盘下注表'; + +CREATE TABLE `game_wheel_user` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', + `user_id` bigint NOT NULL COMMENT 'user表的id', + `total_bet` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '用户总下注额', + `total_win` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '用户总赢得金额', + `count` int NOT NULL DEFAULT '0' COMMENT '总下注次数', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='转盘用户表'; + + +ALTER TABLE sys_config MODIFY COLUMN config_value VARCHAR(1024); + +游戏配置数据: +``` +{ + // 核心配置 + "symbols": [1,2,3,4,5,6,7,8,7,6,5,4,3,2,1], // 转盘格子配置 + "buffer_ratio": 0.9, // 一次旋转,最多可用的奖池比例 + "rtp": 0.85, // 一次旋转最多返回给玩家的总投注比例 + // 下面是体验相关,不影响利润 + "special_effects": [{"name": "系统赢", "ratio": 0.7},{"name": "免费旋转1次", "ratio": 0.2},{"name": "免费旋转2次", "ratio": 0.1}] +} +``` + diff --git a/skins-service/game-wheel-service/pom.xml b/skins-service/game-wheel-service/pom.xml new file mode 100644 index 0000000..aaafd79 --- /dev/null +++ b/skins-service/game-wheel-service/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + com.ruoyi + skins-service + 4.8.2 + + + + 疯狂转盘 + + + game-wheel-service + + + 8 + 8 + UTF-8 + + + + + + com.ruoyi + ruoyi-framework + + + + com.ruoyi + service-admin + 4.8.2 + + + + com.ruoyi + service-user + 4.8.2 + + + + com.ruoyi + service-thirdparty + 4.8.2 + + + + + com.ruoyi + message-service + 4.8.2 + + + + + diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/cache/WheelRewardPoolCacheRepository.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/cache/WheelRewardPoolCacheRepository.java new file mode 100644 index 0000000..7810b97 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/cache/WheelRewardPoolCacheRepository.java @@ -0,0 +1,105 @@ +package com.ruoyi.game.wheel.cache; +import com.ruoyi.game.wheel.mapper.GameWheelPoolAdjustmentMapper; +import com.ruoyi.game.wheel.model.cache.WheelRewardPoolCache; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.concurrent.TimeUnit; + +import static com.ruoyi.admin.config.RedisConstants.WHEEL_GAME_REWARD_POOL_CACHE; +@Component +@Slf4j +public class WheelRewardPoolCacheRepository { + @Autowired + public RedisTemplate redisTemplate; + + @Autowired + private GameWheelPoolAdjustmentMapper poolAdjustmentMapper; + + /** + * 奖池余额(唯一口径)= 累计下注 − 累计返奖 + Σ人工调整(抽水为负)。 + * 开奖上限与后台展示都必须走这里,否则会出现"抽水后页面变小、开奖仍按抽水前算"。 + */ + public BigDecimal getBalance(String poolType) { + WheelRewardPoolCache cache = getCache(poolType); + return cache.getTotalBet().subtract(cache.getTotalWin()) + .add(poolAdjustmentMapper.sumAdjustmentByPoolType(poolType)); + } + + /** + * 金额统一成 2 位小数再用/再存。 + * 池子用 Redis INCRBYFLOAT(double)累加,不夹小数位会积出 0.0000001 级别的漂移; + * 入库/出库都按分(2 位)对齐,与 DB 里的 DECIMAL(10,2) 口径一致。 + */ + private static BigDecimal scale2(BigDecimal v) { + return v == null ? BigDecimal.ZERO : v.setScale(2, RoundingMode.HALF_UP); + } + + /** 幂等键保留时长:异常重跑都发生在当天,8 天足够;periodNo 全局唯一,不会跨局复用 */ + private static final long POOL_ONCE_TTL_DAYS = 8; + + /** + * 按局注入本局下注(同一 periodNo 只注入一次)。 + * 开奖流程在结算之前注入;一旦结算抛异常/进程中断,兜底路径会重跑开奖, + * 没有这个守卫就会把同一局的投注重复计入 totalBet,虚增奖池并抬高后续所有局的发奖上限。 + * + * @return true=本次真正注入;false=该局已注入过(跳过) + */ + public boolean incrementalBetOnce(String poolType, Long periodNo, BigDecimal totalBet) { + if (periodNo == null || totalBet == null || totalBet.signum() <= 0) return false; + Boolean first = redisTemplate.opsForValue() + .setIfAbsent(poolKey(poolType) + ":betOnce:" + periodNo, "1", POOL_ONCE_TTL_DAYS, TimeUnit.DAYS); + if (!Boolean.TRUE.equals(first)) return false; + redisTemplate.opsForValue().increment(poolKey(poolType) + ":bet", scale2(totalBet).doubleValue()); + return true; + } + + /** 按局注入本局返奖(同一 periodNo 只注入一次) */ + public boolean incrementalWinOnce(String poolType, Long periodNo, BigDecimal totalWin) { + if (periodNo == null || totalWin == null || totalWin.signum() <= 0) return false; + Boolean first = redisTemplate.opsForValue() + .setIfAbsent(poolKey(poolType) + ":winOnce:" + periodNo, "1", POOL_ONCE_TTL_DAYS, TimeUnit.DAYS); + if (!Boolean.TRUE.equals(first)) return false; + redisTemplate.opsForValue().increment(poolKey(poolType) + ":win", scale2(totalWin).doubleValue()); + return true; + } + + /** + * 退款时反向冲减该局已注入的下注(金额 = 这一笔被退回的真实本金)。 + * 只在"该局确实注入过"(betOnce 键存在)时生效:投注期就被退款的局从未注入过,不能反向扣。 + * 不会重复冲减:调用方(refundBets)按注单的 settled=0→1 CAS 保证一张注单只退一次。 + * 方向说明:回滚等极端情况下宁可少冲(池子偏保守、少发),也不会出现多冲导致多发。 + */ + public void reverseBetInjection(String poolType, Long periodNo, BigDecimal amount) { + if (periodNo == null || amount == null || amount.signum() <= 0) return; + if (!Boolean.TRUE.equals(redisTemplate.hasKey(poolKey(poolType) + ":betOnce:" + periodNo))) return; + redisTemplate.opsForValue().increment(poolKey(poolType) + ":bet", scale2(amount).negate().doubleValue()); + } + + /** + * 清空指定池的累计下注/返奖统计(配合作废人工调整记录,余额即归零)。 + * 只删统计键,不动进行中回合的缓存。 + */ + public void reset(String poolType) { + redisTemplate.delete(poolKey(poolType) + ":bet"); + redisTemplate.delete(poolKey(poolType) + ":win"); + } + + /** 读取指定池的累计数据 */ + public WheelRewardPoolCache getCache(String poolType) { + var betS = redisTemplate.opsForValue().get(poolKey(poolType) + ":bet"); + var winS = redisTemplate.opsForValue().get(poolKey(poolType) + ":win"); + WheelRewardPoolCache cache = new WheelRewardPoolCache(); + cache.setTotalBet(scale2(betS == null ? null : new BigDecimal(betS.toString()))); + cache.setTotalWin(scale2(winS == null ? null : new BigDecimal(winS.toString()))); + return cache; + } + + private String poolKey(String poolType) { + return WHEEL_GAME_REWARD_POOL_CACHE + ":" + poolType; + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/cache/WheelRoundCacheRepository.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/cache/WheelRoundCacheRepository.java new file mode 100644 index 0000000..1de3175 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/cache/WheelRoundCacheRepository.java @@ -0,0 +1,30 @@ +package com.ruoyi.game.wheel.cache; + +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.json.SnakeCaseJsonUtils; +import com.ruoyi.game.wheel.constants.CacheConstants; +import com.ruoyi.game.wheel.model.cache.WheelRoundCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class WheelRoundCacheRepository { + @Autowired + private RedisCache redisCache; + + public WheelRoundCache getCache() { + String s = redisCache.getCacheObject(CacheConstants.WHEEL_ROUND); + return SnakeCaseJsonUtils.fromSnakeCaseJson(s, WheelRoundCache.class); + } + + public void setCache(WheelRoundCache cache) { + if (cache == null) { + redisCache.setCacheObject(CacheConstants.WHEEL_ROUND, null, 1, TimeUnit.DAYS); + } else { + String json = SnakeCaseJsonUtils.toSnakeCaseJson(cache); + redisCache.setCacheObject(CacheConstants.WHEEL_ROUND, json, 1, TimeUnit.DAYS); + } + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/constants/CacheConstants.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/constants/CacheConstants.java new file mode 100644 index 0000000..5c673b1 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/constants/CacheConstants.java @@ -0,0 +1,15 @@ +package com.ruoyi.game.wheel.constants; + +public class CacheConstants { + private static final String PREFIX = "ruoyi:wheel:"; + + public static final String WHEEL_ROUND = PREFIX + "round"; + + /** 各期注单读取缓存(前缀,实际 key = 前缀 + periodNo):非投注期展示用,避免高并发轮询反复查库 */ + public static final String WHEEL_ROUND_BETS = PREFIX + "round_bets:"; + + public static final String WHEEL_LOCK = PREFIX + "lock"; + + /** 用户级下注锁(前缀,实际 key = 前缀 + userId),串行化同用户下注防连点 */ + public static final String WHEEL_BET_USER_LOCK = PREFIX + "bet:user:"; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/constants/WheelPoolType.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/constants/WheelPoolType.java new file mode 100644 index 0000000..db0d4d4 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/constants/WheelPoolType.java @@ -0,0 +1,57 @@ +package com.ruoyi.game.wheel.constants; + +/** + * 疯狂转盘决策池类型。 + *

+ * 单一真实奖池:所有参与落点/ROTP 决策与统计的下注都归入 real 池。 + * 用户类型划分: + * real(真实池,参与落点与 RTP) = 普通(02) + 主播(01) + 招商(04) + 卡商(05) + * display(展示,可下注/返奖但绝不入真实池) = 机器人(03) + 白号(06) + * 超管(99) 不参与转盘。 + *

+ * 一轮只有一个落点,所有人看到同一个落点;机器人/白号跟同一落点结算,但资金不污染真实池。 + */ +public final class WheelPoolType { + + /** 唯一真实奖池:普通(02)+主播(01)+招商(04)+卡商(05) */ + public static final String REAL = "real"; + /** 展示类型:机器人(03)+白号(06),可下注/返奖但不入真实奖池 */ + public static final String DISPLAY = "display"; + + private WheelPoolType() { + } + + /** + * 用户类型 → 决策池。 + * + * @param userType tt_user.user_type(01主播/02普通/03机器人/04招商/05卡商/06白号/99超管) + * @return real=进真实池;display=机器人/白号不入池;null=不参与转盘(如 99 超管) + */ + public static String ofUserType(String userType) { + if (userType == null) { + return null; + } + switch (userType) { + case "01": + case "02": + case "04": + case "05": + return REAL; + case "03": + case "06": + return DISPLAY; + default: + return null; + } + } + + /** 是否进真实奖池(参与落点/RTP 决策与统计) */ + public static boolean isReal(String poolType) { + return REAL.equals(poolType); + } + + /** 唯一奖池(开奖/统计用) */ + public static String[] all() { + return new String[]{REAL}; + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/admin/response/QueryWheelGameConfig.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/admin/response/QueryWheelGameConfig.java new file mode 100644 index 0000000..ae68e52 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/admin/response/QueryWheelGameConfig.java @@ -0,0 +1,11 @@ +package com.ruoyi.game.wheel.contract.admin.response; + +import com.ruoyi.game.wheel.domain.GameWheelGameConfig; +import lombok.Data; + +@Data +public class QueryWheelGameConfig { + private GameWheelGameConfig config; + /** 4池统计:real/robot/blank/promo 各含 totalBet/totalWin/profit/rtp */ + private java.util.Map poolStats; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/DoBetRequest.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/DoBetRequest.java new file mode 100644 index 0000000..a6ec215 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/DoBetRequest.java @@ -0,0 +1,20 @@ +package com.ruoyi.game.wheel.contract.api.request; + +import lombok.Data; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Data +public class DoBetRequest { + @NotNull(message = "投注金额不能为空") + @Min(value = 1, message = "投注金额不能小于1") + private Integer bet; + + @NotBlank(message = "投注颜色不能为空") + private String color; + + @NotBlank(message = "投注符号不能为空") + private String symbol; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetGameConfigRequest.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetGameConfigRequest.java new file mode 100644 index 0000000..e583c01 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetGameConfigRequest.java @@ -0,0 +1,7 @@ +package com.ruoyi.game.wheel.contract.api.request; + +import lombok.Data; + +@Data +public class GetGameConfigRequest { +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetHistoryBetRequest.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetHistoryBetRequest.java new file mode 100644 index 0000000..3645dd0 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetHistoryBetRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.game.wheel.contract.api.request; + +import lombok.Data; + +@Data +public class GetHistoryBetRequest { + private Integer pageNum; + private Integer pageSize; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetLastRoundInfoRequest.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetLastRoundInfoRequest.java new file mode 100644 index 0000000..eb59ce5 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetLastRoundInfoRequest.java @@ -0,0 +1,7 @@ +package com.ruoyi.game.wheel.contract.api.request; + +import lombok.Data; + +@Data +public class GetLastRoundInfoRequest { +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetRoundHistoryRequest.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetRoundHistoryRequest.java new file mode 100644 index 0000000..f762b3f --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetRoundHistoryRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.game.wheel.contract.api.request; + +import lombok.Data; + +@Data +public class GetRoundHistoryRequest { + private Integer pageNum; + private Integer pageSize; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetRoundResultsRequest.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetRoundResultsRequest.java new file mode 100644 index 0000000..a3c7686 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/request/GetRoundResultsRequest.java @@ -0,0 +1,14 @@ +package com.ruoyi.game.wheel.contract.api.request; + +import lombok.Data; + +/** + * 获取历史开奖结果请求 + */ +@Data +public class GetRoundResultsRequest { + /** 页码(默认 1) */ + private Integer pageNum; + /** 每页条数(默认 20) */ + private Integer pageSize; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/DoBetResponse.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/DoBetResponse.java new file mode 100644 index 0000000..bf32e22 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/DoBetResponse.java @@ -0,0 +1,13 @@ +package com.ruoyi.game.wheel.contract.api.response; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class DoBetResponse { + private Long roundId; + private String cost; + private String balance; + private String credits; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetGameConfigResponse.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetGameConfigResponse.java new file mode 100644 index 0000000..0b2eca7 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetGameConfigResponse.java @@ -0,0 +1,62 @@ +package com.ruoyi.game.wheel.contract.api.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@Data +public class GetGameConfigResponse { + /** 转盘总格数(内外圈各24,索引一一对应) */ + private int segmentCount; + /** 外圈24格符号固定布局,如 AABBBCCCCCCCCDDDDDDDDDDD(A金符/B红符/C紫符/D蓝符) */ + private String outerSymbols; + /** 内圈颜色分布(每轮随机排布,合计=segmentCount) */ + private InnerColorDist innerColorDist; + /** 可选下注额(10档) */ + private List bets; + /** 12 个倍率(3内圈色 × 4外圈符号) */ + private List multipliers; + /** 当前用户所属决策池:real/robot/blank/promo;null=不可参与(如超管99) */ + private String myPoolType; + /** 奖励矩阵(120条,前端下注面板按下注额筛选展示对应饰品) */ + private List rewardMatrix; + + /** 内圈颜色分布 */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class InnerColorDist { + private int gold; + private int red; + private int purple; + } + + /** 倍率项:内圈色 × 外圈符号 */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Multiplier { + private String color; + private String symbol; + private Integer multiplier; + } + + /** 奖励矩阵单元:下注额 × 颜色 × 符号 → 自定义饰品 */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class RewardCell { + private Integer betAmount; + private String color; + private String symbol; + private Integer multiplier; + private BigDecimal prizeValue; + private String prizeName; + private Integer prizeGrade; + private String prizeExterior; + private String prizeImage; + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetRoundHistoryResponse.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetRoundHistoryResponse.java new file mode 100644 index 0000000..ed52749 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetRoundHistoryResponse.java @@ -0,0 +1,41 @@ +package com.ruoyi.game.wheel.contract.api.response; + +import lombok.Data; + +import java.util.List; + +@Data +public class GetRoundHistoryResponse { + + /** + * 总记录数 + */ + private long total; + + /** + * 列表数据 + */ + private List rows; + + @Data + public static class BetItem { + private Long periodNo; + private String endTime; + private Long userId; + private String nickName; + // 用户头像 + private String avatar; + private String bet; + // 中奖符号 + private String betSymbol; + private String win; + // 该中奖符号下注次数 + private Integer betCount; + // 饰品品质等级 + private Integer prizeGrade; + // 饰品名称 + private String prizeName; + // 饰品图片 + private String prizeImage; + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetRoundInfoResponse.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetRoundInfoResponse.java new file mode 100644 index 0000000..66c89ce --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetRoundInfoResponse.java @@ -0,0 +1,108 @@ +package com.ruoyi.game.wheel.contract.api.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@Data +public class GetRoundInfoResponse { + private Long roundId; + private Long periodNo; + private Long serverTime; + // 本局转盘开始时间 + private Long startTime; + // 本局转盘下注结束时间(封盘) + private Long bettedTime; + // 开始旋转时间 + private Long spinTime; + // 结束时间 + private Long endTime; + /** @see com.ruoyi.game.wheel.enums.WheelRoundStatusEnum */ + private String status; + + // ===== 游戏配置 ===== + private List outerSymbols; + private List bets; + private String myPoolType; + private List rewardMatrix; + + /** + * 游戏关闭 / 维护中(全站维护或轮盘维护),前端据此提示"维护中"而不是"当前不在下注期"。 + * 注意类型必须是包装类 Boolean:字段名以 is 开头时,Lombok 只会给 Boolean 生成 getIsMaintenance(), + * 用基本类型 boolean 会生成 isMaintenance(),序列化出去的键会变成 maintenance,前端读不到。 + */ + private Boolean isMaintenance; + + /** 本轮内圈24色(gold/red/purple,前端渲染盘面) */ + private List innerColors; + + // ===== 当前用户所属池的开奖结果(未开奖为 null) ===== + private Integer resultIndex; + private String resultColor; + private String resultSymbol; + + // 当前用户本轮总返奖(饰品价值合计) + private String totalWin; + // 当前用户本轮下注明细 + private List userBets; + // 当前用户本轮中奖明细(命中的注,每注一条) + private List winResult; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class RewardCell { + private Integer betAmount; + private String color; + private String symbol; + private Integer multiplier; + private BigDecimal prizeValue; + private String prizeName; + private Integer prizeGrade; + private String prizeExterior; + private String prizeImage; + /** 饰品品质图片 */ + private String prizeLevelImg; + } + + /** 当前用户下注汇总(按颜色+符号聚合) */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class UserBet { + private String color; + private String symbol; + /** 下注次数 */ + private Integer count; + /** 总下注额 */ + private BigDecimal totalBet; + } + + /** 当前用户发奖明细(每注一条,命中发矩阵饰品,未命中发安慰饰品) */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class WinResult { + private String color; + private String symbol; + /** 是否命中(true=中奖矩阵饰品,false=安慰饰品) */ + private boolean hit; + /** 背包记录ID(用于分解/出售) */ + private Long boxRecordId; + /** 饰品名称 */ + private String prizeName; + /** 饰品图片 */ + private String prizeImage; + /** 饰品品质等级 */ + private Integer prizeGrade; + /** 饰品品质图片 */ + private String prizeLevelImg; + /** 饰品磨损度 */ + private String prizeExterior; + /** 饰品价格 */ + private BigDecimal prizeValue; + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetRoundResultsResponse.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetRoundResultsResponse.java new file mode 100644 index 0000000..f2d4590 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/contract/api/response/GetRoundResultsResponse.java @@ -0,0 +1,38 @@ +package com.ruoyi.game.wheel.contract.api.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 获取历史开奖结果响应 + */ +@Data +public class GetRoundResultsResponse { + /** 总记录数 */ + private Long total; + /** 开奖记录列表 */ + private List rows; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class RoundResult { + /** 期号(完整13位) */ + private Long periodNo; + /** 日期(yyyyMMdd,8位) */ + private String date; + /** 当天局号(1-99999) */ + private Long roundId; + /** 开奖内圈颜色 gold/red/purple */ + private String resultColor; + /** 开奖外圈符号 A/B/C/D */ + private String resultSymbol; + /** 开奖格子索引(0-23) */ + private Integer resultIndex; + /** 结束时间(yyyy-MM-dd HH:mm:ss) */ + private String endTime; + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/controller/AdminWheelController.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/controller/AdminWheelController.java new file mode 100644 index 0000000..7acf6a3 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/controller/AdminWheelController.java @@ -0,0 +1,226 @@ +package com.ruoyi.game.wheel.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.game.wheel.domain.*; +import com.ruoyi.game.wheel.mapper.*; +import com.ruoyi.game.wheel.constants.WheelPoolType; +import com.ruoyi.game.wheel.service.AdminWheelService; +import com.ruoyi.game.wheel.vo.WheelBetRecordVO; +import com.ruoyi.game.wheel.vo.WheelPoolAdjustmentVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +@Api(tags = "疯狂转盘") +@Slf4j +@RestController +@RequestMapping("/admin/wheel") +public class AdminWheelController extends BaseController { + + @Autowired + private AdminWheelService adminWheelService; + + @Autowired + private GameWheelPrizeMapper wheelPrizeMapper; + + @Autowired + private GameWheelPrizeOrnamentMapper prizeOrnamentMapper; + + @Autowired + private GameWheelPrizeRangeMapper prizeRangeMapper; + + @Autowired + private ApiGameWheelRoundMapper wheelRoundMapper; + + @Autowired + private ApiGameWheelRoundBetMapper wheelRoundBetMapper; + + @Autowired + private GameWheelRewardMatrixMapper rewardMatrixMapper; + + @Autowired + private GameWheelPoolAdjustmentMapper poolAdjustmentMapper; + + @ApiOperation("获取疯狂转盘游戏配置") + @GetMapping("/gameconfig") + public AjaxResult getGameConfig() { + return adminWheelService.getGameConfig(); + } + + @Log(title = "游戏配置", action = "保存", businessType = BusinessType.UPDATE) + @ApiOperation("更新疯狂转盘游戏配置") + @PostMapping("/gameconfig") + public AjaxResult updateGameConfig(@RequestBody String body) { + return adminWheelService.updateGameConfig(body); + } + + @ApiOperation("回合记录列表") + @GetMapping("/round/list") + public TableDataInfo roundList( + @RequestParam(required = false) String date, + @RequestParam(required = false) Long roundId) { + startPage(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.isNotEmpty(date)) { + wrapper.eq(GameWheelRound::getDate, date); + } + if (roundId != null) { + wrapper.eq(GameWheelRound::getRoundId, roundId); + } + wrapper.orderByDesc(GameWheelRound::getId); + List list = wheelRoundMapper.selectList(wrapper); + return getDataTable(list); + } + + @ApiOperation("投注记录列表") + @GetMapping("/bet/list") + public TableDataInfo betList( + @RequestParam(required = false) Long roundId, + @RequestParam(required = false) Long userId, + @RequestParam(required = false) String periodNo) { + startPage(); + List list = wheelRoundBetMapper.selectBetRecords(roundId, userId, periodNo); + return getDataTable(list); + } + + // ======================== 奖励矩阵 CRUD ======================== + + @ApiOperation("奖励矩阵列表") + @GetMapping("/matrix/list") + public AjaxResult matrixList( + @RequestParam(required = false) Integer betAmount, + @RequestParam(required = false) String color, + @RequestParam(required = false) String symbol) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GameWheelRewardMatrix::getStatus, "1"); + if (betAmount != null) wrapper.eq(GameWheelRewardMatrix::getBetAmount, betAmount); + if (StringUtils.isNotEmpty(color)) wrapper.eq(GameWheelRewardMatrix::getColor, color); + if (StringUtils.isNotEmpty(symbol)) wrapper.eq(GameWheelRewardMatrix::getSymbol, symbol); + wrapper.orderByAsc(GameWheelRewardMatrix::getBetAmount) + .orderByAsc(GameWheelRewardMatrix::getColor) + .orderByAsc(GameWheelRewardMatrix::getSymbol); + List list = rewardMatrixMapper.selectList(wrapper); + return AjaxResult.success(Map.of("list", list)); + } + + @Log(title = "奖励矩阵", action = "保存", businessType = BusinessType.UPDATE) + @ApiOperation("保存奖励矩阵行") + @PostMapping("/matrix/save") + public AjaxResult matrixSave(@RequestBody GameWheelRewardMatrix matrix) { + GameWheelRewardMatrix exist = matrix.getId() == null ? null : rewardMatrixMapper.selectById(matrix.getId()); + if (exist != null) { + // 更新分支同样要重算奖品价值:否则 multiplier 变了、prize_value 还是旧值, + // 后台/客户端展示的价值与实际结算(下注额×快照倍率)长期不一致 + Integer betAmount = matrix.getBetAmount() != null ? matrix.getBetAmount() : exist.getBetAmount(); + Integer multiplier = matrix.getMultiplier() != null ? matrix.getMultiplier() : exist.getMultiplier(); + if (betAmount != null && multiplier != null) { + matrix.setPrizeValue(BigDecimal.valueOf(betAmount).multiply(BigDecimal.valueOf(multiplier))); + } + rewardMatrixMapper.updateById(matrix); + } else { + if (matrix.getMultiplier() != null && matrix.getBetAmount() != null) { + matrix.setPrizeValue(BigDecimal.valueOf(matrix.getBetAmount()) + .multiply(BigDecimal.valueOf(matrix.getMultiplier()))); + } + matrix.setStatus("1"); + rewardMatrixMapper.insert(matrix); + } + return AjaxResult.success(); + } + + @Log(title = "奖励矩阵", action = "删除", businessType = BusinessType.DELETE) + @ApiOperation("删除奖励矩阵行(软删:置 status=0)") + @DeleteMapping("/matrix/delete/{id}") + public AjaxResult matrixDelete(@PathVariable Long id) { + rewardMatrixMapper.update(null, new LambdaUpdateWrapper() + .eq(GameWheelRewardMatrix::getId, id) + .set(GameWheelRewardMatrix::getStatus, "0")); + return AjaxResult.success(); + } + + // ======================== 奖池统计 ======================== + + @ApiOperation("奖池统计") + @GetMapping("/pool/stats") + public AjaxResult poolStats() { + return AjaxResult.success(adminWheelService.buildPoolStats()); + } + + // ======================== 奖池调节 ======================== + + @Log(title = "游戏配置", action = "调整奖池", businessType = BusinessType.GRANT) + @ApiOperation("奖池调整(注水/抽水)") + @PostMapping("/pool/adjust") + @RepeatSubmit(interval = 3000) + public AjaxResult adjustPool(@RequestBody Map params) { + return adminWheelService.adjustPool(params, getUsername()); + } + + @Log(title = "游戏配置", action = "重置奖池", businessType = BusinessType.CLEAN) + @ApiOperation("重置奖池(累计统计与余额一起归零)") + @PostMapping("/pool/reset") + public AjaxResult resetPool(@RequestBody(required = false) Map params) { + Map p = params == null ? Collections.emptyMap() : params; + String poolType = (String) p.getOrDefault("poolType", WheelPoolType.REAL); + String reason = p.get("reason") == null ? "" : p.get("reason").toString(); + return adminWheelService.resetPool(poolType, reason, getUsername()); + } + + @ApiOperation("奖池调整记录") + @GetMapping("/pool/adjustLog") + public TableDataInfo adjustLog(@RequestParam(required = false) String poolType) { + startPage(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.isNotEmpty(poolType)) { + wrapper.eq(GameWheelPoolAdjustment::getPoolType, poolType); + } + wrapper.orderByDesc(GameWheelPoolAdjustment::getCreateTime); + List list = poolAdjustmentMapper.selectList(wrapper).stream() + .map(this::toPoolAdjustmentVO) + .collect(Collectors.toList()); + return getDataTable(list); + } + + private WheelPoolAdjustmentVO toPoolAdjustmentVO(GameWheelPoolAdjustment source) { + WheelPoolAdjustmentVO target = new WheelPoolAdjustmentVO(); + target.setId(source.getId()); + target.setPoolType(source.getPoolType()); + target.setAdjustmentAmount(source.getAdjustmentAmount()); + target.setBalanceBefore(source.getBalanceBefore()); + target.setBalanceAfter(source.getBalanceAfter()); + target.setReason(source.getReason()); + target.setOperator(source.getOperator()); + target.setCreateTime(source.getCreateTime() == null ? null + : source.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + return target; + } + + @ApiOperation("获取奖池余额(含人工调整)") + @GetMapping("/pool/balance") + public AjaxResult getPoolBalance(@RequestParam(required = false) String poolType) { + if (StringUtils.isEmpty(poolType)) { + poolType = WheelPoolType.REAL; + } + BigDecimal balance = adminWheelService.getPoolBalance(poolType); + Map result = new LinkedHashMap<>(); + result.put("poolType", poolType); + result.put("balance", balance); + return AjaxResult.success(result); + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/controller/ApiWheelController.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/controller/ApiWheelController.java new file mode 100644 index 0000000..e117afa --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/controller/ApiWheelController.java @@ -0,0 +1,121 @@ +package com.ruoyi.game.wheel.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.game.wheel.contract.api.request.DoBetRequest; +import com.ruoyi.game.wheel.contract.api.request.GetGameConfigRequest; +import com.ruoyi.game.wheel.contract.api.request.GetHistoryBetRequest; +import com.ruoyi.game.wheel.contract.api.request.GetLastRoundInfoRequest; +import com.ruoyi.game.wheel.contract.api.request.GetRoundHistoryRequest; +import com.ruoyi.game.wheel.contract.api.request.GetRoundResultsRequest; +import com.ruoyi.game.wheel.contract.api.response.DoBetResponse; +import com.ruoyi.game.wheel.contract.api.response.GetGameConfigResponse; +import com.ruoyi.game.wheel.contract.api.response.GetRoundHistoryResponse; +import com.ruoyi.game.wheel.contract.api.response.GetRoundInfoResponse; +import com.ruoyi.game.wheel.contract.api.response.GetRoundResultsResponse; +import com.ruoyi.game.wheel.service.ApiWheelService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.service.ApiUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import java.util.Map; +import org.springframework.web.bind.annotation.RestController; + +@Api(tags = "疯狂转盘") +@Slf4j +@RestController +@RequestMapping("/api/wheel") +public class ApiWheelController extends BaseController { + + @Autowired + private ApiWheelService apiWheelService; + + @Autowired + private ApiUserService userService; + + @Autowired + private ISysConfigService sysConfigService; + + public R checkLogin() { + Long userId; + try { + userId = getUserId(); + if (ObjectUtil.isEmpty(userId)) return R.fail(401, "登录过期,请重新登录。"); + return R.ok(userId); + } catch (Exception e) { + return R.fail(401, "登录过期,请重新登录。"); + } + } + + @ApiOperation("投注") + @PostMapping("/dobet") + // 这里**不加** @RepeatSubmit:一局允许同一组合重复下注(最多 50 注),连点两次 5 元 = 两注, + // 是正常玩法而不是重复提交;加了会把第二次合法下注挡成"不允许重复提交"。 + // 防连点由客户端 _bettingRequesting 锁负责,服务端由用户锁 + 50 注上限兜住。 + @UpdateUserCache + public R doBet(@RequestBody @Validated DoBetRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String wheelMaintenance = sysConfigService.selectConfigByKey("wheelMaintenance"); + if ("1".equals(wheelMaintenance)) { + return R.fail("疯狂转盘维护中......"); + } + R loginCheck = checkLogin(); + if (!loginCheck.getCode().equals(200)) return loginCheck; + Long userId = (Long) loginCheck.getData(); + + var r = userService.checkRecharge(userId.intValue()); + if (R.isError(r)) { + // 透传业务码(如 703 未充值),供前端识别 + return R.fail(r.getCode().intValue(), r.getMsg()); + } + return apiWheelService.doBet(request, userId); + } + + @ApiOperation("获取最新一期游戏状态") + @PostMapping("/last_round_info") + public R getLastRoundInfo(@RequestBody GetLastRoundInfoRequest request) { + R loginCheck = checkLogin(); + if (!loginCheck.getCode().equals(200)) return loginCheck; + Long userId = (Long) loginCheck.getData(); + return apiWheelService.getLastRoundInfo(userId); + } + + @ApiOperation("获取玩家的游戏历史记录") + @PostMapping("/history_bet_info") + public R getHistoryBetInfo(@RequestBody GetHistoryBetRequest request) { + R loginCheck = checkLogin(); + if (!loginCheck.getCode().equals(200)) return loginCheck; + Long userId = (Long) loginCheck.getData(); + return apiWheelService.getHistoryBetInfo(request, userId); + } + + @ApiOperation("获取疯狂转盘游戏历史记录") + @PostMapping("/round_history_bet_info") + public R getRoundHistoryBetInfo(@RequestBody GetRoundHistoryRequest request) { + R loginCheck = checkLogin(); + if (!loginCheck.getCode().equals(200)) return loginCheck; + Long userId = (Long) loginCheck.getData(); + return apiWheelService.getRoundHistoryInfo(request); + } + + @ApiOperation("获取历史开奖结果(每一轮的开奖符号)") + @PostMapping("/round_results") + public R getRoundResults(@RequestBody GetRoundResultsRequest request) { + R loginCheck = checkLogin(); + if (!loginCheck.getCode().equals(200)) return loginCheck; + return apiWheelService.getRoundResults(request); + } + + +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelGameConfig.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelGameConfig.java new file mode 100644 index 0000000..1ba1ce7 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelGameConfig.java @@ -0,0 +1,61 @@ +package com.ruoyi.game.wheel.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +public class GameWheelGameConfig { + // 玩法配置 + // 是否关闭游戏 false + private boolean switchOff; + // 可选的下注数 + private List bets; + + // ===== 内圈玩法新配置(内圈3色 + 外圈4符号,各24格) ===== + /** 转盘总格数(内外圈各24格,索引一一对应) */ + private int segmentCount = 24; + /** 外圈24格符号固定布局(A金符/B红符/C紫符/D蓝符,分布 金2红3紫8蓝11) */ + private String outerSymbols; + /** 内圈金色格数(每轮随机排布,三色合计=segmentCount) */ + private int innerGoldCount = 6; + /** 内圈红色格数 */ + private int innerRedCount = 8; + /** 内圈紫色格数 */ + private int innerPurpleCount = 10; + /** 12个倍率:3内圈色 × 4外圈符号(跨10档下注额共用) */ + private List multipliers; + + // 下注期 + private int bettingDuration = 10; + // 封盘期5秒 + private int bettedDuration = 0; + // 开奖动画时长(秒) + private int animationDuration = 5; + /** @deprecated 后端不使用此字段,前端也拿不到;保留仅为兼容已存储配置的反序列化 */ + @Deprecated + private int resultDisplayDuration = 5; + + // ===== 发奖上限(payoutCap = min(奖池余额×bufferRatio, 累计真实投注×rtp − 累计真实返奖)) ===== + // 一次旋转最多可动用的奖池比例(作用在奖池余额上)。字段名沿用旧名,语义与"保留比例"相反: + // 0.6 = 最多拿出奖池余额的 60%,不是保留 60%。改键名会让存量配置反序列化失效,故保留 bufferRatio。 + private double bufferRatio; + // 目标 RTP(累计口径):本局可发额度 = 累计真实投注 × rtp − 累计真实返奖,下限 0。 + // 历史 RTP 恰好 = rtp 时该额度退化为「本局投注 × rtp」;历史返奖低则额度累积(可爆大奖),高则收紧。 + private double rtp; + + /** 内圈色×外圈符号的倍率项(12个组合) */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class MultiplierItem { + /** 内圈颜色 gold/red/purple */ + private String color; + /** 外圈符号 A/B/C/D */ + private String symbol; + /** 倍率 */ + private Integer multiplier; + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelPoolAdjustment.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelPoolAdjustment.java new file mode 100644 index 0000000..869d85b --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelPoolAdjustment.java @@ -0,0 +1,50 @@ +package com.ruoyi.game.wheel.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 疯狂转盘奖池调节记录 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName("game_wheel_pool_adjustment") +public class GameWheelPoolAdjustment implements Serializable { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 池类型(real/display) */ + private String poolType; + + /** 调整金额(正数=注水,负数=抽水) */ + private BigDecimal adjustmentAmount; + + /** 调整前余额 */ + private BigDecimal balanceBefore; + + /** 调整后余额 */ + private BigDecimal balanceAfter; + + /** 调整原因 */ + private String reason; + + /** 操作人 */ + private String operator; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelPrizeOrnament.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelPrizeOrnament.java new file mode 100644 index 0000000..7b7234c --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelPrizeOrnament.java @@ -0,0 +1,36 @@ +package com.ruoyi.game.wheel.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@TableName("game_wheel_prize_ornament") +public class GameWheelPrizeOrnament { + @TableId(type = IdType.AUTO) + private Long id; + @TableField("game_type") + private String gameType; + private String name; + @TableField("image_url") + private String imageUrl; + private Integer grade; + private Integer sort; + private String status; + @TableField("is_fallback") + private Integer isFallback; + @TableField("exterior_name") + private String exteriorName; + @TableField("create_time") + private Date createTime; + + @TableField(exist = false) + private BigDecimal winLow; + @TableField(exist = false) + private BigDecimal winHigh; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelPrizeRange.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelPrizeRange.java new file mode 100644 index 0000000..610b371 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelPrizeRange.java @@ -0,0 +1,21 @@ +package com.ruoyi.game.wheel.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import java.math.BigDecimal; + +@Data +@TableName("game_wheel_prize_range") +public class GameWheelPrizeRange { + @TableId(type = IdType.AUTO) + private Long id; + @TableField("game_type") + private String gameType; + private BigDecimal winLow; + private BigDecimal winHigh; + private Long ornamentId; + private Integer sort; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelRewardMatrix.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelRewardMatrix.java new file mode 100644 index 0000000..6b85c75 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelRewardMatrix.java @@ -0,0 +1,67 @@ +package com.ruoyi.game.wheel.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 疯狂转盘奖励矩阵(方案B:完全自定义饰品)。 + *

+ * 10 档下注额 × 3 内圈色 × 4 外圈符号 = 120 条,每条绑定一件完全自定义饰品。 + * 价值 prizeValue = betAmount × multiplier(自动计算); + * 名称/品质/磨损/图片为自定义属性,不受倍率影响。 + */ +@Data +@TableName("game_wheel_reward_matrix") +public class GameWheelRewardMatrix { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 下注额层:5/10/20/50/100/200/500/1000/2000/5000 */ + @TableField("bet_amount") + private Integer betAmount; + + /** 内圈颜色:gold/red/purple */ + private String color; + + /** 外圈符号:A/B/C/D */ + private String symbol; + + /** 倍率(后台可配;同一 color+symbol 的 12 个倍率跨下注额共用) */ + private Integer multiplier; + + /** 价值 = 下注额 × 倍率(自动计算,展示/发放记录用) */ + @TableField("prize_value") + private BigDecimal prizeValue; + + /** 饰品名称(自定义) */ + @TableField("prize_name") + private String prizeName; + + /** 品质等级(自定义,对应 tt_ornaments_level.id) */ + @TableField("prize_grade") + private Integer prizeGrade; + + /** 磨损度(自定义) */ + @TableField("prize_exterior") + private String prizeExterior; + + /** 饰品图片(自定义) */ + @TableField("prize_image") + private String prizeImage; + + /** 状态:0 停用 1 启用 */ + private String status; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelRound.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelRound.java new file mode 100644 index 0000000..d9f839b --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelRound.java @@ -0,0 +1,48 @@ +package com.ruoyi.game.wheel.domain; + + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.game.wheel.domain.handler.WheelRoundPhaseTimesTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "game_wheel_round", autoResultMap = true) +public class GameWheelRound implements Serializable { + + private Long id; + private String date; + private Long roundId; + /** 全局唯一期号 = date×100000 + 当日序号,业务主键(替代 date+roundId 复合键) */ + private Long periodNo; + private LocalDateTime startTime; + private LocalDateTime endTime; + @TableField(typeHandler = WheelRoundPhaseTimesTypeHandler.class) + private List phaseTimes; + private String symbols; + /** 本轮内圈24色随机排布快照(外圈 symbols 为固定布局) */ + private String innerColors; + // 下面的字段只做统计用,不参与业务逻辑 + private BigDecimal totalBet; + private BigDecimal totalWin; + // 旋转的结果,包含中间旋转结果,每个结果用`,`分割。如S2,6,9,表示命中额外抽奖两次,6,9,表示命中额外抽奖一次 + private String result; + private Integer isManual; + private String status; + + private LocalDateTime create_time; + private LocalDateTime update_time; +} \ No newline at end of file diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelRoundBet.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelRoundBet.java new file mode 100644 index 0000000..7e3b55a --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelRoundBet.java @@ -0,0 +1,54 @@ +package com.ruoyi.game.wheel.domain; + + +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "game_wheel_round_bet") +public class GameWheelRoundBet implements Serializable { + + private Long id; + private Long roundId; + /** 全局唯一期号 = date×100000 + 当日序号,业务主键(查询/结算/判重单列定位) */ + private Long periodNo; + /** 所属日期(yyyyMMdd):仅作按天统计,唯一定位改用 periodNo */ + private String date; + private Long userId; + private BigDecimal bet; + private String betSymbol; + /** 下注内圈颜色 gold/red/purple */ + private String color; + private BigDecimal win; + private Integer settled; // 0=等决定 1=已结算 + /** + * 是否走了"退本金"(0=否 1=是)。 + * settled 只有两态(未结算/已结算),发过奖和退过款在库里长得一样, + * 事后无法逐注区分;退款时额外打这个标记,便于对账与客服核对。 + */ + private Integer refunded; + /** 决策池 real/robot/blank/promo(落库时定池,不受下注后用户类型变更影响) */ + private String poolType; + /** 下注时快照倍率(防中途改配置影响已下注) */ + private Integer multiplier; + /** 命中奖品矩阵行ID(game_wheel_reward_matrix.id)快照 */ + private Long prizeId; + /** 下注时实际扣减的积分额(退款按此原路退回;仅金币扣款时为0) */ + private BigDecimal creditsCut; + + private LocalDateTime createTime; + private LocalDateTime updateTime; +} \ No newline at end of file diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelUser.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelUser.java new file mode 100644 index 0000000..66bc703 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/GameWheelUser.java @@ -0,0 +1,31 @@ +package com.ruoyi.game.wheel.domain; + + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +@TableName(value = "game_wheel_user") +public class GameWheelUser implements Serializable { + + private Long id; + private Long userId; + private BigDecimal totalBet; + private BigDecimal totalWin; + private Integer count; + + private LocalDateTime create_time; + private LocalDateTime update_time; +} \ No newline at end of file diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/handler/WheelRoundPhaseTimesTypeHandler.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/handler/WheelRoundPhaseTimesTypeHandler.java new file mode 100644 index 0000000..98f2fa8 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/domain/handler/WheelRoundPhaseTimesTypeHandler.java @@ -0,0 +1,14 @@ +package com.ruoyi.game.wheel.domain.handler; + +import com.ruoyi.common.typehandler.JsonObjectHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import java.util.List; + + +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class WheelRoundPhaseTimesTypeHandler extends JsonObjectHandler> { +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/enums/SpecialEffectEnum.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/enums/SpecialEffectEnum.java new file mode 100644 index 0000000..57a48f4 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/enums/SpecialEffectEnum.java @@ -0,0 +1,25 @@ +package com.ruoyi.game.wheel.enums; + + +public enum SpecialEffectEnum { + SystemWin("系统赢"), + FreeSpin1("免费旋转1次"), + FreeSpin2("免费旋转2次"), + FreeSpin3("免费旋转3次"); + + private String desc; + + SpecialEffectEnum(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return this.name().toLowerCase(); + } + + + public boolean equals(String other) { + return this.desc.equals(other); + } +} \ No newline at end of file diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/enums/WheelRoundStatusEnum.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/enums/WheelRoundStatusEnum.java new file mode 100644 index 0000000..626a875 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/enums/WheelRoundStatusEnum.java @@ -0,0 +1,14 @@ +package com.ruoyi.game.wheel.enums; + + +public enum WheelRoundStatusEnum { + BETTING, + @Deprecated + BETED, + SPINED; + + @Override + public String toString() { + return this.name().toLowerCase(); + } +} \ No newline at end of file diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/ApiGameWheelRoundBetMapper.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/ApiGameWheelRoundBetMapper.java new file mode 100644 index 0000000..eb8b02c --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/ApiGameWheelRoundBetMapper.java @@ -0,0 +1,124 @@ +package com.ruoyi.game.wheel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.wheel.domain.GameWheelRoundBet; +import com.ruoyi.game.wheel.vo.WheelBetRecordVO; +import com.ruoyi.game.wheel.vo.WheelWinBetVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface ApiGameWheelRoundBetMapper extends BaseMapper { + @Select("select * from game_wheel_round_bet where round_id = #{roundId}") + List selectByRoundId(@Param("roundId") Long roundId); + + // 直接按注单自身 date 列过滤(旧 join 写法只限定回合表日期,跨天同 roundId 的旧注单会被误查出) + @Select("select * from game_wheel_round_bet where date = #{date} and round_id = #{roundId}") + List selectByDateAndRoundId(@Param("date") String date, @Param("roundId") Long roundId); + + // 按全局唯一期号查本轮全部注单(结算事实源:DB,而非 Redis 缓存) + // ORDER BY id:保证遍历顺序确定性,背包记录自增 id 顺序与注单 id 顺序一致,回显按下标匹配才不会错位 + @Select("select * from game_wheel_round_bet where period_no = #{periodNo} order by id") + List selectByPeriodNo(@Param("periodNo") Long periodNo); + + @Select("select * from game_wheel_round_bet where round_id = #{roundId} and bet_symbol = #{symbol}") + List selectByRoundIdAndTarget(@Param("roundId") Long roundId, @Param("symbol") String symbol); + + @Select("select * from game_wheel_round_bet where round_id = #{roundId} and user_id = #{uid} limit 1") + GameWheelRoundBet selectByRoundIdAndUid(@Param("roundId") Long roundId, @Param("uid") Long uid); + + /** + * 全服仅展示真实中奖记录;同一期、同用户、同符号、同一实际饰品合并展示。 + */ + @Select("SELECT b.period_no, b.user_id, b.bet, b.bet_symbol, SUM(b.win) AS win, " + + "MAX(r.end_time) AS end_time, MAX(u.nick_name) AS nick_name, MAX(u.avatar) AS avatar, " + + "box.ornaments_level_id AS prize_grade, box.ornament_name AS prize_name, box.image_url AS prize_image, " + + "COUNT(*) AS bet_count " + + "FROM game_wheel_round_bet b " + + "JOIN tt_user u ON b.user_id = u.user_id " + + "JOIN game_wheel_round r ON b.period_no = r.period_no " + + "LEFT JOIN (SELECT box.*, ROW_NUMBER() OVER (PARTITION BY box.period_no, box.user_id ORDER BY box.id) AS rn " + + " FROM tt_box_records box WHERE box.source = 99 AND box.box_name = 'WHEEL') box " + + "ON box.period_no = b.period_no AND box.user_id = b.user_id AND box.rn = " + + "(SELECT COUNT(*) FROM game_wheel_round_bet b2 WHERE b2.period_no = b.period_no AND b2.user_id = b.user_id AND b2.id <= b.id) " + + "WHERE r.status = 'spined' AND r.result IS NOT NULL AND r.result != '' " + + "AND b.win > 0.01 AND u.is_blank_account != 1 AND u.user_type != '03' AND u.status != '1' AND u.del_flag = '0' " + + "GROUP BY b.period_no, b.user_id, b.bet, b.bet_symbol, box.ornaments_level_id, box.ornament_name, box.image_url " + + "ORDER BY MAX(r.end_time) DESC, MAX(b.id) DESC LIMIT #{offset}, #{pageSize}") + List selectWinBetsPage(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize); + + @Select("SELECT COUNT(*) FROM (SELECT 1 FROM game_wheel_round_bet b " + + "JOIN tt_user u ON b.user_id = u.user_id " + + "JOIN game_wheel_round r ON b.period_no = r.period_no " + + "LEFT JOIN (SELECT box.*, ROW_NUMBER() OVER (PARTITION BY box.period_no, box.user_id ORDER BY box.id) AS rn " + + " FROM tt_box_records box WHERE box.source = 99 AND box.box_name = 'WHEEL') box " + + "ON box.period_no = b.period_no AND box.user_id = b.user_id AND box.rn = " + + "(SELECT COUNT(*) FROM game_wheel_round_bet b2 WHERE b2.period_no = b.period_no AND b2.user_id = b.user_id AND b2.id <= b.id) " + + "WHERE r.status = 'spined' AND r.result IS NOT NULL AND r.result != '' " + + "AND b.win > 0.01 AND u.is_blank_account != 1 AND u.user_type != '03' AND u.status != '1' AND u.del_flag = '0' " + + "GROUP BY b.period_no, b.user_id, b.bet, b.bet_symbol, box.ornaments_level_id, box.ornament_name, box.image_url) records") + Long countWinBets(); + + /** + * 个人记录同时展示中奖与安慰奖;同一期、同符号、同一实际饰品合并并累计总价值。 + */ + @Select("SELECT b.period_no, b.user_id, b.bet, b.bet_symbol, SUM(b.win) AS win, " + + "MAX(r.end_time) AS end_time, MAX(u.nick_name) AS nick_name, MAX(u.avatar) AS avatar, " + + "box.ornaments_level_id AS prize_grade, box.ornament_name AS prize_name, box.image_url AS prize_image, " + + "COUNT(*) AS bet_count " + + "FROM game_wheel_round_bet b " + + "JOIN tt_user u ON b.user_id = u.user_id " + + "JOIN game_wheel_round r ON b.period_no = r.period_no " + + "LEFT JOIN (SELECT box.*, ROW_NUMBER() OVER (PARTITION BY box.period_no, box.user_id ORDER BY box.id) AS rn " + + " FROM tt_box_records box WHERE box.source = 99 AND box.box_name = 'WHEEL') box " + + "ON box.period_no = b.period_no AND box.user_id = b.user_id AND box.rn = " + + "(SELECT COUNT(*) FROM game_wheel_round_bet b2 WHERE b2.period_no = b.period_no AND b2.user_id = b.user_id AND b2.id <= b.id) " + + "WHERE r.status = 'spined' AND r.result IS NOT NULL AND r.result != '' AND b.user_id = #{userId} " + + "GROUP BY b.period_no, b.user_id, b.bet, b.bet_symbol, box.ornaments_level_id, box.ornament_name, box.image_url " + + "ORDER BY MAX(r.end_time) DESC, MAX(b.id) DESC LIMIT #{offset}, #{pageSize}") + List selectUserBetsPage(@Param("userId") Long userId, @Param("offset") Integer offset, @Param("pageSize") Integer pageSize); + + @Select("SELECT COUNT(*) FROM (SELECT 1 FROM game_wheel_round_bet b " + + "JOIN game_wheel_round r ON b.period_no = r.period_no " + + "LEFT JOIN (SELECT box.*, ROW_NUMBER() OVER (PARTITION BY box.period_no, box.user_id ORDER BY box.id) AS rn " + + " FROM tt_box_records box WHERE box.source = 99 AND box.box_name = 'WHEEL') box " + + "ON box.period_no = b.period_no AND box.user_id = b.user_id AND box.rn = " + + "(SELECT COUNT(*) FROM game_wheel_round_bet b2 WHERE b2.period_no = b.period_no AND b2.user_id = b.user_id AND b2.id <= b.id) " + + "WHERE r.status = 'spined' AND r.result IS NOT NULL AND r.result != '' AND b.user_id = #{userId} " + + "GROUP BY b.period_no, b.user_id, b.bet, b.bet_symbol, box.ornaments_level_id, box.ornament_name, box.image_url) records") + Long countUserBets(@Param("userId") Long userId); + + /** + * 管理后台投注记录分页查询(按 round_id + user_id 分组,分页基于分组行) + * 用 DATE_FORMAT(create_time) 关联 game_wheel_round.date 避免跨天 round_id 串数据 + */ + @Select("") + List selectBetRecords(@Param("roundId") Long roundId, @Param("userId") Long userId, + @Param("periodNo") String periodNo); +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/ApiGameWheelRoundMapper.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/ApiGameWheelRoundMapper.java new file mode 100644 index 0000000..bd505ab --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/ApiGameWheelRoundMapper.java @@ -0,0 +1,21 @@ +package com.ruoyi.game.wheel.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.wheel.domain.GameWheelRound; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + + +@Mapper +public interface ApiGameWheelRoundMapper extends BaseMapper { + @Select("SELECT MAX(round_id) FROM game_wheel_round where `date` = #{today}") + public Long getTodayLastRoundId(@Param("today") String today); + + default GameWheelRound selectByRoundId(@Param("roundId") Long roundId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GameWheelRound::getRoundId, roundId); + return selectOne(wrapper); + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/ApiGameWheelUserMapper.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/ApiGameWheelUserMapper.java new file mode 100644 index 0000000..4597de0 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/ApiGameWheelUserMapper.java @@ -0,0 +1,21 @@ +package com.ruoyi.game.wheel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.wheel.domain.GameWheelUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.math.BigDecimal; + +@Mapper +public interface ApiGameWheelUserMapper extends BaseMapper { + @Select("select * from game_wheel_user where user_id = #{userId}") + GameWheelUser getByUserId(@Param("userId") Long userId); + + @Update("update game_wheel_user set total_bet = total_bet + #{totalBet}, total_win = total_win + #{totalWin}, count = count + #{count} where user_id = #{userId}") + int updateIncrement(@Param("totalBet") BigDecimal totalBet, + @Param("totalWin") BigDecimal totalWin, + @Param("count") Integer count, @Param("userId") Long userId); +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPoolAdjustmentMapper.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPoolAdjustmentMapper.java new file mode 100644 index 0000000..b0783ab --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPoolAdjustmentMapper.java @@ -0,0 +1,22 @@ +package com.ruoyi.game.wheel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.wheel.domain.GameWheelPoolAdjustment; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; + +/** + * 疯狂转盘奖池调节记录 Mapper + */ +@Mapper +public interface GameWheelPoolAdjustmentMapper extends BaseMapper { + + /** + * 统计指定池的累计调节金额(正数=净注水,负数=净抽水) + */ + @Select("SELECT COALESCE(SUM(adjustment_amount), 0) FROM game_wheel_pool_adjustment WHERE pool_type = #{poolType}") + BigDecimal sumAdjustmentByPoolType(@Param("poolType") String poolType); +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPrizeMapper.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPrizeMapper.java new file mode 100644 index 0000000..91703d8 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPrizeMapper.java @@ -0,0 +1,53 @@ +package com.ruoyi.game.wheel.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import java.math.BigDecimal; +import java.util.Map; + +@Mapper +public interface GameWheelPrizeMapper { + + @Select("SELECT po.id, po.name, po.image_url AS imageUrl, po.grade, " + + "po.exterior_name AS exteriorName, " + + "lv.level_img AS levelImageUrl " + + "FROM game_wheel_prize_range pr " + + "JOIN game_wheel_prize_ornament po ON po.id = pr.ornament_id " + + "LEFT JOIN tt_ornaments_level lv ON lv.id = po.grade " + + "WHERE pr.game_type = 'wheel' " + + " AND #{totalWin} >= pr.win_low AND #{totalWin} <= pr.win_high " + + " AND po.status = '1' " + + "LIMIT 1") + Map findPrizeByWin(@Param("totalWin") BigDecimal totalWin); + + @Select("SELECT po.id, po.name, po.image_url AS imageUrl, po.grade, " + + "po.exterior_name AS exteriorName, " + + "lv.level_img AS levelImageUrl " + + "FROM game_wheel_prize_ornament po " + + "LEFT JOIN tt_ornaments_level lv ON lv.id = po.grade " + + "WHERE po.game_type = 'wheel' " + + " AND po.is_fallback = 1 AND po.status = '1' " + + "ORDER BY RAND() LIMIT 1") + Map findFallbackPrize(); + + @Select("SELECT po.id, po.name, po.image_url AS imageUrl, po.grade, " + + "po.exterior_name AS exteriorName, " + + "lv.level_img AS levelImageUrl " + + "FROM game_wheel_prize_range pr " + + "JOIN game_wheel_prize_ornament po ON po.id = pr.ornament_id " + + "LEFT JOIN tt_ornaments_level lv ON lv.id = po.grade " + + "WHERE pr.game_type = 'wheel' AND po.status = '1' " + + "ORDER BY pr.win_low ASC LIMIT 1") + Map findLowestPrize(); + + @Select("SELECT po.id, po.name, po.image_url AS imageUrl, po.grade, " + + "po.exterior_name AS exteriorName, " + + "lv.level_img AS levelImageUrl " + + "FROM game_wheel_prize_range pr " + + "JOIN game_wheel_prize_ornament po ON po.id = pr.ornament_id " + + "LEFT JOIN tt_ornaments_level lv ON lv.id = po.grade " + + "WHERE pr.game_type = 'wheel' AND po.status = '1' " + + "ORDER BY pr.win_high DESC LIMIT 1") + Map findHighestPrize(); +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPrizeOrnamentMapper.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPrizeOrnamentMapper.java new file mode 100644 index 0000000..3eeba5a --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPrizeOrnamentMapper.java @@ -0,0 +1,8 @@ +package com.ruoyi.game.wheel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.wheel.domain.GameWheelPrizeOrnament; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface GameWheelPrizeOrnamentMapper extends BaseMapper {} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPrizeRangeMapper.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPrizeRangeMapper.java new file mode 100644 index 0000000..fb147a4 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelPrizeRangeMapper.java @@ -0,0 +1,8 @@ +package com.ruoyi.game.wheel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.wheel.domain.GameWheelPrizeRange; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface GameWheelPrizeRangeMapper extends BaseMapper {} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelRewardMatrixMapper.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelRewardMatrixMapper.java new file mode 100644 index 0000000..9fe2612 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/mapper/GameWheelRewardMatrixMapper.java @@ -0,0 +1,21 @@ +package com.ruoyi.game.wheel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.game.wheel.domain.GameWheelRewardMatrix; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +/** + * 疯狂转盘奖励矩阵 Mapper(120 条自定义饰品配置:10 档 × 3 色 × 4 符号)。 + */ +@Mapper +public interface GameWheelRewardMatrixMapper extends BaseMapper { + + /** + * 按内圈色+外圈符号更新倍率和奖品价值(跨所有下注额层)。 + * 配置层倍率变动时联动同步用。 + */ + @Update("UPDATE game_wheel_reward_matrix SET multiplier = #{multiplier}, prize_value = bet_amount * #{multiplier} WHERE color = #{color} AND symbol = #{symbol} AND status = '1'") + int updateMultiplier(@Param("color") String color, @Param("symbol") String symbol, @Param("multiplier") int multiplier); +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/model/cache/WheelRewardPoolCache.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/model/cache/WheelRewardPoolCache.java new file mode 100644 index 0000000..d421bce --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/model/cache/WheelRewardPoolCache.java @@ -0,0 +1,16 @@ +package com.ruoyi.game.wheel.model.cache; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class WheelRewardPoolCache { + /** + * 疯狂转盘奖池缓存 + * totalBet:累计总投注 + * totalWin:累计总返奖 + */ + private BigDecimal totalBet; + private BigDecimal totalWin; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/model/cache/WheelRoundCache.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/model/cache/WheelRoundCache.java new file mode 100644 index 0000000..656b315 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/model/cache/WheelRoundCache.java @@ -0,0 +1,30 @@ +package com.ruoyi.game.wheel.model.cache; + +import com.ruoyi.game.wheel.domain.GameWheelRound; +import com.ruoyi.game.wheel.domain.GameWheelRoundBet; +import lombok.Data; + +import java.io.Serializable; +import java.util.Map; + +@Data +public class WheelRoundCache implements Serializable { + private Long lastRoundId; + private GameWheelRound round; + + private Map betMap; + + /** processResult 写入背包后,保存每个用户对应的 boxRecordId(非自动分解模式) */ + private Map userBoxRecordIdMap; + + public static String betKey(Long roundId, Long userId, String symbol) { + return String.format("%s_%s_%s", roundId, userId, symbol.toLowerCase()); + } + + public static String betKey(GameWheelRoundBet bet) { + if (bet == null) { + return ""; + } + return String.format("%s_%s_%s", bet.getRoundId(), bet.getUserId(), bet.getBetSymbol().toLowerCase()); + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/AdminWheelService.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/AdminWheelService.java new file mode 100644 index 0000000..eef4ea1 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/AdminWheelService.java @@ -0,0 +1,264 @@ +package com.ruoyi.game.wheel.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.game.wheel.cache.WheelRewardPoolCacheRepository; +import com.ruoyi.game.wheel.constants.CacheConstants; +import com.ruoyi.game.wheel.constants.WheelPoolType; +import com.ruoyi.game.wheel.contract.admin.response.QueryWheelGameConfig; +import com.ruoyi.game.wheel.domain.GameWheelGameConfig; +import com.ruoyi.game.wheel.domain.GameWheelPoolAdjustment; +import com.ruoyi.game.wheel.mapper.GameWheelRewardMatrixMapper; +import com.ruoyi.game.wheel.mapper.GameWheelPoolAdjustmentMapper; +import com.ruoyi.game.wheel.model.cache.WheelRewardPoolCache; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +public class AdminWheelService { + + private static final String EVENT_TAG = "幸运转盘"; + + + @Autowired + private CommonWheelService commonWheelService; + + @Autowired + private WheelRewardPoolCacheRepository wheelRewardPoolCacheRepository; + + @Autowired + private GameWheelRewardMatrixMapper rewardMatrixMapper; + + @Autowired + private GameWheelPoolAdjustmentMapper poolAdjustmentMapper; + + @Autowired + private RedisLock redisLock; + + /** 配置合并保存用的解析器。别改成 @Autowired:Boot 4 只装配 tools.jackson 版的 ObjectMapper,注入 Jackson 2 版会启动失败 */ + private static final com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER = + // 关掉未知字段报错,与 Spring MVC 默认的请求体绑定宽松度一致(前端多传字段不该让保存直接 500) + new com.fasterxml.jackson.databind.ObjectMapper() + .configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + /** 单次人工调整金额上限(注水/抽水),防手滑输入天文数字把奖池打飞 */ + private static final BigDecimal MAX_POOL_ADJUST_AMOUNT = new BigDecimal("100000"); + + public AjaxResult getGameConfig() { + GameWheelGameConfig config = commonWheelService.getGameConfig(); + + QueryWheelGameConfig result = new QueryWheelGameConfig(); + result.setConfig(config); + result.setPoolStats(buildPoolStats()); + + return AjaxResult.success(result); + } + + /** + * 奖池统计:真实池的下注/返奖/毛利/RTP + 人工调整 + 含调整的奖池余额。 + * profit 与 rtp 保持毛口径(下注、返奖是历史事实),余额才扣人工调整。 + */ + public Map buildPoolStats() { + Map poolStats = new LinkedHashMap<>(); + for (String pool : WheelPoolType.all()) { + WheelRewardPoolCache cache = wheelRewardPoolCacheRepository.getCache(pool); + BigDecimal profit = cache.getTotalBet().subtract(cache.getTotalWin()); + BigDecimal poolBalance = wheelRewardPoolCacheRepository.getBalance(pool); + Map poolData = new LinkedHashMap<>(); + poolData.put("totalBet", cache.getTotalBet()); + poolData.put("totalWin", cache.getTotalWin()); + poolData.put("profit", profit); + // 人工调整(注水为正/抽水为负)= 含调整余额 − 毛利润 + poolData.put("adjustment", poolBalance.subtract(profit)); + poolData.put("poolBalance", poolBalance); + double rtp = cache.getTotalBet().compareTo(BigDecimal.ZERO) > 0 + ? cache.getTotalWin().divide(cache.getTotalBet(), 4, RoundingMode.HALF_UP).doubleValue() : 0; + poolData.put("rtp", rtp); + poolStats.put(pool, poolData); + } + return poolStats; + } + + /** + * 保存配置:**合并式**(只在现有配置上覆盖请求体里出现的字段)+ **结构性校验**。 + * + * 为什么收原始 JSON:反序列化成 POJO 后无法区分"字段没传"和"字段传了 0", + * 原来的整份覆盖保存会把没传的字段清成 Java 初值(只传 bets 就会清掉倍率/盘面,switchOff 缺失 + * 还会把 wheelMaintenance 静默改回 0 = 解除维护)。合并保存后,缺失字段一律保持旧值。 + * + * 校验不通过直接拒绝、不落库;倍率同步到奖励矩阵表与配置写在同一个事务里(避免"配置写了、矩阵只同步一半")。 + */ + @Transactional(rollbackFor = Exception.class) + public AjaxResult updateGameConfig(String rawJson) { + if (StringUtils.isEmpty(rawJson)) { + return AjaxResult.error("配置内容为空"); + } + GameWheelGameConfig merged; + boolean switchOffPresent; + try { + GameWheelGameConfig current = commonWheelService.getGameConfig(); + switchOffPresent = OBJECT_MAPPER.readTree(rawJson).has("switchOff"); + merged = OBJECT_MAPPER.readerForUpdating(current).readValue(rawJson); + } catch (Exception e) { + log.warn("{} 解析游戏配置失败: {}", EVENT_TAG, e.getMessage()); + return AjaxResult.error("配置格式不正确,请检查字段类型"); + } + String err = commonWheelService.validate(merged); + if (err != null) { + return AjaxResult.error(err); + } + commonWheelService.updateGameConfig(merged, switchOffPresent); + // 联动同步:配置层倍率变动 → 自动更新奖励矩阵表(跨所有下注额层)。 + // validate() 已保证倍率恰好 12 项且颜色/符号/倍率都合法,这里不用再逐项判空 + for (GameWheelGameConfig.MultiplierItem mi : merged.getMultipliers()) { + rewardMatrixMapper.updateMultiplier(mi.getColor(), mi.getSymbol(), mi.getMultiplier()); + } + log.info("{} 配置层倍率已同步到奖励矩阵表", EVENT_TAG); + return AjaxResult.success(); + } + + /** + * 奖池调整(注水/抽水)。入参是原始 Map,所有解析与范围校验都在这里做,避免缺参/非数字直接 500。 + * 全程持 WHEEL_LOCK:让"调整前后余额"快照与并发的注水/结算串行化(否则并发下审计记录自相矛盾)。 + */ + @Transactional + public AjaxResult adjustPool(Map params, String operator) { + if (params == null) { + return AjaxResult.error("参数不能为空"); + } + Object amountObj = params.get("amount"); + if (amountObj == null) { + return AjaxResult.error("请填写调整金额"); + } + BigDecimal adjustmentAmount; + try { + adjustmentAmount = new BigDecimal(amountObj.toString().trim()); + } catch (Exception e) { + return AjaxResult.error("调整金额格式不正确"); + } + if (adjustmentAmount.compareTo(BigDecimal.ZERO) == 0) { + return AjaxResult.error("调整金额不能为0"); + } + // 小数位/单次金额上限:列定义是 DECIMAL(12,2),超过 2 位小数会被静默舍入成"成功但没效果" + if (adjustmentAmount.scale() > 2) { + return AjaxResult.error("调整金额最多两位小数"); + } + if (adjustmentAmount.abs().compareTo(MAX_POOL_ADJUST_AMOUNT) > 0) { + return AjaxResult.error("单次调整金额不能超过 " + MAX_POOL_ADJUST_AMOUNT + " 元"); + } + String poolType = params.get("poolType") == null ? WheelPoolType.REAL : params.get("poolType").toString(); + String reason = params.get("reason") == null ? "" : params.get("reason").toString(); + if (!WheelPoolType.REAL.equals(poolType)) { + return AjaxResult.error("只支持调整真实池(real)"); + } + + if (!redisLock.tryLock(CacheConstants.WHEEL_LOCK, 3, -1, TimeUnit.SECONDS)) { + return AjaxResult.error("游戏正在结算中,请稍后再试"); + } + try { + // 计算调整前后余额(含历史人工调整) + BigDecimal balanceBefore = wheelRewardPoolCacheRepository.getBalance(poolType); + BigDecimal balanceAfter = balanceBefore.add(adjustmentAmount); + // 抽水红线:不能抽成负数。池子为负只该来自"全盘照赔"的正常透支(随后随下注自然回正), + // 人为抽成负数则会让 poolCap 长期为 0、玩家持续空转,故卡在这里。 + if (balanceAfter.compareTo(BigDecimal.ZERO) < 0) { + return AjaxResult.error("抽水金额超过奖池余额,当前余额 " + balanceBefore); + } + + // 保存调整记录 + GameWheelPoolAdjustment record = GameWheelPoolAdjustment.builder() + .poolType(poolType) + .adjustmentAmount(adjustmentAmount) + .balanceBefore(balanceBefore) + .balanceAfter(balanceAfter) + .reason(reason) + .operator(operator) + .createTime(LocalDateTime.now()) + .build(); + poolAdjustmentMapper.insert(record); + + log.info("{} 奖池调整成功: poolType={}, amount={}, operator={}, balanceBefore={}, balanceAfter={}", + EVENT_TAG, poolType, adjustmentAmount, operator, balanceBefore, balanceAfter); + + return AjaxResult.success("奖池调整成功"); + } finally { + redisLock.unlock(CacheConstants.WHEEL_LOCK); + } + } + + /** + * 重置奖池:累计下注/返奖统计清零,并把历史人工调整"反向抵消",使余额归零。 + * + * 审计要求:不再物理删除历史调整记录(历史注水/抽水/历次重置都要能在调整记录页查), + * 而是插入一条反向抵消记录(adjustmentAmount = −Σ历史调整),配合统计清零达成余额 = 0: + * 余额口径 = 累计下注 − 累计返奖 + Σ人工调整 → 0 − 0 + (Σ历史 + 反向抵消) = 0。 + * + * 全程持 WHEEL_LOCK:避免与正在结算/注水的回合并发,导致"Redis 已清零但调整记录还在"的账实不符。 + */ + @Transactional + public AjaxResult resetPool(String poolType, String reason, String operator) { + if (!WheelPoolType.REAL.equals(poolType)) { + return AjaxResult.error("只支持重置真实池(real)"); + } + if (!redisLock.tryLock(CacheConstants.WHEEL_LOCK, 3, -1, TimeUnit.SECONDS)) { + return AjaxResult.error("游戏正在结算中,请稍后再试"); + } + try { + WheelRewardPoolCache cache = wheelRewardPoolCacheRepository.getCache(poolType); + BigDecimal balanceBefore = wheelRewardPoolCacheRepository.getBalance(poolType); + BigDecimal netAdjustment = poolAdjustmentMapper.sumAdjustmentByPoolType(poolType); + BigDecimal offset = netAdjustment == null ? BigDecimal.ZERO : netAdjustment; + + // 统计清零延到事务提交之后再执行:Redis 不参与数据库事务, + // 先清 Redis 再写库一旦 DB 回滚,就会出现"统计没了、调整记录还在"的账实不符。 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + wheelRewardPoolCacheRepository.reset(poolType); + } + }); + + String summary = String.format("【重置奖池】重置前 累计下注 %s / 累计返奖 %s / 人工调整 %s / 余额 %s", + cache.getTotalBet(), cache.getTotalWin(), offset, balanceBefore); + String resetReason = reason == null || reason.isBlank() ? summary : summary + ";备注:" + reason; + if (resetReason.length() > 200) { + resetReason = resetReason.substring(0, 200); + } + poolAdjustmentMapper.insert(GameWheelPoolAdjustment.builder() + .poolType(poolType) + .adjustmentAmount(offset.negate()) // 抵消历史人工调整;历史记录保留可见 + .balanceBefore(balanceBefore) + .balanceAfter(BigDecimal.ZERO) + .reason(resetReason) + .operator(operator) + .createTime(LocalDateTime.now()) + .build()); + + log.info("{} 奖池重置成功: poolType={}, operator={}, 抵消历史调整={}, {}", EVENT_TAG, poolType, operator, offset.negate(), summary); + return AjaxResult.success("奖池已重置(历史调整记录已保留)"); + } finally { + redisLock.unlock(CacheConstants.WHEEL_LOCK); + } + } + + /** + * 获取奖池余额(包含人工调整) + */ + public BigDecimal getPoolBalance(String poolType) { + return wheelRewardPoolCacheRepository.getBalance(poolType); + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/ApiWheelService.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/ApiWheelService.java new file mode 100644 index 0000000..a9166aa --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/ApiWheelService.java @@ -0,0 +1,1880 @@ +package com.ruoyi.game.wheel.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.model.UpdateUserAccountBo; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.DateTimeUtils; +import com.ruoyi.common.utils.MoneyUtil; +import com.ruoyi.common.utils.RandomUtil; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.dto.packSack.DecomposeParam; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.game.wheel.cache.WheelRoundCacheRepository; +import com.ruoyi.game.wheel.constants.CacheConstants; +import com.ruoyi.game.wheel.contract.api.request.DoBetRequest; +import com.ruoyi.game.wheel.contract.api.request.GetHistoryBetRequest; +import com.ruoyi.game.wheel.contract.api.request.GetRoundHistoryRequest; +import com.ruoyi.game.wheel.contract.api.request.GetRoundResultsRequest; +import com.ruoyi.game.wheel.contract.api.response.DoBetResponse; +import com.ruoyi.game.wheel.contract.api.response.GetGameConfigResponse; +import com.ruoyi.game.wheel.contract.api.response.GetRoundHistoryResponse; +import com.ruoyi.game.wheel.contract.api.response.GetRoundInfoResponse; +import com.ruoyi.game.wheel.contract.api.response.GetRoundResultsResponse; +import com.ruoyi.game.wheel.domain.GameWheelGameConfig; +import com.ruoyi.game.wheel.domain.GameWheelRound; +import com.ruoyi.game.wheel.domain.GameWheelRoundBet; +import com.ruoyi.game.wheel.domain.GameWheelRewardMatrix; +import com.ruoyi.game.wheel.constants.WheelPoolType; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.game.wheel.domain.GameWheelUser; +import com.ruoyi.game.wheel.enums.WheelRoundStatusEnum; +import com.ruoyi.game.wheel.mapper.ApiGameWheelRoundBetMapper; +import com.ruoyi.game.wheel.mapper.ApiGameWheelRoundMapper; +import com.ruoyi.game.wheel.vo.WheelWinBetVO; +import com.ruoyi.game.wheel.mapper.ApiGameWheelUserMapper; +import com.ruoyi.game.wheel.mapper.GameWheelRewardMatrixMapper; +import com.ruoyi.game.wheel.model.cache.WheelRoundCache; +import com.ruoyi.game.wheel.cache.WheelRewardPoolCacheRepository; +import com.ruoyi.message.service.MessageService; +import com.ruoyi.user.service.ApiUserPackSackService; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.commons.lang3.time.DateFormatUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Service +@Slf4j +public class ApiWheelService { + + private static final String EVENT_TAG = "幸运转盘"; + private ScheduledExecutorService scheduler; + + private static GameWheelGameConfig gameConfig; + + @Autowired + private RedisLock redisLock; + + @Autowired + private WheelRoundCacheRepository wheelRoundCacheRepository; + + @Autowired + private CommonWheelService commonWheelService; + @Autowired + private ApiGameWheelRoundMapper wheelRoundMapper; + @Autowired + private ApiGameWheelRoundBetMapper wheelRoundBetMapper; + @Autowired + private ApiGameWheelUserMapper wheelUserMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private WheelRewardPoolCacheRepository wheelRewardPoolCacheRepository; + + @Autowired + private MessageService messageService; + + @Autowired + private com.ruoyi.common.core.redis.RedisCache redisCache; + + @Autowired(required = false) + private com.ruoyi.admin.mapper.TtBoxRecordsMapper boxRecordsMapper; + + @Autowired(required = false) + private com.ruoyi.admin.mapper.TtOrnamentsLevelMapper ornamentsLevelMapper; + + @Autowired + private GameWheelRewardMatrixMapper rewardMatrixMapper; + + @Autowired(required = false) + private com.ruoyi.admin.mapper.TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private ISysConfigService sysConfigService; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private ApiUserPackSackService apiUserPackSackService; + + private static final BigDecimal MIN_PRIZE = new BigDecimal("0.01"); + + /** 玩家端分页接口的页大小上限:防单次大页把整表拉出来(历史记录/中奖榜/开奖结果) */ + private static final int MAX_PAGE_SIZE = 100; + + /** 兜底扫描单次处理的注单条数上限:把单次持锁时长压住,剩余交由下次触发继续(幂等) */ + private static final int ABANDONED_SCAN_LIMIT = 200; + + // ===== 内圈玩法颜色编码 ===== + private static final char COLOR_GOLD = 'G'; + private static final char COLOR_RED = 'R'; + private static final char COLOR_PURPLE = 'P'; + + /** color(gold/red/purple) → 显示名称 */ + private String colorName(String color) { + if ("gold".equals(color)) return "金色"; + if ("red".equals(color)) return "红色"; + if ("purple".equals(color)) return "紫色"; + return color; + } + + /** color(gold/red/purple) → 内圈颜色编码 G/R/P,无效返回 '\0' */ + private char colorCharOf(String color) { + if ("gold".equals(color)) return COLOR_GOLD; + if ("red".equals(color)) return COLOR_RED; + if ("purple".equals(color)) return COLOR_PURPLE; + return '\0'; + } + + /** 内圈颜色编码 G/R/P → 颜色名 gold/red/purple,无效返回 null */ + private String colorNameOfChar(char c) { + if (c == COLOR_GOLD) return "gold"; + if (c == COLOR_RED) return "red"; + if (c == COLOR_PURPLE) return "purple"; + return null; + } + + /** 内圈颜色字符串(G/R/P 连续)→ 颜色名数组(gold/red/purple) */ + private List innerColorsToList(String innerColors) { + if (innerColors == null) return Collections.emptyList(); + List list = new ArrayList<>(innerColors.length()); + for (int i = 0; i < innerColors.length(); i++) { + list.add(colorNameOfChar(innerColors.charAt(i))); + } + return list; + } + + /** 外圈符号字符串(A/B/C/D 连续)→ 符号数组 */ + private List outerSymbolsToList(String outerSymbols) { + if (outerSymbols == null) return Collections.emptyList(); + List list = new ArrayList<>(outerSymbols.length()); + for (int i = 0; i < outerSymbols.length(); i++) { + list.add(String.valueOf(outerSymbols.charAt(i))); + } + return list; + } + + /** 生成内圈24色随机排布字符串(G×goldCount + R×redCount + P×purpleCount,每次 shuffle) */ + private String generateInnerColors(GameWheelGameConfig cfg) { + StringBuilder sb = new StringBuilder(cfg.getSegmentCount()); + for (int i = 0; i < cfg.getInnerGoldCount(); i++) sb.append(COLOR_GOLD); + for (int i = 0; i < cfg.getInnerRedCount(); i++) sb.append(COLOR_RED); + for (int i = 0; i < cfg.getInnerPurpleCount(); i++) sb.append(COLOR_PURPLE); + List chars = sb.chars().mapToObj(c -> (char) c).collect(Collectors.toList()); + Collections.shuffle(chars); + StringBuilder result = new StringBuilder(cfg.getSegmentCount()); + chars.forEach(result::append); + return result.toString(); + } + + /** 全局唯一期号 = date(yyyyMMdd)×100000 + 当日序号 */ + private long computePeriodNo(String date, long roundId) { + return Long.parseLong(date) * 100000L + roundId; + } + + /** 本局生效的奖励矩阵快照:newRound 抓一次,局内不再变(后台改动下一局才对外生效) */ + private volatile List matrixSnapshot = Collections.emptyList(); + + private List loadActiveMatrix() { + List list = rewardMatrixMapper.selectList( + new LambdaQueryWrapper() + .eq(GameWheelRewardMatrix::getStatus, "1")); + return list == null ? Collections.emptyList() : list; + } + + /** 本局生效矩阵:优先本局快照,快照尚未建立时回退实时查库 */ + private List currentMatrix() { + List snap = matrixSnapshot; + return snap.isEmpty() ? loadActiveMatrix() : snap; + } + + /** 从奖励矩阵查行:betAmount × color × symbol(优先本局快照,缺失的行-如本局中途新增-再查库兜底) */ + private GameWheelRewardMatrix findRewardRow(int betAmount, String color, String symbol) { + GameWheelRewardMatrix row = currentMatrix().stream() + .filter(m -> m.getBetAmount() != null && m.getBetAmount() == betAmount + && color.equals(m.getColor()) && symbol.equals(m.getSymbol())) + .findFirst().orElse(null); + if (row != null) return row; + return rewardMatrixMapper.selectOne( + new LambdaQueryWrapper() + .eq(GameWheelRewardMatrix::getBetAmount, betAmount) + .eq(GameWheelRewardMatrix::getColor, color) + .eq(GameWheelRewardMatrix::getSymbol, symbol) + .eq(GameWheelRewardMatrix::getStatus, "1") + .last("LIMIT 1")); + } + + /** 从配置查倍率:color × symbol */ + private Integer findMultiplier(String color, String symbol) { + if (gameConfig.getMultipliers() == null) return null; + return gameConfig.getMultipliers().stream() + .filter(m -> m.getColor().equals(color) && m.getSymbol().equals(symbol)) + .map(GameWheelGameConfig.MultiplierItem::getMultiplier) + .findFirst().orElse(null); + } + + /** + * 解析落点索引。新格式为纯整数(如 "5");兼容旧 4 池格式(如 "real:5,robot:10,...",取 real 池落点)。 + * 解析失败返回 -1(无结果)。 + */ + private int parseResultIndex(String str) { + if (StringUtils.isBlank(str)) return -1; + String s = str.trim(); + try { + return Integer.parseInt(s); + } catch (NumberFormatException ignored) { + // 旧格式兼容 + } + for (String token : s.split(",")) { + String[] parts = token.split(":"); + if (parts.length == 2 && "real".equals(parts[0].trim())) { + try { return Integer.parseInt(parts[1].trim()); } catch (NumberFormatException ignored) {} + } + } + return -1; + } + + /** 检查是否处于维护模式(全站维护或转盘维护) */ + public boolean isMaintenanceMode() { + try { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + String wheelMaintenance = sysConfigService.selectConfigByKey("wheelMaintenance"); + return "1".equals(websiteMaintenance) || "1".equals(wheelMaintenance); + } catch (Exception e) { + return false; + } + } + + @PostConstruct + public void init() { + // 创建单线程的定时任务执行器 + this.scheduler = Executors.newSingleThreadScheduledExecutor(); + + // 服务启动:扫描并退款遗留的未结算注单(自愈触发点①) + AsyncManager.me().run(() -> { + // 先初始化配置,防止 newRound 中 gameConfig 为 null 导致 NPE + gameConfig = commonWheelService.getGameConfig(); + settleAbandonedWheelBets(); + newRound(); + // 新局已建立:上一局的残留注单已成孤儿(投注期内重启也能立即退本金,无需等年龄阈值) + settleAbandonedWheelBets(); + }); + } + + @PreDestroy + public void destroy() { + // 关服:强制结算所有未结算注单(自愈触发点④) + try { + settleAbandonedWheelBets(true); + } catch (Exception e) { + log.error("{} 关服兜底结算异常", EVENT_TAG, e); + } + if (scheduler != null) { + scheduler.shutdown(); + } + } + + private boolean getLock() { + // leaseTime=-1 → Redisson 默认租约 + 看门狗自动续期,防长流程锁中途失效 + return redisLock.tryLock(CacheConstants.WHEEL_LOCK, 3, -1, TimeUnit.SECONDS); + } + + private void newRound() { + log.info("{} 开始新的转盘游戏", EVENT_TAG); + boolean created = false; // 是否真的建出了新局:只有建了局才需要排开奖,避免链外链 + + if (!getLock()) { + int delay = gameConfig != null ? gameConfig.getBettingDuration() : 10; + log.info("{} newRound,获取锁失败,{} 秒后重试", EVENT_TAG, delay); + scheduler.schedule(this::newRound, delay, TimeUnit.SECONDS); + return; + } + try { + // 开新局前置结算上一局的遗留注单(自愈触发点②) + try { + settleAbandonedWheelBets(); + } catch (Exception e) { + log.error("{} newRound 前置兜底结算异常", EVENT_TAG, e); + } + while (true) { + // 新的一局,检查更新游戏配置 + gameConfig = commonWheelService.getGameConfig(); + // 奖励矩阵与本局配置同频刷新:后台改动只影响下一局,进行中的这一局对外数据完全不变 + matrixSnapshot = loadActiveMatrix(); + if (gameConfig.isSwitchOff()) { + log.debug("{} 游戏已关闭,{} 秒后重新开始", EVENT_TAG, 10); + wheelRoundCacheRepository.setCache(null); + // 维护期:下注被拦、也不会建新局 → 残留注单永远结算不了,立即退本金 + try { + settleAbandonedWheelBets(true); + } catch (Exception e) { + log.error("{} 维护期兜底扫描异常", EVENT_TAG, e); + } + Thread.sleep(Duration.ofSeconds(10)); + continue; + } + + String today = DateTimeUtils.dateTime(); + WheelRoundCache cache = wheelRoundCacheRepository.getCache(); + // 建局判重:当前局还在投注期就不要建新局。 + // 否则多条调度链(异常重试、玩家轮询兜底触发等)会各建一局,只有最后进缓存的那局会被开奖, + // 其余成为"幽灵局"(永不开奖 → 注单最终按孤儿退本金,平台白丢一局收入)。 + // 不重排自己:正在跑的那条链会在开奖结束后排下一局(轮询兜底也是自愈入口)。 + if (cache != null && cache.getRound() != null + && WheelRoundStatusEnum.BETTING.toString().equals(cache.getRound().getStatus())) { + log.info("{} 当前局 roundId={} 仍在投注期,本次不建新局(避免多链重复建局)", + EVENT_TAG, cache.getRound().getRoundId()); + return; + } + if (cache == null) { + cache = initCache(today); + } else { + // 检查是否跨天 + if (cache.getRound() != null && !today.equals(cache.getRound().getDate())) { + cache.setLastRoundId(0L); + } + } + + cache.setBetMap(new HashMap<>()); + cache.setLastRoundId(cache.getLastRoundId() + 1); + GameWheelRound round = createGameWheelRound(cache.getLastRoundId(), today); + if (round == null) { + continue; + } + cache.setRound(round); + wheelRoundCacheRepository.setCache(cache); + created = true; + break; + } + } catch (Exception e) { + log.error("newRound错误", e); + } finally { + // 只有真的建了局才排开奖:被"当前局仍在投注期"挡回、或处于维护循环时都不排, + // 否则每次多余调用都会凭空续出一条调度链 + if (created) { + int betDur = gameConfig != null ? gameConfig.getBettingDuration() : 30; + scheduler.schedule(this::drawReward, betDur, TimeUnit.SECONDS); + } + redisLock.unlock(CacheConstants.WHEEL_LOCK); + } + + } + + private WheelRoundCache initCache(String today) { + WheelRoundCache cache = new WheelRoundCache(); + Long roundId = wheelRoundMapper.getTodayLastRoundId(today); + if (roundId == null) { + roundId = 0L; + } + cache.setLastRoundId(roundId); + return cache; + } + + private List getAllBets(GameWheelRound round) { + if (round == null || round.getPeriodNo() == null) { + return Collections.emptyList(); + } + // 结算事实源:按 periodNo 从 DB 查,Redis 缓存只做加速/展示,缓存丢失不影响结算 + return wheelRoundBetMapper.selectByPeriodNo(round.getPeriodNo()); + } + + /** + * 展示用注单读取:结算前实时读库(下注仍在变);结算后走 Redis 缓存。 + * 高并发轮询 last_round_info 时避免每个人都查一期注单。结算用 getAllBets(不走缓存,保证准确)。 + */ + private List getRoundBets(GameWheelRound round) { + if (round == null || round.getPeriodNo() == null) { + return Collections.emptyList(); + } + if ("betting".equals(round.getStatus())) { + return wheelRoundBetMapper.selectByPeriodNo(round.getPeriodNo()); + } + String key = CacheConstants.WHEEL_ROUND_BETS + round.getPeriodNo(); + Object cached = redisCache.getCacheObject(key); + if (cached instanceof List) { + return (List) cached; + } + List bets = wheelRoundBetMapper.selectByPeriodNo(round.getPeriodNo()); + // ponytail: 一期注单短缓存(5分钟),覆盖开奖/展示期;投注期不走缓存,下注实时可查 + redisCache.setCacheObject(key, bets, 5, TimeUnit.MINUTES); + return bets; + } + + // 进入开奖期 + /** + * 进入开奖期。 + * 续链规则:**只有真正把这一局开出来(drew=true)才排下一局**。 + * 早退(缓存空 / 已开奖 / 状态已翻转)一律不再排 newRound —— 否则每次并发轮询兜底触发 + * 都会凭空多出一条调度链,多条链各建一局 → 幽灵局(永不开奖,注单最终按孤儿退本金)。 + * 断链风险由既有自愈入口兜住:玩家轮询 last_round_info 会在"已过封盘且无结果"时再次触发开奖。 + */ + private void drawReward() { + boolean drew = false; + // 时间守卫:如果调度器提前触发了,重新调度到 bettedTime 再执行 + WheelRoundCache cache = wheelRoundCacheRepository.getCache(); + if (cache != null && cache.getRound() != null) { + Long bettedTime = cache.getRound().getPhaseTimes().get(0); + long now = DateTimeUtils.now(); + if (now < bettedTime) { + long delay = bettedTime - now; + log.info("{} drawReward 提前触发,{} 秒后重新调度", EVENT_TAG, delay); + scheduler.schedule(this::drawReward, delay, TimeUnit.SECONDS); + return; + } + } + + if (!getLock()) { + int delay = gameConfig != null ? gameConfig.getBettedDuration() : 3; + log.info("{} drawReward,获取锁失败,{} 秒后重试", EVENT_TAG, delay); + scheduler.schedule(this::drawReward, delay, TimeUnit.SECONDS); + return; + } + + try { + cache = wheelRoundCacheRepository.getCache(); + if (cache == null || cache.getRound() == null) { + log.warn("{} drawReward: 回合缓存为空,跳过本次开奖,等待下一局重建", EVENT_TAG); + return; + } + GameWheelRound round = cache.getRound(); + + // 防止重复开奖:已经开奖过的直接跳过(兜底触发/调度器/前端轮询可能并发进入) + if (round.getResult() != null && !round.getResult().isEmpty()) { + log.info("{} roundId={} 已经开奖,跳过drawReward", EVENT_TAG, round.getRoundId()); + return; + } + if (!"betting".equals(round.getStatus())) { + log.info("{} roundId={} 状态已为{},跳过drawReward", EVENT_TAG, round.getRoundId(), round.getStatus()); + return; + } + + round.setStatus(WheelRoundStatusEnum.SPINED.toString()); + + // 开奖:单一落点,只由真实池下注(01/02/04/05)决定;机器人/白号下注不入池、不影响落点 + List allBets = getAllBets(round); + BigDecimal totalBet = calculateTotalBet(allBets); + cache.getRound().setTotalBet(totalBet); + + List realBets = allBets.stream() + .filter(b -> WheelPoolType.isReal(b.getPoolType())) + .collect(Collectors.toList()); + BigDecimal realBet = realBets.stream() + .map(GameWheelRoundBet::getBet) + .reduce(BigDecimal.ZERO, BigDecimal::add); + if (realBet.compareTo(BigDecimal.ZERO) > 0) { + // 按局幂等注入:开奖异常/进程中断后兜底会重跑开奖,重复注入会让奖池虚增 + if (!wheelRewardPoolCacheRepository.incrementalBetOnce(WheelPoolType.REAL, round.getPeriodNo(), realBet)) { + log.warn("{} periodNo={} 真实池下注已注入过,跳过重复注入 amount={}", EVENT_TAG, round.getPeriodNo(), realBet); + } + } + int idx = doSpin(round, realBets); + + cache.getRound().setEndTime(LocalDateTime.now().plusSeconds(gameConfig.getAnimationDuration())); + cache.getRound().setResult(String.valueOf(idx)); + + // 进行赔付:所有人按统一落点结算,真实注单入池、机器人/白号独立账本 + this.processResult(round, idx, allBets); + + wheelRoundCacheRepository.setCache(cache); + wheelRoundMapper.updateById(cache.getRound()); + drew = true; + } catch (Exception e) { + log.error("drawReward错误", e); + } finally { + redisLock.unlock(CacheConstants.WHEEL_LOCK); + // 只有"真正开完这一局"才续下一局;早退不再排 newRound(详见方法注释) + if (drew) { + int animDur = gameConfig != null ? gameConfig.getAnimationDuration() : 7; + scheduler.schedule(this::newRound, animDur, TimeUnit.SECONDS); + } + } + } + + + /** + * 按统一落点结算赔付。 + * 方案 B(平台兜底):命中一律按 下注额 × 快照倍率 全额赔付,不做"超上限就不发命中奖"的降级—— + * 上限只用于挑落点(见 doSpin);池子因此可以短暂为负,之后随下注自然回正。 + */ + private void processResult(GameWheelRound round, int resultIdx, List allBets) { + // 防御重复赔付:periodNo 判重 + Long settledCount = wheelRoundBetMapper.selectCount( + new LambdaQueryWrapper() + .eq(GameWheelRoundBet::getPeriodNo, round.getPeriodNo()) + .eq(GameWheelRoundBet::getSettled, 1)); + if (settledCount != null && settledCount > 0) { + log.warn("{} periodNo={} 已存在已结算注单,跳过重复processResult", EVENT_TAG, round.getPeriodNo()); + return; + } + + String innerColors = round.getInnerColors(); + String outerSymbols = round.getSymbols(); + // allBets 由调用方传入(drawReward 已加载),避免重复查库 + + // 批量预加载:奖励矩阵行 / 饰品等级 / 安慰饰品候选,消除逐注 N+1 查询 + Map matrixMap = loadMatrixMap(allBets); + List consolationPrizes = loadConsolationPrizes(); + Map levelMap = loadLevelMap(matrixMap, consolationPrizes); + + // 统一落点的颜色+符号(所有人同一个落点) + char colorChar = innerColors.charAt(resultIdx); + char symbolChar = outerSymbols.charAt(resultIdx); + String cellColor = colorChar == COLOR_GOLD ? "gold" : colorChar == COLOR_RED ? "red" : "purple"; + String cellSymbol = String.valueOf(symbolChar); + + Map userWinMap = new HashMap<>(); + final BigDecimal[] totalWinHolder = {BigDecimal.ZERO}; + final BigDecimal[] realWinHolder = {BigDecimal.ZERO}; + + transactionTemplate.executeWithoutResult(status -> { + List records = new ArrayList<>(); + for (GameWheelRoundBet bet : allBets) { + String pt = bet.getPoolType(); + if (pt == null) continue; + + boolean hit = cellColor.equals(bet.getColor()) && cellSymbol.equals(bet.getBetSymbol()); + if (hit) { + Integer mult = bet.getMultiplier(); + bet.setWin(mult != null ? bet.getBet().multiply(BigDecimal.valueOf(mult)) : BigDecimal.ZERO); + } else { + bet.setWin(MIN_PRIZE); + } + + // CAS 防重复结算:仅当 settled=0 时更新为 settled=1,防止与兜底退款并发导致重复发奖 + int updated = wheelRoundBetMapper.update(null, + new LambdaUpdateWrapper() + .eq(GameWheelRoundBet::getId, bet.getId()) + .eq(GameWheelRoundBet::getSettled, 0) + .set(GameWheelRoundBet::getSettled, 1) + .set(GameWheelRoundBet::getWin, bet.getWin())); + if (updated == 0) { + log.warn("{} betId={} 已被结算/退款,跳过发奖", EVENT_TAG, bet.getId()); + continue; + } + + // CAS 成功,构建背包记录(命中发矩阵饰品,未命中随机安慰饰品) + com.ruoyi.domain.entity.TtBoxRecords record = buildPrizeRecord(bet, hit, matrixMap, consolationPrizes, levelMap); + if (record != null) { + records.add(record); + } + + // 只有真实池下注才计入真实池返奖统计;机器人/白号返奖不入池 + if (WheelPoolType.isReal(pt)) { + realWinHolder[0] = realWinHolder[0].add(bet.getWin()); + } + totalWinHolder[0] = totalWinHolder[0].add(bet.getWin()); + userWinMap.merge(bet.getUserId(), bet.getWin(), BigDecimal::add); + } + + // 批量落库:背包记录批量插入 + 用户统计聚合更新(替代原逐注 insert/update) + batchInsertBoxRecords(records); + userWinMap.forEach((uid, win) -> wheelUserMapper.updateIncrement(BigDecimal.ZERO, win, 0, uid)); + }); + + round.setTotalWin(totalWinHolder[0]); + + // 写入真实池返奖统计(机器人/白号不污染真实池);同样按局幂等,防重复结算重复注入 + if (realWinHolder[0].compareTo(BigDecimal.ZERO) > 0) { + if (!wheelRewardPoolCacheRepository.incrementalWinOnce(WheelPoolType.REAL, round.getPeriodNo(), realWinHolder[0])) { + log.warn("{} periodNo={} 真实池返奖已注入过,跳过重复注入 amount={}", EVENT_TAG, round.getPeriodNo(), realWinHolder[0]); + } + } + + AsyncManager.me().run(() -> sendWheelResultNotice(round, matrixMap, allBets)); + } + + /** + * 疯狂转盘结果通知(过滤机器人用户)。 + * 展示每局消耗的总金额、获得的最高价值饰品及其价值、以及本局获得的总价值。 + * 格式:疯狂转盘本局结果:消耗 X,获得最高价值饰品:饰品名(价值¥Y),本局共获得价值¥Z的饰品。 + */ + private void sendWheelResultNotice(GameWheelRound round, Map matrixMap, List allBets) { + try { + if (allBets.isEmpty()) return; + + List betUserIds = allBets.stream() + .map(GameWheelRoundBet::getUserId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (betUserIds.isEmpty()) return; + + // 查询真实用户(排除机器人) + List realUsers = new LambdaQueryChainWrapper<>(ttUserMapper) + .in(TtUser::getUserId, betUserIds) + .ne(TtUser::getUserType, UserType.ROBOT_USER.getCode()) + .list(); + + // 按 userId 分组注单 + Map> userBetMap = allBets.stream() + .filter(bet -> bet.getUserId() != null) + .collect(Collectors.groupingBy(GameWheelRoundBet::getUserId)); + + for (TtUser realUser : realUsers) { + Long userId = Long.valueOf(realUser.getUserId()); + List userBets = userBetMap.get(userId); + if (userBets == null || userBets.isEmpty()) continue; + + // 计算总消耗 + BigDecimal totalBetAmount = userBets.stream() + .map(bet -> bet.getBet() != null ? bet.getBet() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 计算总获得价值 + BigDecimal totalWinAmount = userBets.stream() + .map(bet -> bet.getWin() != null ? bet.getWin() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 找出价值最高的饰品 + GameWheelRoundBet bestPrizeBet = null; + String bestPrizeName = null; + BigDecimal bestPrizeValue = BigDecimal.ZERO; + + for (GameWheelRoundBet bet : userBets) { + BigDecimal win = bet.getWin() != null ? bet.getWin() : BigDecimal.ZERO; + if (win.compareTo(BigDecimal.ZERO) > 0) { + // 查询该注对应的饰品信息 + String prizeName = getPrizeNameForBet(bet, matrixMap); + if (prizeName != null) { + // 比较价值,记录最高价值的饰品 + if (bestPrizeValue.compareTo(BigDecimal.ZERO) == 0 || + win.compareTo(bestPrizeValue) > 0) { + bestPrizeValue = win; + bestPrizeName = prizeName; + bestPrizeBet = bet; + } + } + } + } + + // 构建消息内容 + String title = MessageCategoryEnum.GAME_WHEEL.getTitle(); + String category = MessageCategoryEnum.GAME_WHEEL.getCode(); + + StringBuilder contentBuilder = new StringBuilder(); + contentBuilder.append("疯狂转盘本局结果:消耗 ") + .append(totalBetAmount.stripTrailingZeros().toPlainString()); + + // 如果有获得饰品,展示最高价值饰品信息 + if (bestPrizeName != null && bestPrizeValue.compareTo(BigDecimal.ZERO) > 0) { + contentBuilder.append(",获得最高价值饰品:") + .append(bestPrizeName) + .append("(价值¥") + .append(bestPrizeValue.stripTrailingZeros().toPlainString()) + .append(")"); + } + + // 展示本局共获得的总价值 + contentBuilder.append(",本局共获得价值¥") + .append(totalWinAmount.stripTrailingZeros().toPlainString()) + .append("的饰品。"); + + String content = contentBuilder.toString(); + messageService.sendNotice(userId, title, content, category); + } + } catch (Exception e) { + log.error("疯狂转盘通知推送失败,error={}", e.getMessage(), e); + } + } + + /** + * 获取某注单对应的饰品名称 + * 优先从快照的 prizeId 查询 matrix 行获取饰品名称,找不到时返回通用文案 + */ + private String getPrizeNameForBet(GameWheelRoundBet bet, Map matrixMap) { + Long prizeId = bet.getPrizeId(); + if (prizeId != null) { + GameWheelRewardMatrix matrixRow = matrixMap.get(prizeId); + if (matrixRow != null && matrixRow.getPrizeName() != null) { + return matrixRow.getPrizeName(); + } + } + // 如果没找到 prizeId,可能是安慰奖,返回通用文案 + return "安慰饰品"; + } + + + + private BigDecimal calculateTotalBet(Collection bets) { + return bets.stream() + .map(GameWheelRoundBet::getBet) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + /** + * 单一落点开奖:按本轮24格盘面算每格总赔付,结合真实奖池余额(bufferRatio)与累计返奖额度(rtp)选中奖格索引。 + * 只吃真实池下注;机器人/白号下注不参与(它们跟同一落点结算但不影响落点)。 + * payout = 命中格的奖励价值(下注额×快照倍率) + 其余格每注0.01安慰奖 + * cap = min(真实奖池余额 × bufferRatio, 累计真实投注 × rtp − 累计真实返奖) + * 谁在限制取决于两者谁更小:奖池余额×bufferRatio 更小则由它保池,否则由累计返奖额度限制。 + * 默认 0.85/0.6 下的分界 = 累计 RTP 62.5%(= (rtp−bufferRatio)/(1−bufferRatio)):低于它由 bufferRatio 限制。 + * 额度允许时单局返奖可以远超本局投注(奖池厚 → 可爆大奖)。 + * 筛选 safe(payout <= cap) 随机选,全超则选最小 + */ + private int doSpin(GameWheelRound round, Collection realBets) { + String innerColors = round.getInnerColors(); + String outerSymbols = round.getSymbols(); + int segCount = gameConfig.getSegmentCount(); + + if (realBets == null || realBets.isEmpty()) { + return RandomUtil.rand(segCount); + } + + BigDecimal[] cellPayouts = new BigDecimal[segCount]; + List noRealHitIndices = new ArrayList<>(); // 该格组合没有任何真实注(开这一格=全体未中,只有安慰奖) + for (int i = 0; i < segCount; i++) { + char colorChar = innerColors.charAt(i); + char symbolChar = outerSymbols.charAt(i); + String color = colorChar == COLOR_GOLD ? "gold" : colorChar == COLOR_RED ? "red" : "purple"; + String symbol = String.valueOf(symbolChar); + + BigDecimal hitPayout = BigDecimal.ZERO; + BigDecimal missPayout = BigDecimal.ZERO; + for (GameWheelRoundBet bet : realBets) { + if (color.equals(bet.getColor()) && symbol.equals(bet.getBetSymbol())) { + Integer mult = bet.getMultiplier(); + if (mult != null) { + hitPayout = hitPayout.add(bet.getBet().multiply(BigDecimal.valueOf(mult))); + } + } else { + missPayout = missPayout.add(MIN_PRIZE); + } + } + cellPayouts[i] = hitPayout.add(missPayout); + if (hitPayout.signum() == 0) noRealHitIndices.add(i); + } + + // 本局发奖上限(保池 + 累计返奖额度取小,详见 currentPayoutCap) + BigDecimal payoutCap = currentPayoutCap(); + + List safeIndices = new ArrayList<>(); + for (int i = 0; i < segCount; i++) { + if (cellPayouts[i].compareTo(payoutCap) <= 0) safeIndices.add(i); + } + if (!safeIndices.isEmpty()) return RandomUtil.choice(safeIndices); + + // 全盘赔付都超过上限(cap 极小 / 额度耗尽 / 池子见底):方案 B —— 平台兜底,照规则赔。 + // 但先尽量避开"有人押、却赔不起"的格子:优先开"没有任何真实注的组合"那一格, + // 这样对玩家就是"本局没中、拿安慰奖",机制自洽;绝不会出现"中了却不按倍率赔"。 + if (!noRealHitIndices.isEmpty()) { + log.warn("{} 全盘赔付超过发奖上限 cap={},优先开无真实注组合的格子({} 个候选)避免'中了赔不起'", + EVENT_TAG, payoutCap, noRealHitIndices.size()); + return RandomUtil.choice(noRealHitIndices); + } + + // 12 个组合都被真实玩家押上,已无处可避:按规则照常赔付。 + // 玩家中了就按 下注额 × 快照倍率 全额赔(池子可能短暂为负,随后随下注自然回正) + int minIdx = 0; + BigDecimal minPayout = cellPayouts[0]; + for (int i = 1; i < segCount; i++) { + if (cellPayouts[i].compareTo(minPayout) < 0) { minPayout = cellPayouts[i]; minIdx = i; } + } + log.warn("{} 全盘赔付均超过发奖上限 cap={}(12个组合都有真实注),按规则赔付最小赔付格 idx={} payout={}", + EVENT_TAG, payoutCap, minIdx, minPayout); + return minIdx; + } + + /** + * 本局发奖上限 = min(真实奖池余额 × bufferRatio, 累计真实投注 × rtp − 累计真实返奖)。 + * 上限不够时按方案 B 由平台兜底照规则赔,池子随后随下注自然回正。 + * 本局投注在封盘时已注入累计投注、返奖在结算后才注入,故与结算读到的状态一致。 + */ + private BigDecimal currentPayoutCap() { + BigDecimal jackpotBalance = wheelRewardPoolCacheRepository.getBalance(WheelPoolType.REAL); + if (jackpotBalance.compareTo(BigDecimal.ZERO) < 0) jackpotBalance = BigDecimal.ZERO; + // 保池上限 = 累计奖池余额 × bufferRatio(bufferRatio 是"最多拿出多少",不是"保留多少") + BigDecimal poolCap = jackpotBalance.multiply(BigDecimal.valueOf(gameConfig.getBufferRatio())); + // 返奖额度上限 = 累计真实投注 × rtp − 累计真实返奖(下限 0,不预支未来) + var poolCache = wheelRewardPoolCacheRepository.getCache(WheelPoolType.REAL); + BigDecimal rtpCap = poolCache.getTotalBet().multiply(BigDecimal.valueOf(gameConfig.getRtp())) + .subtract(poolCache.getTotalWin()).max(BigDecimal.ZERO); + return poolCap.min(rtpCap); + } + + /** 安慰饰品候选(fightLoserPrize 配置解析结果,避免每注重复解析/查询) */ + private static class ConsolationPrize { + Long ornamentId; + String name; + String imageUrl; + Integer grade; + BigDecimal price; + String exterior; + ConsolationPrize(Long ornamentId, String name, String imageUrl, Integer grade) { + this.ornamentId = ornamentId; + this.name = name; + this.imageUrl = imageUrl; + this.grade = grade; + } + } + + /** 批量预加载奖励矩阵行(消除逐注 selectById) */ + private Map loadMatrixMap(List bets) { + List prizeIds = bets.stream() + .map(GameWheelRoundBet::getPrizeId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (prizeIds.isEmpty()) return Collections.emptyMap(); + // 先吃本局快照(与客户端展示、下注解析同一份),快照里没有的(本局中途新增的行)再查库兜底 + Map map = currentMatrix().stream() + .filter(r -> r.getId() != null && prizeIds.contains(r.getId())) + .collect(Collectors.toMap(GameWheelRewardMatrix::getId, r -> r, (a, b) -> a)); + List missing = prizeIds.stream().filter(id -> !map.containsKey(id)).collect(Collectors.toList()); + if (!missing.isEmpty()) { + rewardMatrixMapper.selectBatchIds(missing).forEach(r -> map.put(r.getId(), r)); + } + return map; + } + + /** 批量预加载饰品等级(消除逐注 selectById) */ + private Map loadLevelMap( + Map matrixMap, List consolationPrizes) { + if (ornamentsLevelMapper == null) return Collections.emptyMap(); + Set grades = new HashSet<>(); + matrixMap.values().forEach(r -> { if (r.getPrizeGrade() != null) grades.add(r.getPrizeGrade()); }); + consolationPrizes.forEach(p -> { if (p.grade != null) grades.add(p.grade); }); + if (grades.isEmpty()) return Collections.emptyMap(); + Map map = new HashMap<>(); + ornamentsLevelMapper.selectBatchIds(grades).forEach(lv -> map.put(lv.getId(), lv)); + return map; + } + + /** 预加载安慰饰品候选:fightLoserPrize 配置只读一次、解析一次,并批量补全饰品名称/图片/品质等级 */ + private List loadConsolationPrizes() { + List result = new ArrayList<>(); + try { + String cfg = sysConfigService.selectConfigByKey("fightLoserPrize"); + if (StringUtils.isBlank(cfg)) return result; + cn.hutool.json.JSONArray arr = cn.hutool.json.JSONUtil.parseArray(cfg); + for (int i = 0; i < arr.size(); i++) { + Object item = arr.get(i); + Long ornamentId = null; + String name = null, imageUrl = null; + Integer grade = null; // null=未显式指定,下面从饰品自身 quality 推导 + if (item instanceof cn.hutool.json.JSONObject) { + cn.hutool.json.JSONObject obj = (cn.hutool.json.JSONObject) item; + ornamentId = obj.getLong("id"); + name = obj.getStr("name"); + imageUrl = obj.getStr("imageUrl"); + grade = obj.getInt("grade"); + } else { + ornamentId = cn.hutool.core.convert.Convert.toLong(item); + } + if (ornamentId == null) continue; + result.add(new ConsolationPrize(ornamentId, name, imageUrl, grade)); + } + if (!result.isEmpty() && ttOrnamentMapper != null) { + List ids = result.stream().map(c -> c.ornamentId).distinct().collect(Collectors.toList()); + Map ornMap = new HashMap<>(); + // TtOrnament 主键是 marketHashName,不能用 selectBatchIds(主键);必须按 id 字段查 + ttOrnamentMapper.selectList( + new LambdaQueryWrapper() + .in(TtOrnament::getId, ids)) + .forEach(o -> ornMap.put(o.getId(), o)); + result.forEach(c -> { + TtOrnament orn = ornMap.get(c.ornamentId); + if (orn != null) { + if (c.name == null) c.name = orn.getName(); + if (c.imageUrl == null) c.imageUrl = orn.getImageUrl(); + // 品质等级:配置显式指定用配置;否则取饰品自身 quality(1-5,同经典开箱填货/对战 loserPrize 口径), + // 避免安慰奖一律按默认金色展示,导致蓝色等其它品质饰品显示错误 + if (c.grade == null && orn.getQuality() != null) { + String q = orn.getQuality().trim(); + if (q.matches("[1-5]")) { + c.grade = Integer.parseInt(q); + } + } + if (c.grade == null) c.grade = 1; + c.price = orn.getUsePrice() != null ? orn.getUsePrice() : orn.getPrice(); + c.exterior = orn.getExteriorName(); + } else { + if (c.grade == null) c.grade = 1; + } + }); + } + } catch (Exception e) { + log.error("{} 解析 fightLoserPrize 失败", EVENT_TAG, e); + } + return result; + } + + /** 构建单注背包记录:命中发矩阵饰品,未命中随机安慰饰品(均走预加载缓存,不查库) */ + private TtBoxRecords buildPrizeRecord(GameWheelRoundBet bet, boolean hit, + Map matrixMap, + List consolationPrizes, + Map levelMap) { + if (boxRecordsMapper == null) return null; + + Long ornamentId; + String name = null, imageUrl = null; + BigDecimal price; + Integer grade = 1; + + if (hit) { + GameWheelRewardMatrix row = matrixMap.get(bet.getPrizeId()); + if (row == null) { + log.warn("{} buildPrizeRecord: matrix row not found, prizeId={}", EVENT_TAG, bet.getPrizeId()); + return null; + } + ornamentId = row.getId(); + name = row.getPrizeName(); + imageUrl = row.getPrizeImage(); + // 用快照倍率重算,而非矩阵行当前 prize_value:保证后台中途改倍率只影响下一局,进行中本局不受影响 + price = bet.getBet().multiply(BigDecimal.valueOf(bet.getMultiplier() != null ? bet.getMultiplier() : 0)); + grade = row.getPrizeGrade() != null ? row.getPrizeGrade() : 1; + } else { + if (consolationPrizes.isEmpty()) { + log.warn("{} 安慰饰品配置为空,跳过发奖, betId={}", EVENT_TAG, bet.getId()); + return null; + } + ConsolationPrize cp = consolationPrizes.get(cn.hutool.core.util.RandomUtil.randomInt(consolationPrizes.size())); + ornamentId = cp.ornamentId; + name = cp.name; + imageUrl = cp.imageUrl; + grade = cp.grade; + // 安慰奖=未中奖奖励,固定按 MIN_PRIZE(0.01) 记账且安慰奖饰品不可提货(用户确认的设计口径): + // 记账 0.01 = 分解/入账 0.01 = 平台成本 0.01,三处一致,勿改成饰品 usePrice。 + price = MIN_PRIZE; + } + + return buildBoxRecord(bet, ornamentId, name, imageUrl, price, grade, levelMap); + } + + /** 构建背包记录(命中/安慰共用) */ + private com.ruoyi.domain.entity.TtBoxRecords buildBoxRecord(GameWheelRoundBet bet, Long ornamentId, + String ornamentName, String imageUrl, BigDecimal price, Integer grade, + Map levelMap) { + String levelImg = null; + com.ruoyi.domain.other.TtOrnamentsLevel lv = levelMap.get(grade); + if (lv != null) levelImg = lv.getLevelImg(); + return com.ruoyi.domain.entity.TtBoxRecords.builder() + .userId(bet.getUserId().intValue()) + .holderUserId(bet.getUserId().intValue()) + .boxId(0) + .boxName("WHEEL") + .boxPrice(bet.getBet()) + .periodNo(bet.getPeriodNo()) + .ornamentId(ornamentId) + .ornamentsPrice(price) + .ornamentName(ornamentName) + .imageUrl(imageUrl) + .ornamentsLevelId(grade) + .ornamentLevelImg(levelImg) + .source(TtboxRecordSource.WHEEL_BOX.getCode()) + .status(0) + .isShow(1) + .createTime(new Date()) + .build(); + } + + /** 批量插入背包记录(分批,避免单条 SQL 过长) */ + private void batchInsertBoxRecords(List records) { + if (records == null || records.isEmpty() || boxRecordsMapper == null) return; + int batchSize = 500; + for (int i = 0; i < records.size(); i += batchSize) { + boxRecordsMapper.insertBatch(records.subList(i, Math.min(i + batchSize, records.size()))); + } + } + + private GameWheelRound createGameWheelRound(long roundId, String today) { + + GameWheelRound round = new GameWheelRound(); + round.setRoundId(roundId); + round.setDate(today); + round.setStartTime(LocalDateTime.now()); + round.setPhaseTimes(Arrays.asList( + DateTimeUtils.now() + gameConfig.getBettingDuration(), // 开始封盘 + DateTimeUtils.now() + gameConfig.getBettingDuration() + gameConfig.getBettedDuration()) // 开始开奖 + ); + round.setEndTime(LocalDateTime.now().plusSeconds(gameConfig.getBettingDuration() + + gameConfig.getBettedDuration() + + gameConfig.getAnimationDuration() + )); + round.setSymbols(gameConfig.getOuterSymbols()); + round.setInnerColors(generateInnerColors(gameConfig)); + round.setPeriodNo(computePeriodNo(today, roundId)); + round.setTotalBet(BigDecimal.ZERO); + round.setTotalWin(BigDecimal.ZERO); + round.setResult(""); + round.setIsManual(0); + round.setStatus(WheelRoundStatusEnum.BETTING.toString()); + + try { + wheelRoundMapper.insert(round); + } catch (Exception e) { + log.error("{} 创建新局失败", EVENT_TAG, e); + wheelRoundCacheRepository.setCache(null); + return null; + } + + + return round; + } + + public R getGameConfig(Long uid) { + // 本局生效配置:优先用 static gameConfig(下一局才刷新),保证前端展示的倍率/盘面与下注结算一致; + // gameConfig 未初始化时回退实时查库 + GameWheelGameConfig config = gameConfig != null ? gameConfig : commonWheelService.getGameConfig(); + GetGameConfigResponse resp = new GetGameConfigResponse(); + resp.setSegmentCount(config.getSegmentCount()); + resp.setOuterSymbols(config.getOuterSymbols()); + resp.setBets(config.getBets()); + resp.setInnerColorDist(new GetGameConfigResponse.InnerColorDist( + config.getInnerGoldCount(), config.getInnerRedCount(), config.getInnerPurpleCount())); + if (config.getMultipliers() != null) { + resp.setMultipliers(config.getMultipliers().stream() + .map(m -> new GetGameConfigResponse.Multiplier(m.getColor(), m.getSymbol(), m.getMultiplier())) + .collect(Collectors.toList())); + } + if (uid != null) { + TtUser u = userService.getById(uid); + resp.setMyPoolType(u == null ? null : WheelPoolType.ofUserType(u.getUserType())); + } + // 奖励矩阵(120条)取本局快照:整局内不变,后台改动下一局生效(结算走注单快照,不受影响) + List matrix = currentMatrix(); + if (matrix != null && !matrix.isEmpty()) { + resp.setRewardMatrix(matrix.stream().map(m -> new GetGameConfigResponse.RewardCell( + m.getBetAmount(), m.getColor(), m.getSymbol(), m.getMultiplier(), + m.getPrizeValue(), m.getPrizeName(), m.getPrizeGrade(), + m.getPrizeExterior(), m.getPrizeImage())) + .collect(Collectors.toList())); + } + return R.ok(resp); + } + + // 实时更新ttUser|流水和wheelUser信息 + public R doBet(DoBetRequest request, Long uid) { + if (request.getBet() == null || request.getSymbol() == null || request.getColor() == null) { + return R.fail("参数错误"); + } + GameWheelGameConfig cfg = gameConfig; + if (cfg == null) { + return R.fail("游戏未开始"); + } + if (!cfg.getBets().contains(request.getBet())) { + return R.fail("下注金额错误"); + } + String symbol = request.getSymbol(); + if (!"A".equals(symbol) && !"B".equals(symbol) && !"C".equals(symbol) && !"D".equals(symbol)) { + return R.fail("下注符号错误"); + } + String color = request.getColor(); + char colorChar = colorCharOf(color); + if (colorChar == '\0') { + return R.fail("下注颜色错误"); + } + + // 用户类型校验 + 池判定 + TtUser player = userService.getById(uid); + if (player == null) { + return R.fail("用户不存在"); + } + String poolType = WheelPoolType.ofUserType(player.getUserType()); + if (poolType == null) { + return R.fail("当前用户类型不可参与转盘游戏"); + } + + // 用户级锁:串行化同一用户下注,防连点/重复提交绕过50注上限 + Boolean userLock = redisLock.tryLock(CacheConstants.WHEEL_BET_USER_LOCK + uid, 1, 5, TimeUnit.SECONDS); + if (!Boolean.TRUE.equals(userLock)) { + return R.fail("请求频繁,请稍后重试"); + } + + // 锁外快速预检:减少无效请求进入锁 + // 注意:下面三个早退都在 try/finally 之外,必须显式释放用户锁,否则该用户会被自己的锁卡住 5 秒(租约 5s) + WheelRoundCache preCheck = wheelRoundCacheRepository.getCache(); + if (preCheck == null || preCheck.getRound() == null) { + redisLock.unlock(CacheConstants.WHEEL_BET_USER_LOCK + uid); + return R.fail("游戏未开始"); + } + if (!preCheck.getRound().getStatus().equals(WheelRoundStatusEnum.BETTING.toString())) { + redisLock.unlock(CacheConstants.WHEEL_BET_USER_LOCK + uid); + return R.fail("游戏不在投注期"); + } + + // 50注上限(按 periodNo + userId 统计) + Long myBetCount = wheelRoundBetMapper.selectCount( + new LambdaQueryWrapper() + .eq(GameWheelRoundBet::getPeriodNo, preCheck.getRound().getPeriodNo()) + .eq(GameWheelRoundBet::getUserId, uid)); + if (myBetCount != null && myBetCount >= 50) { + redisLock.unlock(CacheConstants.WHEEL_BET_USER_LOCK + uid); + return R.fail("每轮最多下注50注"); + } + + // 全局锁:串行所有下注,防止 WheelRoundCache 并发 read-modify-write 导致丢失更新 + Boolean lock = getLock(); + if (!Boolean.TRUE.equals(lock)) { + redisLock.unlock(CacheConstants.WHEEL_BET_USER_LOCK + uid); + return R.fail("请求频繁,加锁失败"); + } + try { + // 锁后重新读取 cache,避免使用锁外 stale 数据覆盖结算结果 + WheelRoundCache cache = wheelRoundCacheRepository.getCache(); + if (cache == null || cache.getRound() == null) { + return R.fail("游戏未开始"); + } + if (!cache.getRound().getStatus().equals(WheelRoundStatusEnum.BETTING.toString())) { + return R.fail("游戏不在投注期"); + } + + // 封盘时间强校验:状态要等 drawReward 执行到才翻转,中间那段"已过封盘、还没开奖"的窗口 + // 里状态仍是 betting,若不按时间拦,玩家可以在别人注单已定之后补注并影响落点。 + Long bettedTime = cache.getRound().getPhaseTimes() != null && !cache.getRound().getPhaseTimes().isEmpty() + ? cache.getRound().getPhaseTimes().get(0) : null; + if (bettedTime != null && DateTimeUtils.now() >= bettedTime) { + return R.fail("本局已封盘,请等下一局"); + } + + // 锁内二次校验50注上限(防止并发请求绕过锁外预检) + Long myBetCountLocked = wheelRoundBetMapper.selectCount( + new LambdaQueryWrapper() + .eq(GameWheelRoundBet::getPeriodNo, cache.getRound().getPeriodNo()) + .eq(GameWheelRoundBet::getUserId, uid)); + if (myBetCountLocked != null && myBetCountLocked >= 50) { + return R.fail("每轮最多下注50注"); + } + + // 余额预检查 + BigDecimal betAmount = BigDecimal.valueOf(request.getBet()); + player = userService.getById(uid); + if (player.getAccountAmount() == null + || player.getAccountAmount().compareTo(betAmount) < 0) { + return R.fail(702, "账户余额不足"); + } + + // 快照倍率 + 奖励矩阵行 + Integer multiplier = findMultiplier(color, symbol); + if (multiplier == null) { + return R.fail("倍率配置缺失"); + } + GameWheelRewardMatrix rewardRow = findRewardRow(request.getBet(), color, symbol); + if (rewardRow == null) { + return R.fail("该下注组合未配置奖品,请联系管理员"); + } + Long prizeId = rewardRow.getId(); + + // 连带压积分:updateUserAccount 的 subtractAccountAmount 会把积分压到 min(积分, 扣款后金币)。 + // 实际压减额由 SQL 在执行瞬间按数据库行计算,所以这里不能用户锁外读到的旧快照来算 —— + // 必须放到扣款同一事务里,先对用户行加锁(FOR UPDATE)再算,否则与"积分兑金币/充值"并发时 + // 记录进 credits_cut 的值会与实际不符,退款按记录值返还就等于凭空多退/少退积分。 + final WheelRoundCache betCache = cache; + final String fSymbol = symbol; + final String fColor = color; + final String fPoolType = poolType; + final Integer fMultiplier = multiplier; + final Long fPrizeId = prizeId; + final BigDecimal[] pressedHolder = {BigDecimal.ZERO}; + + UpdateUserAccountBo accountBo = transactionTemplate.execute(status -> { + TtUser locked = ttUserMapper.selectByUserIdForUpdate(uid); + BigDecimal creditsBefore = (locked != null && locked.getAccountCredits() != null) + ? locked.getAccountCredits() : BigDecimal.ZERO; + BigDecimal goldBefore = (locked != null && locked.getAccountAmount() != null) + ? locked.getAccountAmount() : BigDecimal.ZERO; + BigDecimal afterGold = goldBefore.subtract(betAmount); + BigDecimal afterCredits = creditsBefore.min(afterGold).max(BigDecimal.ZERO); + pressedHolder[0] = creditsBefore.subtract(afterCredits); + + // 与其他游戏一致:只扣金币(积分由 subtractAccountAmount 连带压到金币余额),不再额外扣积分 + R updateResult = userService.updateUserAccount(uid.intValue(), + betAmount.negate(), + TtAccountRecordSource.LUCKLY_WHEEL_SPIN); + if (R.isError(updateResult)) { + throw new RuntimeException(updateResult.getMsg()); + } + + // 每注独立一条注单,绝不累加 + GameWheelRoundBet bet = new GameWheelRoundBet(); + bet.setRoundId(betCache.getRound().getRoundId()); + bet.setPeriodNo(betCache.getRound().getPeriodNo()); + bet.setDate(betCache.getRound().getDate()); + bet.setUserId(uid); + bet.setBet(betAmount); + bet.setBetSymbol(fSymbol); + bet.setColor(fColor); + bet.setPoolType(fPoolType); + bet.setMultiplier(fMultiplier); + bet.setPrizeId(fPrizeId); + bet.setCreditsCut(pressedHolder[0]); + bet.setWin(BigDecimal.ZERO); + bet.setSettled(0); + wheelRoundBetMapper.insert(bet); + + if (wheelUserMapper.getByUserId(uid) == null) { + initWheelUser(uid); + } + wheelUserMapper.updateIncrement(bet.getBet(), BigDecimal.ZERO, 1, uid); + return updateResult.getData(); + }); + + return R.ok(new DoBetResponse(betCache.getRound().getRoundId(), request.getBet().toString(), + MoneyUtil.toStr(accountBo.getAccountAmount()), + MoneyUtil.toStr(accountBo.getAccountCredits())) + ); + } catch (Exception e) { + log.error("wheel doBet 错误", e); + return R.fail("内部错误,请联系管理员"); + } finally { + redisLock.unlock(CacheConstants.WHEEL_LOCK); + redisLock.unlock(CacheConstants.WHEEL_BET_USER_LOCK + uid); + } + } + + private void initWheelUser(Long uid) { + GameWheelUser user = new GameWheelUser(); + user.setUserId(uid); + user.setTotalBet(BigDecimal.ZERO); + user.setTotalWin(BigDecimal.ZERO); + user.setCount(0); + wheelUserMapper.insert(user); + } + + public R getLastRoundInfo(Long uid) { + WheelRoundCache cache = wheelRoundCacheRepository.getCache(); + if (cache == null || cache.getRound() == null) { + return R.fail("游戏未开始"); + } + // 兜底:倒计时已过但调度器还没触发开奖 → 主动触发,防止前端跳过spined动画 + GameWheelRound round = cache.getRound(); + if ("betting".equals(round.getStatus())) { + long now = DateTimeUtils.now(); + Long bettedTime = round.getPhaseTimes() != null && !round.getPhaseTimes().isEmpty() + ? round.getPhaseTimes().get(0) : null; + if (bettedTime != null && now >= bettedTime && (round.getResult() == null || round.getResult().isEmpty())) { + log.info("{} 兜底触发drawReward, roundId={}, now={}, bettedTime={}", EVENT_TAG, round.getRoundId(), now, bettedTime); + drawReward(); + // drawReward 内部对新的缓存实例开奖并写回 Redis,此处需重新拉取,避免使用开奖前的过期缓存导致本次返回无结果 + WheelRoundCache refreshed = wheelRoundCacheRepository.getCache(); + if (refreshed != null && refreshed.getRound() != null) { + cache = refreshed; + round = refreshed.getRound(); + } + } + } + return R.ok(buildRoundResp(cache, round, uid)); + } + + private GetRoundInfoResponse buildRoundResp(WheelRoundCache cache, GameWheelRound round, Long uid) { + GetRoundInfoResponse resp = new GetRoundInfoResponse(); + resp.setRoundId(round.getRoundId()); + resp.setPeriodNo(round.getPeriodNo()); + resp.setServerTime(DateTimeUtils.now()); + // 游戏关闭/维护中:前端靠这个字段提示"维护中",之前一直没返回,导致关服后客户端提示"当前不在下注期" + resp.setIsMaintenance(isMaintenanceMode()); + resp.setStartTime(DateTimeUtils.toSeconds(round.getStartTime())); + resp.setEndTime(DateTimeUtils.toSeconds(round.getEndTime())); + resp.setBettedTime(round.getPhaseTimes().get(0)); + resp.setSpinTime(round.getPhaseTimes().get(1)); + resp.setStatus(round.getStatus()); + resp.setInnerColors(innerColorsToList(round.getInnerColors())); + + // ===== 游戏配置 ===== + GameWheelGameConfig config = gameConfig != null ? gameConfig : commonWheelService.getGameConfig(); + // 外圈布局取本局快照(与 innerColors 同口径):运营改配置只影响下一局,不改进行中这一局的盘面 + String outerSymbols = StringUtils.isNotEmpty(round.getSymbols()) ? round.getSymbols() : config.getOuterSymbols(); + resp.setOuterSymbols(outerSymbolsToList(outerSymbols)); + resp.setBets(config.getBets()); + // 奖励矩阵取本局快照:整局内对客户端完全不变,后台改动下一局生效 + List matrix = currentMatrix(); + if (matrix != null && !matrix.isEmpty()) { + // 查品质图片(prizeGrade → TtOrnamentsLevel.levelImg) + Map levelImgMap = new HashMap<>(); + if (ornamentsLevelMapper != null) { + Set grades = matrix.stream() + .map(GameWheelRewardMatrix::getPrizeGrade) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (!grades.isEmpty()) { + ornamentsLevelMapper.selectBatchIds(grades) + .forEach(lv -> levelImgMap.put(lv.getId(), lv.getLevelImg())); + } + } + List cells = new ArrayList<>(matrix.size()); + for (GameWheelRewardMatrix m : matrix) { + GetRoundInfoResponse.RewardCell cell = new GetRoundInfoResponse.RewardCell(); + cell.setBetAmount(m.getBetAmount()); + cell.setColor(m.getColor()); + cell.setSymbol(m.getSymbol()); + cell.setMultiplier(m.getMultiplier()); + cell.setPrizeValue(m.getPrizeValue()); + cell.setPrizeName(m.getPrizeName()); + cell.setPrizeGrade(m.getPrizeGrade()); + cell.setPrizeExterior(m.getPrizeExterior()); + cell.setPrizeImage(m.getPrizeImage()); + cell.setPrizeLevelImg(m.getPrizeGrade() != null ? levelImgMap.get(m.getPrizeGrade()) : null); + cells.add(cell); + } + resp.setRewardMatrix(cells); + } + + // ===== 统一落点的开奖结果(所有玩家同一个落点) ===== + TtUser user = userService.getById(uid); + String poolType = (user != null) ? WheelPoolType.ofUserType(user.getUserType()) : null; + resp.setMyPoolType(poolType); + int idx = parseResultIndex(round.getResult()); + if (idx >= 0) { + resp.setResultIndex(idx); + if (round.getInnerColors() != null && idx >= 0 && idx < round.getInnerColors().length()) { + char cc = round.getInnerColors().charAt(idx); + resp.setResultColor(cc == COLOR_GOLD ? "gold" : cc == COLOR_RED ? "red" : cc == COLOR_PURPLE ? "purple" : null); + } + if (round.getSymbols() != null && idx >= 0 && idx < round.getSymbols().length()) { + resp.setResultSymbol(String.valueOf(round.getSymbols().charAt(idx))); + } + } + + // ===== 当前用户本轮下注明细 ===== + List allBets = getRoundBets(round); + List userBetList = allBets.stream() + .filter(b -> b.getUserId().equals(uid)) + .sorted(Comparator.comparing(GameWheelRoundBet::getId)) + .collect(Collectors.toList()); + + // 批量加载奖励矩阵(饰品信息) + Map matrixMap = loadMatrixMap(userBetList); + + // 批量加载背包记录(按 periodNo 精确匹配 + 自增 id 排序,与注单下注顺序严格一致) + Map boxMap = new HashMap<>(); + if (!userBetList.isEmpty() && boxRecordsMapper != null && round.getPeriodNo() != null) { + List boxes = boxRecordsMapper.selectList( + new LambdaQueryWrapper() + .eq(com.ruoyi.domain.entity.TtBoxRecords::getPeriodNo, round.getPeriodNo()) + .eq(com.ruoyi.domain.entity.TtBoxRecords::getUserId, uid.intValue()) + .orderByAsc(com.ruoyi.domain.entity.TtBoxRecords::getId)); + log.info("{} periodNo={}, userId={}, 匹配到背包记录数={}", EVENT_TAG, round.getPeriodNo(), uid, boxes.size()); + for (int i = 0; i < Math.min(boxes.size(), userBetList.size()); i++) { + boxMap.put(userBetList.get(i).getId(), boxes.get(i)); + } + } + + // 批量加载奖品对应饰品,回显时补 exteriorName(背包记录不落库该字段) + // 命中奖的磨损度来自矩阵 prize_exterior;未命中(安慰)奖按 ornamentId 查饰品补 exteriorName + Map prizeOrnamentMap = new HashMap<>(); + if (!boxMap.isEmpty() && ttOrnamentMapper != null) { + List prizeOrnIds = boxMap.values().stream() + .map(com.ruoyi.domain.entity.TtBoxRecords::getOrnamentId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (!prizeOrnIds.isEmpty()) { + ttOrnamentMapper.selectList( + new LambdaQueryWrapper() + .in(TtOrnament::getId, prizeOrnIds)) + .forEach(o -> prizeOrnamentMap.put(o.getId(), o)); + } + } + + // 本池开奖结果(用于判断每注是否命中) + String winColor = resp.getResultColor(); + String winSymbol = resp.getResultSymbol(); + + List userBets = new ArrayList<>(); + List winResults = new ArrayList<>(); + // 下注汇总(按颜色+符号聚合,统计次数和总额) + Map userBetMap = new LinkedHashMap<>(); + for (GameWheelRoundBet bet : userBetList) { + // 是否命中(hit=true 中奖,hit=false 安慰奖) + boolean hit = winColor != null && winColor.equals(bet.getColor()) + && winSymbol != null && winSymbol.equals(bet.getBetSymbol()); + + // 下注聚合 + String betKey = bet.getColor() + ":" + bet.getBetSymbol(); + GetRoundInfoResponse.UserBet ub = userBetMap.computeIfAbsent(betKey, k -> { + GetRoundInfoResponse.UserBet u = new GetRoundInfoResponse.UserBet(); + u.setColor(bet.getColor()); + u.setSymbol(bet.getBetSymbol()); + u.setCount(0); + u.setTotalBet(BigDecimal.ZERO); + return u; + }); + ub.setCount(ub.getCount() + 1); + ub.setTotalBet(ub.getTotalBet().add(bet.getBet() != null ? bet.getBet() : BigDecimal.ZERO)); + + // 发奖明细(每注一条,命中=矩阵饰品,未命中=安慰饰品,用 hit 区分) + com.ruoyi.domain.entity.TtBoxRecords box = boxMap.get(bet.getId()); + GameWheelRewardMatrix matrixRow = matrixMap.get(bet.getPrizeId()); + + GetRoundInfoResponse.WinResult wr = new GetRoundInfoResponse.WinResult(); + wr.setColor(bet.getColor()); + wr.setSymbol(bet.getBetSymbol()); + wr.setHit(hit); + wr.setBoxRecordId(box != null ? box.getId().longValue() : null); + if (hit) { + // 命中:饰品信息从矩阵读(矩阵是命中奖品的配置源) + wr.setPrizeName(matrixRow != null ? matrixRow.getPrizeName() : (box != null ? box.getOrnamentName() : null)); + wr.setPrizeImage(matrixRow != null ? matrixRow.getPrizeImage() : (box != null ? box.getImageUrl() : null)); + wr.setPrizeGrade(matrixRow != null ? matrixRow.getPrizeGrade() : (box != null ? box.getOrnamentsLevelId() : null)); + wr.setPrizeLevelImg(box != null ? box.getOrnamentLevelImg() : null); + wr.setPrizeExterior(matrixRow != null ? matrixRow.getPrizeExterior() : null); + wr.setPrizeValue(box != null ? box.getOrnamentsPrice() : (matrixRow != null ? matrixRow.getPrizeValue() : null)); + } else { + // 未命中:直接用背包记录字段(buildPrizeRecord已写入安慰奖完整信息) + wr.setPrizeName(box != null ? box.getOrnamentName() : "安慰饰品"); + wr.setPrizeImage(box != null ? box.getImageUrl() : null); + wr.setPrizeGrade(box != null ? box.getOrnamentsLevelId() : null); + wr.setPrizeLevelImg(box != null ? box.getOrnamentLevelImg() : null); + // 磨损度:背包记录不落库 exteriorName,按 ornamentId 查饰品补全;印花等无磨损退化为"无涂装" + String exterior = null; + if (box != null) { + com.ruoyi.domain.entity.TtOrnament orn = prizeOrnamentMap.get(box.getOrnamentId()); + exterior = orn != null ? orn.getExteriorName() : null; + if (StringUtils.isBlank(exterior)) exterior = "无涂装"; + } + wr.setPrizeExterior(exterior); + wr.setPrizeValue(box != null ? box.getOrnamentsPrice() : MIN_PRIZE); + } + winResults.add(wr); + } + userBets.addAll(userBetMap.values()); + resp.setUserBets(userBets); + resp.setWinResult(winResults); + + // ===== 当前用户本轮总返奖 ===== + BigDecimal totalWin = userBetList.stream() + .map(b -> b.getWin() != null ? b.getWin() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + resp.setTotalWin(MoneyUtil.toStr(totalWin)); + + return resp; + } + + public R getHistoryBetInfo(GetHistoryBetRequest request, Long userId) { + if (request.getPageNum() == null || request.getPageNum() <= 0) { + request.setPageNum(1); + } + if (request.getPageSize() == null || request.getPageSize() <= 0) { + request.setPageSize(10); + } else if (request.getPageSize() > MAX_PAGE_SIZE) { + request.setPageSize(MAX_PAGE_SIZE); // 防大页拉全表 + } + + int offset = (request.getPageNum() - 1) * request.getPageSize(); + List winBets = wheelRoundBetMapper.selectUserBetsPage(userId, offset, request.getPageSize()); + Long total = wheelRoundBetMapper.countUserBets(userId); + + return R.ok(buildRoundHistoryResponse(winBets, total)); + } + + // ======================== 分解/保留 ======================== + + @Deprecated + public Map decidePrize(Long userId, String action) { + // 已废弃:奖品在开奖时直接写入背包,无需再做决策 + Map result = new HashMap<>(); + result.put("msg", "奖品已在背包中"); + return result; + } + + /** + * 兜底结算:处理因开奖异常/结算事务回滚导致的 settled=0 残留注单。 + * 注单在 doBet 时即以 settled=0 落库,正常开奖后 processResult 会更新为 settled=1; + * 残留 settled=0 说明该注单从未被成功结算(用户未获得任何赔付),此处退还本金。 + */ + public void settleAbandonedWheelBets() { + settleAbandonedWheelBets(false); + } + + /** + * 兜底结算:有开奖结果则按结果结算发奖,无结果则退本金。 + * 注单在 doBet 时即以 settled=0 落库,正常开奖后 processResult 会更新为 settled=1; + * 残留 settled=0 说明该注从未被成功结算。 + * @param forceAll true=扫描所有 settled=0 记录(维护强制结算用);false=只扫描超过一局时长、或已不属于当前局的异常记录 + */ + public void settleAbandonedWheelBets(boolean forceAll) { + // 加全局锁串行化兜底结算:防并发补开奖导致 round.result 被覆盖 + // (Redisson 可重入,newRound 内调用不会死锁;拿不到锁则跳过,下次触发再试) + if (!getLock()) { + log.warn("{} 兜底结算获取锁失败,跳过本次", EVENT_TAG); + return; + } + try { + settleAbandonedWheelBetsLocked(forceAll); + } finally { + redisLock.unlock(CacheConstants.WHEEL_LOCK); + } + } + + /** + * 兜底扫描的注单年龄阈值(秒):注单比它还老,说明所在回合早已结束(一局最长 + * bettingDuration + bettedDuration + animationDuration),可以结算/退款。 + * 按配置推导而非固定 1 小时:投注期内重启时 @PreDestroy 会跳过该局, + * 旧阈值要等满 1 小时才退,玩家以为钱没了。 + */ + private long abandonedBetAgeSeconds() { + int roundSec = gameConfig != null + ? gameConfig.getBettingDuration() + gameConfig.getBettedDuration() + gameConfig.getAnimationDuration() + : 60; + return roundSec + 60L; + } + + private void settleAbandonedWheelBetsLocked(boolean forceAll) { + // 当前局(Redis 里唯一被驱动的局):不属于它的残留注单,所在回合必已死亡 + // —— 每局只由 Redis 当前局结算,旧局不会再有人碰它,可立即处理,无需等年龄阈值。 + WheelRoundCache currentCache = wheelRoundCacheRepository.getCache(); + Long currentPeriodNo = currentCache != null && currentCache.getRound() != null + ? currentCache.getRound().getPeriodNo() : null; + + var queryWrapper = new LambdaQueryWrapper() + .eq(GameWheelRoundBet::getSettled, 0); + if (!forceAll) { + LocalDateTime deadline = LocalDateTime.now().minusSeconds(abandonedBetAgeSeconds()); + if (currentPeriodNo == null) { + queryWrapper.lt(GameWheelRoundBet::getCreateTime, deadline); + } else { + final Long cp = currentPeriodNo; + queryWrapper.and(w -> w.lt(GameWheelRoundBet::getCreateTime, deadline) + .or().ne(GameWheelRoundBet::getPeriodNo, cp)); + } + } + // 单次扫描条数上限:兜底扫描全程持 WHEEL_LOCK 且逐注一个事务,积压上千注时会长时间阻塞下注与开奖。 + // 限量处理 + 下次触发继续,把单次持锁时长控制在可控范围(结算/退款都是幂等的,分批安全)。 + queryWrapper.orderByAsc(GameWheelRoundBet::getId).last("LIMIT " + ABANDONED_SCAN_LIMIT); + List abandonedBets = wheelRoundBetMapper.selectList(queryWrapper); + + if (abandonedBets.isEmpty()) { + return; + } + + log.warn("发现{}条异常未结算注单(单次最多处理{}条),执行兜底结算, forceAll={}", + abandonedBets.size(), ABANDONED_SCAN_LIMIT, forceAll); + + // 按 periodNo 分组,整局统一处理(能开奖就全结算 / 开不出才全退款) + Map> byPeriod = abandonedBets.stream() + .filter(b -> b.getPeriodNo() != null) + .collect(Collectors.groupingBy(GameWheelRoundBet::getPeriodNo)); + for (Map.Entry> e : byPeriod.entrySet()) { + try { + // 没有当前局时(维护中/尚未建局)强制扫描一律按孤儿处理:此时不会有局再结算它们 + boolean orphan = currentPeriodNo == null + ? forceAll : !currentPeriodNo.equals(e.getKey()); + settleOrRefundRound(e.getKey(), e.getValue(), orphan); + } catch (Exception ex) { + log.error("{} 兜底结算异常, periodNo={}", EVENT_TAG, e.getKey(), ex); + } + } + + // periodNo 为 null 的异常注单(无法定位局)直接退本金 + List noPeriod = abandonedBets.stream() + .filter(b -> b.getPeriodNo() == null) + .collect(Collectors.toList()); + if (!noPeriod.isEmpty()) { + refundBets(noPeriod); + } + } + + /** + * 单局兜底:已有 result 则按结果结算发奖;无 result 直接退本金(不做补开奖)。 + * + * @param orphan true=该局已不属于当前局(回合必已死亡),可无视"仍在投注期"的跳过判断 + */ + private void settleOrRefundRound(Long periodNo, List bets, boolean orphan) { + GameWheelRound round = wheelRoundMapper.selectOne( + new LambdaQueryWrapper().eq(GameWheelRound::getPeriodNo, periodNo)); + if (round == null) { + log.warn("{} periodNo={} 局不存在,退本金", EVENT_TAG, periodNo); + refundBets(bets); + return; + } + + // 仍在投注期的局(未到封盘时间)→ 跳过,等其自然开奖,避免维护 forceAll 提前开奖 + // 孤儿局(不属于当前局)例外:它的回合已经不可能再开奖,留着只会让玩家等 + if (!orphan && "betting".equals(round.getStatus())) { + Long bettedTime = round.getPhaseTimes() != null && !round.getPhaseTimes().isEmpty() + ? round.getPhaseTimes().get(0) : null; + if (bettedTime != null && DateTimeUtils.now() < bettedTime) { + log.info("{} periodNo={} 仍在投注期,跳过兜底(等自然开奖)", EVENT_TAG, periodNo); + return; + } + } + + // 该局已有已结算注单 → 之前已结算过,残留的 settled=0 属异常数据,直接退款(防 periodNo 判重整局跳过) + Long settledCount = wheelRoundBetMapper.selectCount( + new LambdaQueryWrapper() + .eq(GameWheelRoundBet::getPeriodNo, periodNo) + .eq(GameWheelRoundBet::getSettled, 1)); + if (settledCount != null && settledCount > 0) { + log.warn("{} periodNo={} 已有已结算注单,残留 settled=0 注单退本金", EVENT_TAG, periodNo); + refundBets(bets); + return; + } + + int resultIdx = parseResultIndex(round.getResult()); + if (resultIdx >= 0) { + // 已有明确结果 → 直接按结果结算(结果已定,重结算即可) + // 结算发奖(processResult 内部有 periodNo 判重防并发重复) + processResult(round, resultIdx, getAllBets(round)); + wheelRoundMapper.updateById(round); + } else { + // 无结果:drawReward 未生成结果即崩溃/未执行 → 直接退本金。 + // ponytail: 不再重跑随机开奖(redoSpin),避免二次随机结果 + 奖池 bet/win 二次注入的风险 + log.warn("{} periodNo={} 无开奖结果,退本金", EVENT_TAG, periodNo); + refundBets(bets); + } + } + + /** + * 逐注退款(无法开奖时的兜底)。 + */ + private void refundBets(List bets) { + for (GameWheelRoundBet bet : bets) { + try { + // 幂等守卫 + 退款包裹在同一事务:仅当仍为 settled=0 时才退款;退款失败整体回滚,settled 保持 0 可重试 + final boolean[] refunded = {false}; + transactionTemplate.executeWithoutResult(status -> { + int updated = wheelRoundBetMapper.update(null, + new LambdaUpdateWrapper() + .eq(GameWheelRoundBet::getId, bet.getId()) + .eq(GameWheelRoundBet::getSettled, 0) + .set(GameWheelRoundBet::getSettled, 1) + .set(GameWheelRoundBet::getRefunded, 1)); // 标记"退过本金",便于事后区分发奖/退款 + if (updated == 0) { + return; // 已被其他线程结算/退款 + } + if (bet.getUserId() != null && bet.getBet() != null + && bet.getBet().compareTo(BigDecimal.ZERO) > 0) { + // 退金币 + R refund = userService.updateOnlyUserAccount(bet.getUserId().intValue(), + bet.getBet(), TtAccountRecordSource.WHEEL_BET_REFUND); + if (R.isError(refund)) { + throw new RuntimeException("兜底退款失败: " + refund.getMsg()); + } + // 恢复下注时被连带压掉的积分(与 subtractAccountAmount 的 LEAST 压积分对称) + BigDecimal pressedCredits = bet.getCreditsCut() != null ? bet.getCreditsCut() : BigDecimal.ZERO; + if (pressedCredits.compareTo(BigDecimal.ZERO) > 0) { + R creditsRefund = userService.updateUserCredits(bet.getUserId().intValue(), + pressedCredits, TtAccountRecordSource.WHEEL_BET_REFUND); + if (R.isError(creditsRefund)) { + throw new RuntimeException("兜底退款恢复积分失败: " + creditsRefund.getMsg()); + } + } + refunded[0] = true; + // 真实池注单退款 → 同步反向冲减该局已注入的下注,避免"本金退了、奖池还记着这笔投注"的虚增。 + // 放在同一退款事务内:极端回滚只会让池子偏保守(少发),不会多发给玩家。 + if (WheelPoolType.isReal(bet.getPoolType())) { + wheelRewardPoolCacheRepository.reverseBetInjection( + WheelPoolType.REAL, bet.getPeriodNo(), bet.getBet()); + } + log.warn("兜底退款成功, betId={}, userId={}, refund={}, credits={}", + bet.getId(), bet.getUserId(), bet.getBet(), pressedCredits); + } + }); + // 退款成功后异步发站内信告知(不阻塞、失败不影响退款) + if (refunded[0]) { + sendRefundNotice(bet); + } + } catch (Exception e) { + log.error("兜底结算异常, betId={}", bet.getId(), e); + } + } + } + + /** 退款站内信通知(静默退款无弹窗,需站内信告知玩家) */ + private void sendRefundNotice(GameWheelRoundBet bet) { + AsyncManager.me().run(() -> { + try { + String title = MessageCategoryEnum.GAME_WHEEL.getTitle(); + String category = MessageCategoryEnum.GAME_WHEEL.getCode(); + String periodNoStr = bet.getPeriodNo() != null ? String.valueOf(bet.getPeriodNo()) : "-"; + String content = "第 " + periodNoStr + " 期因故未能开奖,已退还您的下注金额 ¥" + + bet.getBet().stripTrailingZeros().toPlainString() + " 至余额。"; + messageService.sendNotice(bet.getUserId(), title, content, category); + } catch (Exception e) { + log.error("{} 退款通知发送失败, betId={}", EVENT_TAG, bet.getId(), e); + } + }); + } + + public R getRoundHistoryInfo(GetRoundHistoryRequest request) { + if (request.getPageNum() == null || request.getPageNum() <= 0) { + request.setPageNum(1); + } + if (request.getPageSize() == null || request.getPageSize() <= 0) { + request.setPageSize(15); + } else if (request.getPageSize() > MAX_PAGE_SIZE) { + request.setPageSize(MAX_PAGE_SIZE); // 防大页拉全表 + } + + int offset = (request.getPageNum() - 1) * request.getPageSize(); + List winBets = wheelRoundBetMapper.selectWinBetsPage(offset, request.getPageSize()); + Long total = wheelRoundBetMapper.countWinBets(); + + return R.ok(buildRoundHistoryResponse(winBets, total)); + } + + /** 将中奖注单列表组装为全服/个人中奖记录响应(共用) */ + private GetRoundHistoryResponse buildRoundHistoryResponse(List winBets, Long total) { + GetRoundHistoryResponse response = new GetRoundHistoryResponse(); + if (CollectionUtils.isEmpty(winBets)) { + response.setRows(new ArrayList<>()); + response.setTotal(0); + return response; + } + + List rows = winBets.stream().map(bet -> { + GetRoundHistoryResponse.BetItem item = new GetRoundHistoryResponse.BetItem(); + // prizeId 是下注时保存的奖品矩阵快照。无论中奖还是未中奖(0.01 安慰奖), + // 都返回对应矩阵行的品质、名称和图片,便于“我的记录”展示实际关联的奖品。 + item.setPeriodNo(bet.getPeriodNo()); + item.setEndTime(bet.getEndTime() != null ? DateFormatUtils.format(bet.getEndTime(), "yyyy-MM-dd HH:mm:ss") : ""); + item.setUserId(bet.getUserId()); + item.setNickName(bet.getNickName()); + item.setAvatar(bet.getAvatar()); + item.setBet(MoneyUtil.toStr(bet.getBet())); + item.setBetSymbol(bet.getBetSymbol()); + item.setWin(MoneyUtil.toStr(bet.getWin())); + item.setBetCount(bet.getBetCount()); + item.setPrizeGrade(bet.getPrizeGrade()); + item.setPrizeName(bet.getPrizeName()); + item.setPrizeImage(bet.getPrizeImage()); + return item; + }).collect(Collectors.toList()); + + response.setRows(rows); + response.setTotal(total != null ? total : 0); + return response; + } + + /** + * 获取历史开奖结果(每一轮的开奖符号) + */ + public R getRoundResults(GetRoundResultsRequest request) { + if (request.getPageNum() == null || request.getPageNum() <= 0) { + request.setPageNum(1); + } + if (request.getPageSize() == null || request.getPageSize() <= 0) { + request.setPageSize(20); + } else if (request.getPageSize() > MAX_PAGE_SIZE) { + request.setPageSize(MAX_PAGE_SIZE); // 防大页拉全表 + } + + int offset = (request.getPageNum() - 1) * request.getPageSize(); + + // 查询已结束的回合(按 periodNo 倒序,最新的在前) + List rounds = wheelRoundMapper.selectList( + new LambdaQueryWrapper() + .eq(GameWheelRound::getStatus, WheelRoundStatusEnum.SPINED.toString()) // 已开奖 + .isNotNull(GameWheelRound::getResult) // 有结果 + .orderByDesc(GameWheelRound::getPeriodNo) + .last("LIMIT " + offset + ", " + request.getPageSize()) + ); + + Long total = wheelRoundMapper.selectCount( + new LambdaQueryWrapper() + .eq(GameWheelRound::getStatus, WheelRoundStatusEnum.SPINED.toString()) + .isNotNull(GameWheelRound::getResult) + ); + + GetRoundResultsResponse response = new GetRoundResultsResponse(); + if (CollectionUtils.isEmpty(rounds)) { + response.setRows(new ArrayList<>()); + response.setTotal(0L); + return R.ok(response); + } + + List rows = rounds.stream().map(round -> { + GetRoundResultsResponse.RoundResult item = new GetRoundResultsResponse.RoundResult(); + item.setPeriodNo(round.getPeriodNo()); + + // 拆分 periodNo:前8位=日期,后5位=roundId + String periodNoStr = String.valueOf(round.getPeriodNo()).replaceAll("^0+", ""); // 去前导零 + if (periodNoStr.length() >= 8) { + item.setDate(periodNoStr.substring(0, 8)); // 前8位:yyyyMMdd + String roundIdStr = periodNoStr.substring(8); // 后面的位数:roundId + item.setRoundId(Long.parseLong(roundIdStr.isEmpty() ? "0" : roundIdStr)); + } else { + // 异常数据兜底 + item.setDate(""); + item.setRoundId(0L); + } + + // 解析统一落点结果(新格式纯整数,旧格式取 real 池) + String resultJson = round.getResult(); + if (resultJson != null && !resultJson.isEmpty()) { + int resultIndex = parseResultIndex(resultJson); + if (resultIndex >= 0) { + item.setResultIndex(resultIndex); + + // 根据 resultIndex 从 innerColors 和 outerSymbols 取颜色和符号 + String innerColors = round.getInnerColors(); // 24个字符,如 "GGGRRRPPPPPP..." + String outerSymbols = round.getSymbols(); // 24个字符,如 "AABBBCCCCC..." + + if (innerColors != null && innerColors.length() > resultIndex) { + char colorChar = innerColors.charAt(resultIndex); + item.setResultColor(colorChar == 'G' ? "gold" : colorChar == 'R' ? "red" : "purple"); + } + + if (outerSymbols != null && outerSymbols.length() > resultIndex) { + item.setResultSymbol(String.valueOf(outerSymbols.charAt(resultIndex))); + } + } + } + + // LocalDateTime → Date → String + if (round.getEndTime() != null) { + Date endDate = Date.from(round.getEndTime().atZone(ZoneId.systemDefault()).toInstant()); + item.setEndTime(DateFormatUtils.format(endDate, "yyyy-MM-dd HH:mm:ss")); + } else { + item.setEndTime(""); + } + + return item; + }).collect(Collectors.toList()); + + response.setRows(rows); + response.setTotal(total); + return R.ok(response); + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/CommonWheelService.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/CommonWheelService.java new file mode 100644 index 0000000..badb482 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/CommonWheelService.java @@ -0,0 +1,182 @@ +package com.ruoyi.game.wheel.service; + + +import com.ruoyi.common.utils.json.SnakeCaseJsonUtils; +import com.ruoyi.game.wheel.domain.GameWheelGameConfig; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.mapper.SysConfigMapper; +import com.ruoyi.system.service.ISysConfigService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; + +@Service +@Slf4j +public class CommonWheelService { + @Autowired + private SysConfigMapper sysConfigMapper; + + @Autowired + private ISysConfigService sysConfigService; + + public GameWheelGameConfig getGameConfig() { + SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("game.wheel.setting"); + if (sysConfig == null) { + GameWheelGameConfig config = new GameWheelGameConfig(); + config.setSwitchOff(false); + config.setBets(Arrays.asList(5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000)); + + // ===== 内圈玩法配置 ===== + config.setSegmentCount(24); + // 外圈24格固定布局:A金符×2 / B红符×3 / C紫符×8 / D蓝符×11(运营可重排) + config.setOuterSymbols("AABBBCCCCCCCCDDDDDDDDDDD"); + // 内圈分布:紫10 / 红8 / 金6(每轮随机排布) + config.setInnerGoldCount(6); + config.setInnerRedCount(8); + config.setInnerPurpleCount(10); + // 12 个倍率:3内圈色 × 4外圈符号 + config.setMultipliers(Arrays.asList( + new GameWheelGameConfig.MultiplierItem("gold", "A", 44), + new GameWheelGameConfig.MultiplierItem("gold", "B", 26), + new GameWheelGameConfig.MultiplierItem("gold", "C", 11), + new GameWheelGameConfig.MultiplierItem("gold", "D", 8), + new GameWheelGameConfig.MultiplierItem("red", "A", 33), + new GameWheelGameConfig.MultiplierItem("red", "B", 21), + new GameWheelGameConfig.MultiplierItem("red", "C", 8), + new GameWheelGameConfig.MultiplierItem("red", "D", 6), + new GameWheelGameConfig.MultiplierItem("purple", "A", 27), + new GameWheelGameConfig.MultiplierItem("purple", "B", 17), + new GameWheelGameConfig.MultiplierItem("purple", "C", 7), + new GameWheelGameConfig.MultiplierItem("purple", "D", 5) + )); + + // 发奖上限:奖池余额×0.6(保池)+ 累计投注×0.9 − 累计返奖(返奖额度)取小 + config.setBufferRatio(0.6); + config.setRtp(0.9); + // 时间:下注 30s / 开盘动画 7s + config.setBettingDuration(30); + config.setAnimationDuration(7); + + + sysConfig = new SysConfig(); + sysConfig.setConfigName("幸运转盘游戏配置"); + sysConfig.setConfigKey("game.wheel.setting"); + sysConfig.setConfigValue(SnakeCaseJsonUtils.toSnakeCaseJson(config)); + sysConfig.setConfigType("N"); + + sysConfigMapper.insertConfig(sysConfig); + return config; + } + return SnakeCaseJsonUtils.fromSnakeCaseJson(sysConfig.getConfigValue(), GameWheelGameConfig.class); + } + + /** + * 保存配置(合并后的完整对象)。 + * + * @param syncMaintenance 是否同步维护开关:只有请求里**显式带了 switchOff** 才为 true, + * 否则部分字段请求会把 wheelMaintenance 静默改成 0(= 解除维护) + */ + public void updateGameConfig(GameWheelGameConfig config, boolean syncMaintenance) { + // 两个发奖比例只有 0~1 才有意义:越界一律夹回。前端与接口都走这里,是唯一入口 + config.setBufferRatio(clamp01(config.getBufferRatio(), 0.6)); + config.setRtp(clamp01(config.getRtp(), 0.9)); + SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("game.wheel.setting"); + if (sysConfig == null) { + sysConfig = new SysConfig(); + sysConfig.setConfigName("幸运转盘游戏配置"); + sysConfig.setConfigKey("game.wheel.setting"); + sysConfig.setConfigType("N"); + sysConfig.setConfigValue(SnakeCaseJsonUtils.toSnakeCaseJson(config)); + sysConfigMapper.insertConfig(sysConfig); + } else { + sysConfig.setConfigValue(SnakeCaseJsonUtils.toSnakeCaseJson(config)); + sysConfigMapper.updateConfig(sysConfig); + } + + if (!syncMaintenance) { + return; // 请求没带 switchOff:不动维护开关 + } + // 同步 switchOff → wheelMaintenance 维护开关 + SysConfig maintenanceConfig = sysConfigMapper.checkConfigKeyUnique("wheelMaintenance"); + String newValue = config.isSwitchOff() ? "1" : "0"; + if (maintenanceConfig == null) { + maintenanceConfig = new SysConfig(); + maintenanceConfig.setConfigName("疯狂转盘维护"); + maintenanceConfig.setConfigKey("wheelMaintenance"); + maintenanceConfig.setConfigType("Y"); + maintenanceConfig.setConfigValue(newValue); + sysConfigMapper.insertConfig(maintenanceConfig); + } else { + maintenanceConfig.setConfigValue(newValue); + sysConfigMapper.updateConfig(maintenanceConfig); + } + // 这里是直接走 mapper 改库,绕过了 SysConfig 的 Redis 缓存(该缓存没有过期时间), + // 不刷新的话 doBet/isMaintenanceMode 会一直读到旧值 → "关了游戏但还提示不在下注期" + sysConfigService.resetConfigCache(); + } + + /** + * 结构性校验:不合法返回错误信息,合法返回 null。配置面唯一入口,前端与接口都过这里。 + * + * 为什么要拦:这些字段直接决定赔率与盘面,填错不会立刻报错但会算错赔率、甚至让每局都开不了奖 + * (例如外圈布局长度 ≠ 总格数 → doSpin 里 charAt 越界;倍率为 0/负数 → 中了不赔或反向扣池)。 + */ + public String validate(GameWheelGameConfig config) { + if (config == null) return "配置内容为空"; + List bets = config.getBets(); + if (bets == null || bets.isEmpty()) return "下注档位不能为空"; + for (Integer b : bets) { + if (b == null || !ALLOWED_BETS.contains(b)) return "下注档位只支持 " + ALLOWED_BETS; + } + int seg = config.getSegmentCount(); + if (seg < 8 || seg > 48) return "转盘总格数需在 8~48 之间(当前 " + seg + ")"; + String outer = config.getOuterSymbols(); + if (outer == null || outer.isEmpty()) return "外圈符号布局不能为空"; + if (outer.length() != seg) return "外圈符号布局长度(" + outer.length() + ")必须等于转盘总格数(" + seg + ")"; + for (char ch : outer.toCharArray()) { + if (ch != 'A' && ch != 'B' && ch != 'C' && ch != 'D') return "外圈符号布局只能包含 A/B/C/D,发现非法字符:" + ch; + } + int innerSum = config.getInnerGoldCount() + config.getInnerRedCount() + config.getInnerPurpleCount(); + if (innerSum != seg) return "内圈三色格数合计(" + innerSum + ")必须等于转盘总格数(" + seg + ")"; + List multipliers = config.getMultipliers(); + if (multipliers == null || multipliers.size() != 12) return "倍率矩阵必须是 12 个组合(3 色 × 4 符号)"; + for (GameWheelGameConfig.MultiplierItem m : multipliers) { + if (m == null || !VALID_COLORS.contains(m.getColor()) || !VALID_SYMBOLS.contains(m.getSymbol())) { + return "倍率矩阵存在非法组合(颜色只支持 gold/red/purple,符号只支持 A/B/C/D)"; + } + if (m.getMultiplier() == null || m.getMultiplier() < 1 || m.getMultiplier() > MAX_MULTIPLIER) { + return "倍率必须在 1~" + MAX_MULTIPLIER + " 之间(组合 " + m.getColor() + "/" + m.getSymbol() + ")"; + } + } + // 12 个组合全押必须亏:这是"照规则全额赔"这个兜底分支唯一的资金护栏。 + // 玩家把 12 个组合都押上时,引擎挑不出"无真实注的格",只能开"赔付最小格"并**不受奖池上限约束**地全额赔。 + // 此时成本 = Σb_i、赔付 = min(b_i×m_i),按 b_i = c/m_i 配比下注可把赔付做成 c / Σ(1/m_i): + // 只要 Σ(1/m_i) ≤ 1 就是"全包必赚",池子再负也拦不住(每局稳吃 (1/Σ(1/m) 的倒数) 倍差)。 + // 前端按目标RTP 重算出来的倍率天然满足(Σ(1/m) ≈ 1/rtp ≥ 1),但倍率可以手工改,所以这里必须卡。 + double reciprocalSum = multipliers.stream().mapToDouble(m -> 1.0 / m.getMultiplier()).sum(); + if (reciprocalSum <= 1.02) { + return String.format("倍率组合不合法:12 个组合全押的回报率约 %.0f%%(必须明显小于 100%%,否则玩家全包必赚," + + "且该分支不受奖池上限约束)", 100 / reciprocalSum); + } + if (config.getBettingDuration() <= 0 || config.getBettedDuration() < 0 || config.getAnimationDuration() <= 0) { + return "时间配置不合法(下注/动画时长必须大于 0,封盘时长不能小于 0)"; + } + return null; + } + + /** 可选下注档位(与奖励矩阵 10 档一致) */ + private static final List ALLOWED_BETS = Arrays.asList(5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000); + private static final List VALID_COLORS = Arrays.asList("gold", "red", "purple"); + private static final List VALID_SYMBOLS = Arrays.asList("A", "B", "C", "D"); + /** 单格倍率上限:5000 档 × 200 = 100 万,够用且防止把赔率打成天文数字 */ + private static final int MAX_MULTIPLIER = 200; + + /** 比例参数夹到 0~1;NaN(异常入参)回落到默认值 */ + private static double clamp01(double v, double fallback) { + if (Double.isNaN(v)) return fallback; + return Math.max(0, Math.min(1, v)); + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/WheelSettleTask.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/WheelSettleTask.java new file mode 100644 index 0000000..520e7f9 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/service/WheelSettleTask.java @@ -0,0 +1,46 @@ +package com.ruoyi.game.wheel.service; + +import com.ruoyi.system.service.ISysConfigService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 疯狂转盘异常兜底定时任务。 + * 扫描 settled=0 的异常残留注单(开奖异常/结算事务回滚导致从未结算)并退还本金。 + * 注单在 doBet 时即以 settled=0 落库,正常开奖后 processResult 会更新为 settled=1。 + * 注册到 RuoYi 系统监控-定时任务,调用目标:wheelSettleTask.scheduledSettle + * 建议执行频率:每10分钟一次(非维护期仅处理超过一局时长的异常注单,不会误退进行中的回合) + */ +@Slf4j +@Component +public class WheelSettleTask { + + @Autowired + private ApiWheelService apiWheelService; + + @Autowired + private ISysConfigService sysConfigService; + + /** + * 定时扫描异常兜底。 + * 维护期间扫描所有 settled=0 记录(不限时间);非维护期间只扫描超过一局时长的异常记录。 + */ + public void scheduledSettle() { + log.info("WheelSettleTask start ----------"); + try { + boolean isMaintenance = false; + try { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + String wheelMaintenance = sysConfigService.selectConfigByKey("wheelMaintenance"); + isMaintenance = "1".equals(websiteMaintenance) || "1".equals(wheelMaintenance); + } catch (Exception e) { + log.warn("WheelSettleTask: failed to check maintenance, error={}", e.getMessage()); + } + apiWheelService.settleAbandonedWheelBets(isMaintenance); + } catch (Exception e) { + log.error("WheelSettleTask error: {}", e.getMessage(), e); + } + log.info("WheelSettleTask done ----------"); + } +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/vo/WheelBetRecordVO.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/vo/WheelBetRecordVO.java new file mode 100644 index 0000000..1d60c08 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/vo/WheelBetRecordVO.java @@ -0,0 +1,60 @@ +package com.ruoyi.game.wheel.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 疑狂转盘投注记录VO(管理后台查看用,按 round_id + user_id 分组) + */ +@Data +public class WheelBetRecordVO implements Serializable { + + private Long roundId; + + /** 全局唯一期号 */ + private Long periodNo; + + private Long userId; + + /** 用户昵称 */ + private String nickName; + + /** 投注符号(逗号分隔,如 A,B,C,D,E,F) */ + private String betSymbols; + + /** 用户该回合总下注 */ + private BigDecimal totalBetAmount; + + /** 投注明细(格式: 符号:金额:赢得|符号:金额:赢得) */ + private String betDetails; + + /** 用户回合实际赔付(含0.01保底) */ + private BigDecimal userRoundPayout; + + /** 结算状态 0=未结算 1=已结算 */ + private Integer settled; + + /** 投注时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 中奖时间(回合结束时间) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + /** 中奖结果(回合旋转结果,如 S2,6,9) */ + private String result; + + /** 回合符号序列 */ + private String symbols; + + /** 回合内圈色序列 */ + private String innerColors; + + /** 用户所属池类型(real/robot/blank/promo) */ + private String poolTypes; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/vo/WheelPoolAdjustmentVO.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/vo/WheelPoolAdjustmentVO.java new file mode 100644 index 0000000..74aced7 --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/vo/WheelPoolAdjustmentVO.java @@ -0,0 +1,22 @@ +package com.ruoyi.game.wheel.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 疯狂转盘奖池调节记录响应。 + * createTime 使用字符串,避免接口直接序列化 LocalDateTime。 + */ +@Data +public class WheelPoolAdjustmentVO { + + private Long id; + private String poolType; + private BigDecimal adjustmentAmount; + private BigDecimal balanceBefore; + private BigDecimal balanceAfter; + private String reason; + private String operator; + private String createTime; +} diff --git a/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/vo/WheelWinBetVO.java b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/vo/WheelWinBetVO.java new file mode 100644 index 0000000..f4eadae --- /dev/null +++ b/skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/vo/WheelWinBetVO.java @@ -0,0 +1,45 @@ +package com.ruoyi.game.wheel.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 疯狂转盘全服中奖记录(含用户昵称、期号、中奖符号下注次数、饰品品质/名称/图片) + */ +@Data +public class WheelWinBetVO implements Serializable { + + private Long periodNo; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date endTime; + + private Long userId; + + private String nickName; + + /** 用户头像 */ + private String avatar; + + private BigDecimal bet; + + private String betSymbol; + + private BigDecimal win; + + /** 该中奖符号下注次数 */ + private Integer betCount; + + /** 饰品品质等级(对应 tt_ornaments_level.id) */ + private Integer prizeGrade; + + /** 饰品名称 */ + private String prizeName; + + /** 饰品图片 */ + private String prizeImage; +} diff --git a/skins-service/message-service/pom.xml b/skins-service/message-service/pom.xml new file mode 100644 index 0000000..9379e41 --- /dev/null +++ b/skins-service/message-service/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + com.ruoyi + skins-service + 4.8.2 + + + message-service + + + 消息通知统一发送模块 + + + + + com.ruoyi + skins-model + ${ruoyi.version} + + + + diff --git a/skins-service/message-service/src/main/java/com/ruoyi/message/mapper/MessageMapper.java b/skins-service/message-service/src/main/java/com/ruoyi/message/mapper/MessageMapper.java new file mode 100644 index 0000000..9736f74 --- /dev/null +++ b/skins-service/message-service/src/main/java/com/ruoyi/message/mapper/MessageMapper.java @@ -0,0 +1,12 @@ +package com.ruoyi.message.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtNotice; +import org.apache.ibatis.annotations.Mapper; + +/** + * 消息通知 Mapper。 + */ +@Mapper +public interface MessageMapper extends BaseMapper { +} diff --git a/skins-service/message-service/src/main/java/com/ruoyi/message/service/MessageService.java b/skins-service/message-service/src/main/java/com/ruoyi/message/service/MessageService.java new file mode 100644 index 0000000..a2b70d7 --- /dev/null +++ b/skins-service/message-service/src/main/java/com/ruoyi/message/service/MessageService.java @@ -0,0 +1,21 @@ +package com.ruoyi.message.service; + +/** + * 消息通知统一发送服务。 + *

+ * 负责向 tt_notice 表写入用户通知,供邮件/消息中心展示。 + */ +public interface MessageService { + + /** + * 发送通知(不带分类,兼容旧调用)。 + */ + void sendNotice(Long userId, String title, String content); + + /** + * 发送通知(带消息分类)。 + * + * @param category 分类 code,如 GAME_PVP_TOP / SYS_VIP_REBATE + */ + void sendNotice(Long userId, String title, String content, String category); +} diff --git a/skins-service/message-service/src/main/java/com/ruoyi/message/service/impl/MessageServiceImpl.java b/skins-service/message-service/src/main/java/com/ruoyi/message/service/impl/MessageServiceImpl.java new file mode 100644 index 0000000..112f8fe --- /dev/null +++ b/skins-service/message-service/src/main/java/com/ruoyi/message/service/impl/MessageServiceImpl.java @@ -0,0 +1,35 @@ +package com.ruoyi.message.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.other.TtNotice; +import com.ruoyi.message.mapper.MessageMapper; +import com.ruoyi.message.service.MessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 消息通知统一发送服务实现。 + */ +@Service +public class MessageServiceImpl implements MessageService { + + @Autowired + private MessageMapper messageMapper; + + @Override + public void sendNotice(Long userId, String title, String content) { + sendNotice(userId, title, content, null); + } + + @Override + public void sendNotice(Long userId, String title, String content, String category) { + TtNotice notice = new TtNotice(); + notice.setUserId(userId); + notice.setTitle(title); + notice.setContent(content); + notice.setRead("0"); + notice.setCategory(category); + notice.setCreateTime(DateUtils.getNowDate()); + messageMapper.insert(notice); + } +} diff --git a/skins-service/pom.xml b/skins-service/pom.xml new file mode 100644 index 0000000..cd631c3 --- /dev/null +++ b/skins-service/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.ruoyi + ruoyi + 4.8.2 + + + skins-service + pom + + message-service + service-user + service-thirdparty + service-admin + service-playingmethod + service-task + game-wheel-service + game-olympus-service + reward-service + + + + + + com.ruoyi + ruoyi-common + + + + com.ruoyi + ruoyi-system + + + + com.ruoyi + skins-model + ${ruoyi.version} + + + + + io.springfox + springfox-boot-starter + ${swagger.version} + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/reward-service/pom.xml b/skins-service/reward-service/pom.xml new file mode 100644 index 0000000..35330e4 --- /dev/null +++ b/skins-service/reward-service/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + com.ruoyi + skins-service + 4.8.2 + + + reward-service + + + com.ruoyi + ruoyi-framework + + + + \ No newline at end of file diff --git a/skins-service/reward-service/src/main/java/com/ruoyi/reward/contract/admin/request/AdjustRewardPoolRequest.java b/skins-service/reward-service/src/main/java/com/ruoyi/reward/contract/admin/request/AdjustRewardPoolRequest.java new file mode 100644 index 0000000..5c7a53e --- /dev/null +++ b/skins-service/reward-service/src/main/java/com/ruoyi/reward/contract/admin/request/AdjustRewardPoolRequest.java @@ -0,0 +1,12 @@ +package com.ruoyi.reward.contract.admin.request; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class AdjustRewardPoolRequest { + // tighten(抽水: bet→profit) | loosen(放水: profit→bet) + private String direction; + private BigDecimal amount; +} diff --git a/skins-service/reward-service/src/main/java/com/ruoyi/reward/contract/admin/response/QueryGlobalRewardConfig.java b/skins-service/reward-service/src/main/java/com/ruoyi/reward/contract/admin/response/QueryGlobalRewardConfig.java new file mode 100644 index 0000000..a198edc --- /dev/null +++ b/skins-service/reward-service/src/main/java/com/ruoyi/reward/contract/admin/response/QueryGlobalRewardConfig.java @@ -0,0 +1,19 @@ +package com.ruoyi.reward.contract.admin.response; + +import com.ruoyi.reward.model.GloalRewardConfig; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class QueryGlobalRewardConfig { + private GloalRewardConfig config; + + // 全局奖池数据 + private BigDecimal totalWin; + private BigDecimal totalBet; + private BigDecimal netProfit; + private BigDecimal availableQuota; + private BigDecimal originalTotalBet; + private double rtp; +} diff --git a/skins-service/reward-service/src/main/java/com/ruoyi/reward/controller/AdminGlobalRewardPoolController.java b/skins-service/reward-service/src/main/java/com/ruoyi/reward/controller/AdminGlobalRewardPoolController.java new file mode 100644 index 0000000..bb32c48 --- /dev/null +++ b/skins-service/reward-service/src/main/java/com/ruoyi/reward/controller/AdminGlobalRewardPoolController.java @@ -0,0 +1,68 @@ +package com.ruoyi.reward.controller; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.reward.contract.admin.request.AdjustRewardPoolRequest; +import com.ruoyi.reward.contract.admin.response.QueryGlobalRewardConfig; +import com.ruoyi.reward.model.GloalRewardConfig; +import com.ruoyi.reward.service.GlobalRewardPoolService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; + +@Api(tags = "全局奖池配置") +@Slf4j +@RestController +@RequestMapping("/admin/globalrewardpool") +public class AdminGlobalRewardPoolController { + @Autowired + private GlobalRewardPoolService globalRewardPoolService; + + @ApiOperation("获取奖池配置和状态") + @GetMapping("/config") + public R getGameConfig() { + var config = globalRewardPoolService.getGameConfig(); + var cache = globalRewardPoolService.getCache(); + QueryGlobalRewardConfig result = new QueryGlobalRewardConfig(); + result.setConfig(config); + result.setTotalWin(cache.getTotalWin()); + result.setTotalBet(cache.getTotalBet()); + result.setNetProfit(cache.getNetProfit()); + result.setAvailableQuota(globalRewardPoolService.availableQuota()); + result.setOriginalTotalBet(cache.getTotalBet().add(cache.getNetProfit())); + result.setRtp(globalRewardPoolService.rtp()); + return R.ok(result); + } + + @ApiOperation("更新奖池配置") + @PostMapping("/config") + public R updateGameConfig(@RequestBody GloalRewardConfig params) { + String msg = globalRewardPoolService.updateGameConfig(params); + if (msg != null && !msg.isEmpty()) + return R.fail(msg); + return R.ok(); + } + + @ApiOperation("重置奖池缓存") + @PostMapping("/reset") + public R resetRewardPool() { + globalRewardPoolService.resetCache(); + return R.ok(null, "奖池缓存已清空"); + } + + @ApiOperation("调整奖池(抽水/放水)") + @PostMapping("/adjust") + public R adjustRewardPool(@RequestBody AdjustRewardPoolRequest req) { + if (req.getAmount() == null || req.getAmount().compareTo(BigDecimal.ZERO) <= 0) { + return R.fail("金额必须大于0"); + } + if (!"tighten".equals(req.getDirection()) && !"loosen".equals(req.getDirection())) { + return R.fail("方向必须为 tighten 或 loosen"); + } + globalRewardPoolService.adjust(req.getDirection(), req.getAmount()); + return R.ok(null, "调整成功"); + } +} diff --git a/skins-service/reward-service/src/main/java/com/ruoyi/reward/model/GloalRewardConfig.java b/skins-service/reward-service/src/main/java/com/ruoyi/reward/model/GloalRewardConfig.java new file mode 100644 index 0000000..2ec00b5 --- /dev/null +++ b/skins-service/reward-service/src/main/java/com/ruoyi/reward/model/GloalRewardConfig.java @@ -0,0 +1,9 @@ +package com.ruoyi.reward.model; + +import lombok.Data; + +@Data +public class GloalRewardConfig { + // 利润率.抽水比例。 如0.3表示下注额的0.3倍将会存入网站,只有下注额的0.7倍才会放入奖池中 + private double profitRate; +} diff --git a/skins-service/reward-service/src/main/java/com/ruoyi/reward/model/cache/GlobalRewardPoolCache.java b/skins-service/reward-service/src/main/java/com/ruoyi/reward/model/cache/GlobalRewardPoolCache.java new file mode 100644 index 0000000..dc50b9e --- /dev/null +++ b/skins-service/reward-service/src/main/java/com/ruoyi/reward/model/cache/GlobalRewardPoolCache.java @@ -0,0 +1,12 @@ +package com.ruoyi.reward.model.cache; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class GlobalRewardPoolCache { + private BigDecimal totalBet; + private BigDecimal totalWin; + private BigDecimal netProfit; +} diff --git a/skins-service/reward-service/src/main/java/com/ruoyi/reward/repository/GlobalRewardPoolCacheRepository.java b/skins-service/reward-service/src/main/java/com/ruoyi/reward/repository/GlobalRewardPoolCacheRepository.java new file mode 100644 index 0000000..2fa90f5 --- /dev/null +++ b/skins-service/reward-service/src/main/java/com/ruoyi/reward/repository/GlobalRewardPoolCacheRepository.java @@ -0,0 +1,56 @@ +package com.ruoyi.reward.repository; + +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.reward.model.cache.GlobalRewardPoolCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +@Component +public class GlobalRewardPoolCacheRepository { + + @Autowired + public RedisTemplate redisTemplate; + + private final static String GLOBAL_REWARD_POOL_CACHE = "ruoyi:global:reward:pool"; + + public void incrementalProfit(BigDecimal profit) { + AsyncManager.me().run(()->{ + redisTemplate.opsForValue().increment(GLOBAL_REWARD_POOL_CACHE + ":profit", profit.doubleValue()); + }); + } + + public void incrementalBet(BigDecimal totalBet) { + redisTemplate.opsForValue().increment(GLOBAL_REWARD_POOL_CACHE + ":bet", totalBet.doubleValue()); + } + + public void incrementalWin(BigDecimal totalWin) { + redisTemplate.opsForValue().increment(GLOBAL_REWARD_POOL_CACHE + ":win", totalWin.doubleValue()); + } + + public GlobalRewardPoolCache getCache() { + var totalBetS = redisTemplate.opsForValue().get(GLOBAL_REWARD_POOL_CACHE + ":bet"); + var totalWinS = redisTemplate.opsForValue().get(GLOBAL_REWARD_POOL_CACHE + ":win"); + var totalProfitS = redisTemplate.opsForValue().get(GLOBAL_REWARD_POOL_CACHE + ":profit"); + var cache = new GlobalRewardPoolCache(); + cache.setTotalBet(totalBetS == null ? BigDecimal.ZERO : new BigDecimal(totalBetS.toString())); + cache.setTotalWin(totalWinS == null ? BigDecimal.ZERO : new BigDecimal(totalWinS.toString())); + cache.setNetProfit(totalProfitS == null ? BigDecimal.ZERO : new BigDecimal(totalProfitS.toString())); + return cache; + } + + // 同步调整 bet 和 profit(允许变负) + public void adjustPool(BigDecimal betDelta, BigDecimal profitDelta) { + redisTemplate.opsForValue().increment(GLOBAL_REWARD_POOL_CACHE + ":bet", betDelta.doubleValue()); + redisTemplate.opsForValue().increment(GLOBAL_REWARD_POOL_CACHE + ":profit", profitDelta.doubleValue()); + } + + // 清空奖池缓存(总下注、总返奖、累计抽水归零) + public void resetCache() { + redisTemplate.delete(GLOBAL_REWARD_POOL_CACHE + ":bet"); + redisTemplate.delete(GLOBAL_REWARD_POOL_CACHE + ":win"); + redisTemplate.delete(GLOBAL_REWARD_POOL_CACHE + ":profit"); + } +} diff --git a/skins-service/reward-service/src/main/java/com/ruoyi/reward/service/GlobalRewardPoolService.java b/skins-service/reward-service/src/main/java/com/ruoyi/reward/service/GlobalRewardPoolService.java new file mode 100644 index 0000000..f84ab78 --- /dev/null +++ b/skins-service/reward-service/src/main/java/com/ruoyi/reward/service/GlobalRewardPoolService.java @@ -0,0 +1,106 @@ +package com.ruoyi.reward.service; + +import com.ruoyi.common.utils.json.JsonUtils; +import com.ruoyi.reward.model.GloalRewardConfig; +import com.ruoyi.reward.model.cache.GlobalRewardPoolCache; +import com.ruoyi.reward.repository.GlobalRewardPoolCacheRepository; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.mapper.SysConfigMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +@Service +public class GlobalRewardPoolService { + + private static final String CONFIG_KEY = "global.reward.setting"; + + @Autowired + private SysConfigMapper sysConfigMapper; + @Autowired + private GlobalRewardPoolCacheRepository poolCacheRepository; + + public void incrementalBet(BigDecimal totalBet) { + BigDecimal profit = totalBet.abs().multiply(BigDecimal.valueOf(getGameConfig().getProfitRate())); + poolCacheRepository.incrementalProfit(profit); + poolCacheRepository.incrementalBet(totalBet.abs().subtract(profit)); + } + + public void incrementalWin(BigDecimal totalWin) { + poolCacheRepository.incrementalWin(totalWin.abs()); + } + + public GlobalRewardPoolCache getCache() { + return poolCacheRepository.getCache(); + } + + // 清空奖池缓存(总下注、总返奖归零) + public void resetCache() { + poolCacheRepository.resetCache(); + } + + // 手动调整奖池:tighten(抽水 bet→profit) | loosen(放水 profit→bet),允许变负 + public void adjust(String direction, BigDecimal amount) { + if ("tighten".equals(direction)) { + poolCacheRepository.adjustPool(amount.negate(), amount); + } else { + poolCacheRepository.adjustPool(amount, amount.negate()); + } + } + + // 奖池净利润 + public BigDecimal netProfits() { + var cache = poolCacheRepository.getCache(); + return cache.getNetProfit(); + } + + // 奖池可用额度 totalBet - totalWin + public BigDecimal availableQuota() { + var cache = poolCacheRepository.getCache(); + return cache.getTotalBet().subtract(cache.getTotalWin()); + } + + // 奖池rtp totalWin/totalBet + public double rtp() { + var cache = poolCacheRepository.getCache(); + BigDecimal divisor = cache.getTotalBet().add(cache.getNetProfit()); + if (divisor.compareTo(BigDecimal.ZERO) == 0) { + return 0; + } + return cache.getTotalWin().divide(divisor, 4, RoundingMode.HALF_UP) + .doubleValue(); + } + + public GloalRewardConfig getGameConfig() { + SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique(CONFIG_KEY); + if (sysConfig == null) { + GloalRewardConfig config = new GloalRewardConfig(); + config.setProfitRate(0.1); + return config; + } + return JsonUtils.fromSnakeCaseJson(sysConfig.getConfigValue(), GloalRewardConfig.class); + } + + + public String updateGameConfig(GloalRewardConfig params) { + if (params.getProfitRate() < 0 || params.getProfitRate() > 1) { + return "profitRate必须在[0,1]之间"; + } + + SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique(CONFIG_KEY); + if (sysConfig == null) { + sysConfig = new SysConfig() {{ + setConfigName("全局奖池配置"); + setConfigKey(CONFIG_KEY); + setConfigValue(JsonUtils.toSnakeCaseJson(params)); + }}; + sysConfigMapper.insertConfig(sysConfig); + } else { + sysConfig.setConfigValue(JsonUtils.toSnakeCaseJson(params)); + sysConfigMapper.updateConfig(sysConfig); + } + return ""; + } +} diff --git a/skins-service/service-admin/pom.xml b/skins-service/service-admin/pom.xml new file mode 100644 index 0000000..c3694e7 --- /dev/null +++ b/skins-service/service-admin/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + com.ruoyi + skins-service + 4.8.2 + + + service-admin + + + + com.ruoyi + ruoyi-quartz + + + com.ruoyi + ruoyi-framework + + + com.ruoyi + message-service + 4.8.2 + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/aspectj/UpdateUserCacheAspect.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/aspectj/UpdateUserCacheAspect.java new file mode 100644 index 0000000..c2c1d1a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/aspectj/UpdateUserCacheAspect.java @@ -0,0 +1,143 @@ +package com.ruoyi.admin.aspectj; + +import cn.hutool.core.util.ObjectUtil; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.entity.UserData; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.framework.web.service.TokenService; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; + +@Aspect +@Component +@Order(1) +@Slf4j +public class UpdateUserCacheAspect { + + private final TtUserService userService; + private final TokenService tokenService; + + public UpdateUserCacheAspect(TtUserService userService, + TokenService tokenService) { + this.userService = userService; + this.tokenService = tokenService; + } + + @Pointcut("@annotation(com.ruoyi.common.annotation.NewUserInfo)") + private void NewUserInfo() { + } + + @Pointcut("@annotation(com.ruoyi.common.annotation.UpdateUserCache)") + private void permissionCheck() { + } + + @Before("NewUserInfo()") + public void NewUserInfo(JoinPoint joinPoint) throws IOException { + // 获取request对象 + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (StringUtils.isNull(attributes)) return; + HttpServletRequest request = attributes.getRequest(); + + // 获取登录用户信息 + LoginUser loginUser = tokenService.getLoginUser(request); + if (ObjectUtil.isEmpty(loginUser)) { + // 不能只 sendError 后 return:请求会继续进入 Controller,getLoginUser() 再抛异常, + // 响应已被提交导致错误信息丢失。直接抛 401,前端按 code=401 登出。 + log.warn("登录过期,请重新登录。"); + throw new ServiceException("登录过期,请重新登录。", HttpStatus.UNAUTHORIZED); + } + TtUser user = userService.getById(loginUser.getUserId()); + if (stopIfUserDeleted(loginUser, user)) return; + + // 设置更新后的属性 + UserData userData = loginUser.getUserData(); + // 对象拷贝 + BeanUtils.copyBeanProp(userData, user); + // 获取上级用户 + TtUser ParentUser = userService.getById(user.getParentId()); + if (StringUtils.isNotNull(ParentUser)) + userData.setParentInvitationCode(ParentUser.getInvitationCode()); // 上级邀请码 + loginUser.setUserData(userData); + + // 刷新token + tokenService.refreshToken(loginUser); + //log.info("用户数据已提前刷新"); + } + + @After("permissionCheck()") + public void afterAdvice(JoinPoint joinPoint) throws IOException { + + // 获取request对象 + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (StringUtils.isNull(attributes)) return; + HttpServletRequest request = attributes.getRequest(); + + // 获取登录用户信息 + LoginUser loginUser = tokenService.getLoginUser(request); + if (ObjectUtil.isEmpty(loginUser)) { + log.warn("登录过期,请重新登录。"); + throw new ServiceException("登录过期,请重新登录。", HttpStatus.UNAUTHORIZED); + } + TtUser user = userService.getById(loginUser.getUserId()); + if (stopIfUserDeleted(loginUser, user)) return; + + // 设置更新后的属性 + UserData userData = loginUser.getUserData(); + if (userData == null) { + userData = UserData.builder().build(); + } + // 对象拷贝 + BeanUtils.copyBeanProp(userData, user); + // 获取上级用户 + TtUser ParentUser = userService.getById(user.getParentId()); + if (StringUtils.isNotNull(ParentUser)) + userData.setParentInvitationCode(ParentUser.getInvitationCode()); // 上级邀请码 + loginUser.setUserData(userData); + + // 刷新token + tokenService.refreshToken(loginUser); + //log.info("用户数据缓存刷新完毕"); + } + + /** + * 登录态存在但用户记录已查不到(后台逻辑删除 del_flag=2 或真删除)时的兜底处理。 + *

+ * 后台删除用户不会动 Redis 里的 Token,若不处理,控制器会继续拿 null 用户执行, + * 表现为 /api/getAccountAmount 一直空指针(Cannot invoke ... because "ttUser" is null), + * 且客户端永远无法登出。此处清掉登录态并返回 401,前端据此自动跳登录页。 + * + * @return true 表示请求不应继续(已抛 401 或后台/运营账号直接跳过缓存刷新) + */ + private boolean stopIfUserDeleted(LoginUser loginUser, TtUser user) { + if (ObjectUtil.isNotEmpty(user)) return false; + UserData userData = loginUser.getUserData(); + // 只处理玩家账号(api_/promo_user_ 登录,身份在 tt_user): + // 后台 SysUser 账号 user 非 null;promo_ 超管账号身份在 promo_admin 表,都不应在此清登录态 + boolean playerAccount = loginUser.getUser() == null && userData != null + && !UserType.SUPER_ADMIN.getCode().equals(userData.getUserType()); + if (!playerAccount) { + log.warn("用户缓存刷新跳过:userId={} 用户不存在(非玩家账号)", loginUser.getUserId()); + return true; + } + tokenService.delLoginUser(loginUser.getToken()); + log.warn("用户已不存在,清除登录态并返回401:userId={}", loginUser.getUserId()); + throw new ServiceException("账号不存在或已被删除,请重新登录。", HttpStatus.UNAUTHORIZED); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/cache/BoxMockCacheRepository.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/cache/BoxMockCacheRepository.java new file mode 100644 index 0000000..e49dde2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/cache/BoxMockCacheRepository.java @@ -0,0 +1,26 @@ +package com.ruoyi.admin.cache; + +import com.ruoyi.admin.model.BoxMockCache; +import com.ruoyi.common.core.redis.RedisCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class BoxMockCacheRepository { + + private static final String BOX_MOCK_CACHE_KEY = "ruoyi:box:mock:"; + + @Autowired + private RedisCache redisCache; + + //将用户的临时爆率存储在redis中,存储七天 + public void setMockResultCache(Integer uid, BoxMockCache value) { + redisCache.setCacheObject(BOX_MOCK_CACHE_KEY + uid, value, 7, TimeUnit.DAYS); + } + //根据用户的id,从redis中取出用户的临时爆率 + public BoxMockCache getMockResultCache(Integer uid) { + return redisCache.getCacheObject(BOX_MOCK_CACHE_KEY + uid); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/config/RedisConstants.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/config/RedisConstants.java new file mode 100644 index 0000000..ee104a0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/config/RedisConstants.java @@ -0,0 +1,100 @@ +package com.ruoyi.admin.config; + +public class RedisConstants { + // 整个应用的通用uid锁 + public static final String COMMON_LOCK = "ruoyi:common:lock_"; + + // 游戏下注防重锁:控制层按 userId 加锁,tryLock wait=0 立即失败,锁包裹整个事务、解锁晚于提交; + // 锁粒度=单用户,仅串行同一用户的重复/并发下注,不影响其他用户,避免弱网重复提交导致重复扣款 + // 注:幸运升级不在此列——其锁按 upgradeId+userType(保护跨用户共享概率计数器,属底层RTP机制),不能改为userId + public static final String OPEN_BOX_LOCK = "open_box:lock_"; // 经典开箱/盲盒 + public static final String EXPONENT_JOIN_LOCK = "exponent_join:lock_"; // 指数盲盒 + public static final String SUGAR_GAME_CACHE = "ruoyi:sugar:"; + public static final String SUGAR_GAME_MOCK_QUEUE = "ruoyi:sugar:mock:"; + public static final String SUGAR_GAME_USER_RECORD_CACHE = "ruoyi:sugar:user:record:"; + public static final String SUGAR_GAME_CONFIG_CACHE = "ruoyi:sugar:config"; + public static final String SUGAR_GAME_REWARD_POOL_CACHE = "ruoyi:sugar:reward:pool"; + public static final String WHEEL_GAME_REWARD_POOL_CACHE = "ruoyi:wheel:reward:pool"; + public static final String BULLET_GAME_REWARD_POOL_CACHE = "ruoyi:bullet:reward:pool"; + // ========== Olympus 电子游戏 ========== + // 单局进行中缓存:ruoyi:olympus:{userId},存整局步骤,TTL 30 分钟 + public static final String OLYMPUS_GAME_CACHE = "ruoyi:olympus:"; + // 按箱奖池(水位):ruoyi:olympus:reward:pool:box:{boxId}:bet/:win/:adjust,仅统计真实玩家(白号隔离) + public static final String OLYMPUS_GAME_REWARD_POOL_CACHE = "ruoyi:olympus:reward:pool"; + // 一轮内RTP动态收敛账本:Hash ruoyi:olympus:cycle:{boxId}:{audience},字段 drawn/mulSum/conv:{ornamentId} + public static final String OLYMPUS_GAME_CYCLE = "ruoyi:olympus:cycle:"; + + + /** + * 开箱奖池 + * 奖池key: baseKey:boxId:poolType + * + * @see com.ruoyi.admin.enums.BoxPoolType + */ + public static final String BASE_POOL_KEY = "prize_pool:"; + + // 对战模式 + public static final String JOIN_FIGHT_LOCK = "join_fight:lock_"; + public static final String JOIN_FIGHT_BEGIN_LOCK = "join_fight:lock_begin_"; + public static final String JOIN_FIGHT_SEAT_READY_LOCK = "join_fight:lock_seat_ready_"; + public static final String JOIN_FIGHT_END_LOCK = "join_fight:lock_end_"; + + + // roll房间 + public static final String JOIN_ROLL_LOCK = "join_roll:lock_"; + + + public static final String RECEIVE_RED_PACKET_LOCK = "receive_red_packet:lock_"; + public static final String CARD_PAY_LOCK = "CARD_PAY:lock_"; + + // 卡商生成卡密:控制层按卡商 userId 加锁(锁粒度=单卡商,不影响其他卡商), + // 锁包裹整个 @Transactional 生成过程,解锁晚于事务提交, + // 防弱网超时重试/多端重复提交导致同一批卡密生成两次、卡商余额扣两次 + public static final String CARD_KEY_GENERATE_LOCK = "card_key_generate:lock_"; + + // 管理员变更卡商余额(充值/修改):控制层按卡商 userId 加锁(锁粒度=单卡商,不影响其他卡商), + // 防双击/弱网重试导致同一笔变动被应用两次(充值是多加一次钱,修改余额是并发下算出错误的差额) + public static final String CARD_MERCHANT_BALANCE_LOCK = "card_merchant_balance:lock_"; + + // 主播生成红包卡:控制层按主播 userId 加锁,防双击/弱网重试导致扣两次主播积分 + 生成两批卡 + public static final String PROMO_RED_PACKET_GENERATE_LOCK = "promo_red_packet_generate:lock_"; + + // 主播生成 CDKEY:控制层按创建者 userId 加锁,防重复提交导致预扣两次主播余额(金额×次数×生成数量) + public static final String PROMO_CDKEY_GENERATE_LOCK = "promo_cdkey_generate:lock_"; + + // 管理后台生成卡密/空白卡密:控制层按管理员 userId 加锁,防重复提交生成两批卡密 + public static final String RECHARGE_CARD_GENERATE_LOCK = "recharge_card_generate:lock_"; + + // 商城兑换 / 背包转赠:控制层按用户 userId 加锁(锁粒度=单用户,仅串行同一用户的并发请求,不影响其他用户), + // 锁包裹整个 @Transactional Service 调用,确保解锁发生在事务提交之后,杜绝弱网重复提交导致的重复扣费/重复发放 + public static final String SHOPPING_EXCHANGE_LOCK = "shopping_exchange:lock_"; + public static final String SHOPPING_INTEGRATING_CONVERSION_LOCK = "shopping_integrating_conversion:lock_"; + public static final String TRANSFER_LOCK = "user_transfer:lock_"; + + + // 幸运升级 + public static final String UPGRADE_RANGE = "upgrade_range:"; // 概率区间 '业务key:饰品id:用户类型' + public static final String UPGRADE_RANGE_FIXED = "upgrade_range_fixed:"; // 固定概率 '业务key:饰品id:用户类型' + + + /** + * 抽奖box的奖品空间 + * open_box_goods_apace: + * box_id: + * odds_key: + * valua + */ + public static final String OPEN_BOX_GOODS_SPACE = "open_box_goods_apace:"; + public static final String OPEN_BOX_LOTTERY = "open_box_lottery:"; + + // 支付拉单频控:x分钟内y次拉单未支付则封禁z分钟(当前 x=5,y=3,z=10) + // 次数缓存:key=前缀+uid,值=累计未支付拉单次数,TTL=x分钟 + public static final String PAY_PULL_UNPAID_COUNT = "pay:pull:unpaid:count_"; + // 封禁缓存:key=前缀+uid,值=z,TTL=z分钟 + public static final String PAY_PULL_BAN = "pay:pull:ban_"; + + // 流水抽奖:按 userId 加锁,防止并发重复消耗流水/重复发奖 + public static final String TURNOVER_LOTTERY_DRAW_LOCK = "turnover_lottery:draw:lock_"; + // 流水抽奖:按宝箱 lotteryId 加锁,防止不同用户并发导致奖品超发 + public static final String TURNOVER_LOTTERY_DRAW_BOX_LOCK = "turnover_lottery:draw:box:lock_"; +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/consumer/WheelCommissionConsumer.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/consumer/WheelCommissionConsumer.java new file mode 100644 index 0000000..20fb11c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/consumer/WheelCommissionConsumer.java @@ -0,0 +1,55 @@ +package com.ruoyi.admin.consumer; + +import com.rabbitmq.client.Channel; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Map; + +/** + * 游戏消费佣金分发消费者。 + * 下注扣款事务提交后,生产者发消息到 wheel_commission_queue;本消费者消费后递归向上发佣金。 + * 消费失败 nack requeue 重投(佣金失败均为 DB 瞬时异常,恢复后即可成功,不会丢)。 + */ +@Component +@Slf4j +public class WheelCommissionConsumer { + + @Autowired + private TtUserService ttUserService; + + @RabbitListener(queues = DelayedQueueConfig.WHEEL_COMMISSION_QUEUE) + public void consume(Map msg, Channel channel, + @Header(AmqpHeaders.DELIVERY_TAG) long tag) { + try { + Integer userId = (Integer) msg.get("userId"); + BigDecimal consumeAmount = new BigDecimal(msg.get("consumeAmount").toString()); + int sourceCode = (Integer) msg.get("sourceCode"); + TtAccountRecordSource source = Arrays.stream(TtAccountRecordSource.values()) + .filter(s -> s.getCode() == sourceCode) + .findFirst() + .orElseThrow(() -> new RuntimeException("未知消费来源 code=" + sourceCode)); + + ttUserService.distributeGameCommission(userId, consumeAmount, source); + channel.basicAck(tag, false); + } catch (Exception e) { + log.error("佣金分发消费失败, msg={}, nack 重投", msg, e); + try { + // requeue=true:重新入队重试,直到成功消费(DB 恢复后即可成功) + channel.basicNack(tag, false, true); + } catch (IOException ioEx) { + log.error("佣金 nack 失败", ioEx); + } + } + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/AdminBlankCardKeyController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/AdminBlankCardKeyController.java new file mode 100644 index 0000000..06a4026 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/AdminBlankCardKeyController.java @@ -0,0 +1,117 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.mapper.TtBlankCardKeyMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.domain.entity.TtBlankCardKey; +import com.ruoyi.domain.entity.TtRechargeProd; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 白号管理 - 空白卡密管理(管理后台生成,独立表 tt_blank_card_key) + */ +@RestController +@RequestMapping("/admin/blankCardKey") +public class AdminBlankCardKeyController extends BaseController { + + @Autowired + private TtBlankCardKeyMapper blankCardKeyMapper; + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private RedisLock redisLock; + + /** + * 获取充值产品列表(金额选项) + */ + @ApiOperation("获取充值产品列表") + @GetMapping("/rechargeList") + public R> getRechargeList() { + List list = rechargeProdMapper.selectList( + new LambdaQueryWrapper().orderByAsc(TtRechargeProd::getPrice)); + return R.ok(list); + } + + @Log(title = "生成空白卡密", action = "生成空白卡密", businessType = BusinessType.INSERT) + /** + * 生成空白卡密(管理后台,不扣余额) + */ + @ApiOperation("生成空白卡密") + @PostMapping("/generate") + public R> generate(@RequestBody java.util.Map request) { + Integer rechargeListId = (Integer) request.get("rechargeListId"); + Integer num = (Integer) request.get("num"); + + if (rechargeListId == null || num == null || num <= 0 || num > 100) { + return R.fail("参数不合法,生成数量需在1-100之间"); + } + + TtRechargeProd rechargeProd = rechargeProdMapper.selectById(rechargeListId); + if (rechargeProd == null) { + return R.fail("充值产品ID无效"); + } + + // 防重:按管理员 userId 加锁(锁粒度=单管理员),防双击/弱网重试一次生成两批空白卡密 + Long operId = getUserId(); + String lockKey = RedisConstants.RECHARGE_CARD_GENERATE_LOCK + operId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail("空白卡密生成中,请勿重复提交"); + } + try { + String creatorName = getUsername(); + + List cardCodes = new java.util.ArrayList<>(); + for (int i = 0; i < num; i++) { + String code = IdUtils.fastSimpleUUID().substring(0, 16).toUpperCase(); + TtBlankCardKey cardKey = TtBlankCardKey.builder() + .cardCode(code) + .amount(rechargeProd.getPrice()) + .creatorName(creatorName) + .status(0) + .createTime(DateUtils.getNowDate()) + .build(); + blankCardKeyMapper.insert(cardKey); + cardCodes.add(code); + } + + return R.ok(cardCodes, "成功生成 " + cardCodes.size() + " 张空白卡密"); + } finally { + redisLock.unlock(lockKey); + } + } + + /** + * 获取空白卡密列表 + */ + @ApiOperation("获取空白卡密列表") + @GetMapping("/list") + public R> list( + @RequestParam(required = false) String usedByName, + @RequestParam(required = false) String creatorName) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.isNotBlank(usedByName)) { + wrapper.like(TtBlankCardKey::getUsedByName, usedByName); + } + if (StringUtils.isNotBlank(creatorName)) { + wrapper.like(TtBlankCardKey::getCreatorName, creatorName); + } + wrapper.orderByDesc(TtBlankCardKey::getCreateTime); + return R.ok(blankCardKeyMapper.selectList(wrapper)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/LossRebateController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/LossRebateController.java new file mode 100644 index 0000000..4b897d3 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/LossRebateController.java @@ -0,0 +1,91 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.mapper.TtLossRebateMapper; +import com.ruoyi.admin.service.LossRebateService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.entity.TtLossRebate; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 管理端 亏损返水(回血红包) + *

+ * 数据由定时任务 lossRebateService.generateHalfMonthLossRebate() 每半月写入, + * 本页只做「查看 + 对超过单期上限的用户人工发放CDK」。 + */ +@Api(tags = "管理端 亏损返水") +@RestController +@RequestMapping("/admin/lossRebate") +public class LossRebateController extends BaseController { + + private final LossRebateService lossRebateService; + + public LossRebateController(LossRebateService lossRebateService) { + this.lossRebateService = lossRebateService; + } + + @ApiOperation("亏损返水结算明细列表") + @GetMapping("/list") + public PageDataInfo list(@RequestParam(required = false) String period, + @RequestParam(required = false) Integer userId, + @RequestParam(required = false) Integer status) { + startPage(); + List list = lossRebateService.list(period, userId, status); + return getPageData(list); + } + + @Log(title = "亏损返水", action = "发放CDK", businessType = BusinessType.GRANT, + entityClass = TtLossRebate.class, mapperClass = TtLossRebateMapper.class, idField = "id") + @ApiOperation("人工发放CDK(超过单期上限/自动发放失败的记录)") + @PostMapping("/grant") + public R grant(@RequestBody GrantRequest request) { + if (request == null || request.getId() == null) return R.fail("缺少明细ID"); + return lossRebateService.grantManually(request.getId(), request.getAmount(), getUsername()); + } + + @ApiOperation("读取单期返水上限") + @GetMapping("/config") + public AjaxResult getConfig() { + return AjaxResult.success(lossRebateService.getMaxAmount()); + } + + @Log(title = "亏损返水", action = "保存上限", businessType = BusinessType.UPDATE) + @ApiOperation("保存单期返水上限(返水超过该金额不自动发放,转人工;0=不设上限)") + @PostMapping("/config") + public AjaxResult saveConfig(@RequestParam(required = false) BigDecimal maxAmount) { + lossRebateService.saveMaxAmount(maxAmount); + return AjaxResult.success("保存成功,下次结算生效"); + } + + /** 人工发放入参 */ + public static class GrantRequest { + private Long id; + /** 实发金额,不传则按明细里的应返水金额发放 */ + private BigDecimal amount; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/PromoCdkeyRedeemController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/PromoCdkeyRedeemController.java new file mode 100644 index 0000000..1df920a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/PromoCdkeyRedeemController.java @@ -0,0 +1,29 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.service.PromoCdkeyRedeemService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.entity.PromoCdkeyRedeem; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/admin/promoCdkeyRedeem") +public class PromoCdkeyRedeemController extends BaseController { + + private final PromoCdkeyRedeemService promoCdkeyRedeemService; + + public PromoCdkeyRedeemController(PromoCdkeyRedeemService promoCdkeyRedeemService) { + this.promoCdkeyRedeemService = promoCdkeyRedeemService; + } + + @GetMapping("/list") + public PageDataInfo list(PromoCdkeyRedeem promoCdkeyRedeem) { + startPage(); + List list = promoCdkeyRedeemService.queryList(promoCdkeyRedeem); + return getPageData(list); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/ShoppingController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/ShoppingController.java new file mode 100644 index 0000000..31d7815 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/ShoppingController.java @@ -0,0 +1,94 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.admin.service.ShoppingService; +import com.ruoyi.domain.other.ShoppingBody; +import com.ruoyi.domain.vo.ShoppingDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +@Api(tags = "管理端 商城管理") +@RestController +@RequestMapping("/admin/shopping") +public class ShoppingController extends BaseController { + + private final ShoppingService shoppingService; + private final TtOrnamentService ornamentsService; + + public ShoppingController(ShoppingService shoppingService, + TtOrnamentService ornamentsService) { + this.shoppingService = shoppingService; + this.ornamentsService = ornamentsService; + } + + @ApiOperation("商品列表") + @GetMapping("/list") + public PageDataInfo list(ShoppingBody shoppingBody) { + startPage(); + return shoppingService.list(shoppingBody); + } + + @Log(title = "商城列表", action = "上架/下架", businessType = BusinessType.UPDATE) + @PostMapping("/batchPutAwayOrSoldOut/{status}") + public R batchPutAwayOrSoldOut(@RequestBody List ornamentsList, + @PathVariable("status") String status) { + if (StringUtils.isNull(ornamentsList) || ornamentsList.isEmpty()) return R.fail(); + // 上架(status=0)时,market_hash_name 为空或 store_price 为0/NULL 的饰品禁止上架 + if ("0".equals(status)) { + boolean hasInvalid = ornamentsList.stream() + .anyMatch(o -> o.getMarketHashName() == null || o.getMarketHashName().trim().isEmpty()); + if (hasInvalid) { + return R.fail("存在缺少第三方名称(market_hash_name)的饰品,无法上架,请先补全或删除该饰品"); + } + boolean hasZeroPrice = ornamentsList.stream() + .anyMatch(o -> o.getStorePrice() == null || o.getStorePrice().compareTo(BigDecimal.ZERO) <= 0); + if (hasZeroPrice) { + return R.fail("存在商城价格(store_price)为0或未设置的饰品,无法上架,请先设置价格"); + } + } + ornamentsList = ornamentsList.stream().peek(ttOrnaments -> ttOrnaments.setIsPutaway(status)).collect(Collectors.toList()); + if (ornamentsService.updateBatchById(ornamentsList, 1)) return R.ok(true); + return R.fail(false); + } + + @Log(title = "商城列表", action = "一键上架", businessType = BusinessType.UPDATE) + @ApiOperation("一键上架所有商品") + @PostMapping("/putAwayAll") + public R putAwayAll() { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(TtOrnament::getIsPutaway, "1") + // market_hash_name 为空的饰品无法第三方提货,一键上架时跳过 + .isNotNull(TtOrnament::getMarketHashName) + .ne(TtOrnament::getMarketHashName, "") + // 商城价格(store_price)为0或NULL的饰品一键上架时跳过 + .isNotNull(TtOrnament::getStorePrice) + .gt(TtOrnament::getStorePrice, BigDecimal.ZERO) + .set(TtOrnament::getIsPutaway, "0"); + ornamentsService.update(updateWrapper); + return R.ok(true); + } + + @Log(title = "商城列表", action = "一键下架", businessType = BusinessType.UPDATE) + @ApiOperation("一键下架所有商品") + @PostMapping("/soldOutAll") + public R soldOutAll() { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(TtOrnament::getIsPutaway, "0") + .set(TtOrnament::getIsPutaway, "1"); + ornamentsService.update(updateWrapper); + return R.ok(true); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtAdvertisementController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtAdvertisementController.java new file mode 100644 index 0000000..1e6c457 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtAdvertisementController.java @@ -0,0 +1,58 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.service.TtAdvertisementService; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.other.TtAdvertisement; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/advertisement") +public class TtAdvertisementController extends BaseController { + + private final TtAdvertisementService ttAdvertisementService; + + public TtAdvertisementController(TtAdvertisementService ttAdvertisementService) { + this.ttAdvertisementService = ttAdvertisementService; + } + + @GetMapping("/list") + public PageDataInfo list() { + startPage(); + List list = ttAdvertisementService.list(); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtAdvertisement ttAdvertisement = ttAdvertisementService.getById(id); + // ttBanner.setPicture(""); + return R.ok(ttAdvertisement); + } + + @PostMapping + public AjaxResult add(@RequestBody TtAdvertisement ttAdvertisement) { + if (StringUtils.isEmpty(ttAdvertisement.getPicture())) ttAdvertisement.setPicture(""); + else ttAdvertisement.setPicture(RuoYiConfig.getDomainName() + ttAdvertisement.getPicture()); + return toAjax(ttAdvertisementService.save(ttAdvertisement)); + } + + @PutMapping + public AjaxResult edit(@RequestBody TtAdvertisement ttAdvertisement) { + String msg = ttAdvertisementService.updatePicById(ttAdvertisement); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(ttAdvertisementService.removeByIds(Arrays.asList(ids))); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtAnnouncementController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtAnnouncementController.java new file mode 100644 index 0000000..3c55b7c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtAnnouncementController.java @@ -0,0 +1,79 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtAnnouncementMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.service.TtAnnouncementService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.other.TtAnnouncement; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@RestController +@RequestMapping("/admin/announcement") +public class TtAnnouncementController extends BaseController { + + @Autowired + private TtAnnouncementService ttAnnouncementService; + + /** + * 获取公告列表 + */ + @GetMapping("/list") + public PageDataInfo list(TtAnnouncement query) { + startPage(); + List announcementList = ttAnnouncementService.getAnnouncementList(query); + return getPageData(announcementList); + } + + /** + * 获取公告详情 + */ + @GetMapping("/{announcementId}") + public AjaxResult getAnnouncementByAnnouncementId(@PathVariable Integer announcementId) { + Long userId = SecurityUtils.getUserId(); + TtAnnouncement ttAnnouncement = ttAnnouncementService.getAnnouncementByAnnouncementId(userId, announcementId); + return AjaxResult.success(ttAnnouncement); + } + + @Log(title = "公告", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtAnnouncement.class, mapperClass = TtAnnouncementMapper.class) + /** + * 新增公告 + */ + @PostMapping + public AjaxResult addAnnouncement(@RequestBody TtAnnouncement ttAnnouncement) { + return ttAnnouncementService.addAnnouncement(ttAnnouncement) > 0 ? AjaxResult.success("新增成功") : AjaxResult.error("新增失败"); + } + + @Log(title = "公告", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtAnnouncement.class, mapperClass = TtAnnouncementMapper.class) + /** + * 修改公告 + */ + @PutMapping + public AjaxResult editAnnouncement(@RequestBody TtAnnouncement ttAnnouncement) { + return ttAnnouncementService.editAnnouncement(ttAnnouncement) > 0 ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败"); + } + + @Log(title = "公告", action = "删除", businessType = BusinessType.DELETE) + /** + * 删除公告 + */ + @DeleteMapping("/{announcementId}") + public AjaxResult removeAnnouncementByAnnouncementId(@PathVariable Integer announcementId) { + return ttAnnouncementService.removeAnnouncementByAnnouncementId(announcementId) > 0 ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBannerController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBannerController.java new file mode 100644 index 0000000..1c68ec0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBannerController.java @@ -0,0 +1,67 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtBannerMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtBanner; +import com.ruoyi.admin.service.TtBannerService; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/banner") +public class TtBannerController extends BaseController { + + private final TtBannerService bannerService; + + public TtBannerController(TtBannerService bannerService) { + this.bannerService = bannerService; + } + + @Anonymous + @GetMapping("/list") + public PageDataInfo list() { + startPage(); + List list = bannerService.list(); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtBanner ttBanner = bannerService.getById(id); + // ttBanner.setPicture(""); + return R.ok(ttBanner); + } + + @Log(title = "Banner设置", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtBanner.class, mapperClass = TtBannerMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtBanner ttBanner) { + if (StringUtils.isEmpty(ttBanner.getPicture())) ttBanner.setPicture(""); + else ttBanner.setPicture(RuoYiConfig.getDomainName() + ttBanner.getPicture()); + return toAjax(bannerService.save(ttBanner)); + } + + @Log(title = "Banner设置", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtBanner.class, mapperClass = TtBannerMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtBanner ttBanner) { + String msg = bannerService.updateBannerById(ttBanner); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "Banner设置", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(bannerService.removeByIds(Arrays.asList(ids))); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBonusController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBonusController.java new file mode 100644 index 0000000..719310d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBonusController.java @@ -0,0 +1,75 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtBonusMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtBonus; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/bonus") +public class TtBonusController extends BaseController { + + private final TtBonusService bonusService; + + public TtBonusController(TtBonusService bonusService) { + this.bonusService = bonusService; + } + + @GetMapping("/list") + public PageDataInfo list(String type){ + startPage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotEmpty(type)) wrapper.eq(TtBonus::getType, type); + List list = bonusService.list(wrapper); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtBonus bonus = bonusService.getById(id); + bonus.setCoverPicture(""); + return R.ok(bonus); + } + + @Log(title = "彩金", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtBonus.class, mapperClass = TtBonusMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtBonus ttBonus) { + if (StringUtils.isEmpty(ttBonus.getCoverPicture())) ttBonus.setCoverPicture(""); + else ttBonus.setCoverPicture(RuoYiConfig.getDomainName() + ttBonus.getCoverPicture()); + ttBonus.setCreateBy(getUsername()); + ttBonus.setCreateTime(DateUtils.getNowDate()); + return toAjax(bonusService.save(ttBonus)); + } + + @Log(title = "彩金", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtBonus.class, mapperClass = TtBonusMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtBonus ttBonus) { + ttBonus.setUpdateBy(getUsername()); + ttBonus.setUpdateTime(DateUtils.getNowDate()); + String msg = bonusService.updateBonusById(ttBonus); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "彩金", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(bonusService.removeByIds(Arrays.asList(ids))); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxController.java new file mode 100644 index 0000000..42e5020 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxController.java @@ -0,0 +1,157 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.domain.body.BoxMockRequest; +import com.ruoyi.admin.model.BoxMockCache; +import com.ruoyi.admin.service.BoxMockService; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtBoxBody; +import com.ruoyi.domain.vo.BoxPoolStatisticsVO; +import com.ruoyi.domain.vo.TtBoxDataVO; +import com.ruoyi.domain.vo.box.TtBoxUserVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +@Api(tags = "管理端 宝箱管理") +@RestController +@RequestMapping("/admin/box") +public class TtBoxController extends BaseController { + + @Autowired + private TtBoxService boxService; + + @Autowired + private LotteryMachine lotteryMachine; + + @Autowired + private BoxMockService boxMockService; + + @GetMapping("/test") + public AjaxResult test() { + return success(); + } + + @ApiOperation("获取宝箱列表") + @GetMapping("/list") + public PageDataInfo list(TtBoxBody ttBoxBody) { + startPage(); + return boxService.selectTtBoxList(ttBoxBody); + } + + @GetMapping(value = "/{boxId}") + public R getInfo(@PathVariable("boxId") Long boxId) { + TtBox ttBox = boxService.getById(boxId); + return R.ok(ttBox); + } + + @Log(title = "宝箱列表", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtBox.class, mapperClass = TtBoxMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtBox ttBox) { + ttBox.setCreateBy(getUsername()); + ttBox.setCreateTime(DateUtils.getNowDate()); + return toAjax(boxService.save(ttBox)); + } + + @Log(title = "宝箱列表", action = "修改", businessType = BusinessType.UPDATE) + @ApiOperation("修改宝箱") + @PutMapping + public AjaxResult edit(@RequestBody TtBoxDataVO ttBoxDataVO) { + String msg = boxService.updateTtBoxById(ttBoxDataVO); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @ApiOperation("清空当前奖池") + @GetMapping("clearPrizePool/{boxId}") + public R clearPrizePool(@PathVariable("boxId") Integer boxId) { + + lotteryMachine.clearBoxPrizePool(boxId); + + return R.ok("清空当前奖池成功。"); + } + + @Log(title = "宝箱列表", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{boxIds}") + public AjaxResult remove(@PathVariable Long[] boxIds) { + return toAjax(boxService.removeByIds(Arrays.asList(boxIds))); + } + + @Log(title = "宝箱列表", action = "重置宝箱", businessType = BusinessType.CLEAN, + entityClass = TtBox.class, mapperClass = TtBoxMapper.class, idField = "boxId") + @GetMapping("/resetBox/{boxId}") + public AjaxResult resetBox(@PathVariable Integer boxId) { + boxService.isReplenishment(boxId); + return AjaxResult.success(); + } + + @ApiOperation("宝箱统计(按奖池/开箱人类型拆分)") + @GetMapping("/statisticsBoxData/{boxId}") + public R> statisticsBoxData(@PathVariable Integer boxId, @RequestParam(required = false) Date date) { + List poolStatistics = boxService.statisticsBoxData(boxId, date); + return R.ok(poolStatistics); + } + + @ApiOperation("添加宝箱三级爆率玩家范围") + @PostMapping("/createTtBoxThirdExplosiveUsers") + public AjaxResult createTtBoxThirdExplosiveUsers(@RequestBody TtBoxUserVO ttBoxUserVO) { + String msg = boxService.createTtBoxThirdExplosiveUsers(ttBoxUserVO); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @ApiOperation("查询宝箱三级爆率玩家范围") + @GetMapping("/queryTtBoxThirdExplosiveUsers/{boxId}") + public AjaxResult queryBoxUsers(@PathVariable Integer boxId) { + return boxService.queryTtBoxThirdExplosiveUsers(boxId); + } + + @ApiOperation("删除宝箱三级爆率玩家范围") + @PostMapping("/deleteTtBoxThirdExplosiveUsers") + public AjaxResult deleteTtBoxThirdExplosiveUsers(@RequestBody TtBoxUserVO ttBoxUserVO) { + return boxService.deleteTtBoxThirdExplosiveUsers(ttBoxUserVO); + } + + @PostMapping("/export") + public void export(HttpServletResponse response, TtBoxBody ttBoxBody) { + var list = boxService.selectTtBoxList(ttBoxBody); + ExcelUtil util = new ExcelUtil<>(TtBoxDataVO.class); + util.exportExcel(response, list.getRows(), "宝箱列表"); + } + + @ApiOperation("查询开箱临时爆率队列") + @GetMapping("/mockresult/list") + public R> getBoxMockResult(@RequestParam("uid") Integer uid) { + return R.ok(boxMockService.getMockResultList(uid)); + } + + @ApiOperation("设置开箱临时爆率队列") + @PostMapping("/mockresult/list") + public R> updateBoxMockResult(@RequestBody BoxMockRequest request) { + return R.ok(boxMockService.updateMockResultList(request)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxOpenChanceController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxOpenChanceController.java new file mode 100644 index 0000000..ff4e4a2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxOpenChanceController.java @@ -0,0 +1,94 @@ +package com.ruoyi.admin.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.admin.mapper.TtBoxOpenChanceMapper; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.admin.domain.TtBoxOpenChance; +import com.ruoyi.admin.service.TtBoxOpenChanceService; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 开箱机会Controller + * + * @author ruoyi + * @date 2024-07-09 + */ +@RestController +@RequestMapping("/admin/chance") +public class TtBoxOpenChanceController extends BaseController +{ + @Autowired + private TtBoxOpenChanceService ttBoxOpenChanceService; + + /** + * 查询开箱机会列表 + */ + @PreAuthorize("@ss.hasPermi('admin:chance:list')") + @GetMapping("/list") + public TableDataInfo list(TtBoxOpenChance ttBoxOpenChance) + { + startPage(); + List list = ttBoxOpenChanceService.selectTtBoxOpenChanceList(ttBoxOpenChance); + return getDataTable(list); + } + + /** + * 获取开箱机会详细信息 + */ + @PreAuthorize("@ss.hasPermi('admin:chance:query')") + @GetMapping(value = "/{ornamentId}") + public AjaxResult getInfo(@PathVariable("ornamentId") Integer ornamentId) + { + return success(ttBoxOpenChanceService.selectTtBoxOpenChanceByOrnamentId(ornamentId)); + } + + /** + * 新增开箱机会 + */ + @PreAuthorize("@ss.hasPermi('admin:chance:add')") + @Log(title = "开箱机会", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtBoxOpenChance.class, mapperClass = TtBoxOpenChanceMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtBoxOpenChance ttBoxOpenChance) + { + return toAjax(ttBoxOpenChanceService.insertTtBoxOpenChance(ttBoxOpenChance)); + } + + /** + * 修改开箱机会 + */ + @PreAuthorize("@ss.hasPermi('admin:chance:edit')") + @Log(title = "开箱机会", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtBoxOpenChance.class, mapperClass = TtBoxOpenChanceMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtBoxOpenChance ttBoxOpenChance) + { + return toAjax(ttBoxOpenChanceService.updateTtBoxOpenChance(ttBoxOpenChance)); + } + + /** + * 删除开箱机会 + */ + @PreAuthorize("@ss.hasPermi('admin:chance:remove')") + @Log(title = "开箱机会", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ornamentIds}") + public AjaxResult remove(@PathVariable Integer[] ornamentIds) + { + return toAjax(ttBoxOpenChanceService.deleteTtBoxOpenChanceByOrnamentIds(ornamentIds)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxOrnamentsController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxOrnamentsController.java new file mode 100644 index 0000000..2ceb2e6 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxOrnamentsController.java @@ -0,0 +1,132 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtBoxOrnaments; +import com.ruoyi.admin.service.TtBoxOrnamentsService; +import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.constraints.NotEmpty; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Api(tags = "管理端 宝箱物品") +@RestController +@RequestMapping("/admin/boxOrnaments") +public class TtBoxOrnamentsController extends BaseController { + + private final TtBoxOrnamentsService boxOrnamentsService; + + public TtBoxOrnamentsController(TtBoxOrnamentsService boxOrnamentsService) { + this.boxOrnamentsService = boxOrnamentsService; + } + + @ApiOperation("宝箱物品详情") + @GetMapping("/list/{boxId}") + public PageDataInfo list(@PathVariable("boxId") Integer boxId) { + // 在分页之前算总数(避免PageHelper干扰百分比计算) + int[] totals = boxOrnamentsService.sumOdds(boxId); + startPage(); + List list = boxOrnamentsService.selectTtBoxOrnamentsListWithTotals(boxId, + totals[0], totals[1], totals[2], totals[3], totals[4], totals[5], totals[6], totals[7]); + return getPageData(list); + } + + @ApiOperation("宝箱统计数据") + @GetMapping("/globalData/{boxId}") + public R globalData(@PathVariable("boxId") Integer boxId) { + return boxOrnamentsService.globalData(boxId); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtBoxOrnaments ttBoxOrnaments = boxOrnamentsService.getById(id); + return R.ok(ttBoxOrnaments); + } + + @Log(title = "宝箱列表", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtBoxOrnaments.class, mapperClass = TtBoxOrnamentsMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtBoxOrnaments ttBoxOrnaments) { + ttBoxOrnaments.setCreateBy(getUsername()); + ttBoxOrnaments.setCreateTime(DateUtils.getNowDate()); + String msg = boxOrnamentsService.saveBoxOrnaments(ttBoxOrnaments); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "宝箱列表", action = "修改", businessType = BusinessType.UPDATE) + /** + * 修改宝箱物品 + * + * @param ttBoxOrnamentsDataVO + * @return + */ + @PutMapping + public AjaxResult edit(@RequestBody TtBoxOrnamentsDataVO ttBoxOrnamentsDataVO) { + String msg = boxOrnamentsService.updateBoxOrnamentsById(ttBoxOrnamentsDataVO); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "宝箱列表", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{boxId}/{ids}") + public AjaxResult remove(@PathVariable Integer boxId, @PathVariable Long[] ids) { + String msg = boxOrnamentsService.removeBoxOrnamentsByIds(boxId, Arrays.asList(ids)); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @GetMapping("/getProfitMargin/{boxId}") + public AjaxResult getProfitMargin(@PathVariable("boxId") Integer boxId) { + return boxOrnamentsService.getProfitMargin(boxId); + } + + /** + * 校验饰品是否可添加到宝箱(饰品独占机制) + * 始终返回成功响应,通过 data.available 表示是否可用,data.msg 说明被哪个宝箱占用, + * 避免走全局错误拦截导致前端无法读取具体提示。 + */ + @GetMapping("/checkOrnament/{ornamentId}") + public R> checkOrnament(@PathVariable("ornamentId") Long ornamentId) { + String msg = boxOrnamentsService.checkOrnamentUnique(ornamentId, null); + Map result = new HashMap<>(); + result.put("available", StringUtils.isEmpty(msg)); + result.put("msg", msg == null ? "" : msg); + return R.ok(result); + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + @Builder + public static class batchAddParam{ + private Integer boxId; + private Integer partyType; + // private List ornamentsIds; + + @NotEmpty(message = "物品id不能为空") + private List ornamentIds; + } + @Log(title = "宝箱列表", action = "批量填货", businessType = BusinessType.INSERT) + // 宝箱填货 + @ApiOperation("宝箱填货") + @PostMapping("/batchAdd") + public AjaxResult batchAdd(@RequestBody batchAddParam param) { + return boxOrnamentsService.batchAdd(param); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxRecordsController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxRecordsController.java new file mode 100644 index 0000000..ba626d7 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxRecordsController.java @@ -0,0 +1,30 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.domain.other.TtBoxRecordsBody; +import com.ruoyi.domain.vo.TtBoxRecordsDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.page.PageDataInfo; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/admin/boxRecords") +public class TtBoxRecordsController extends BaseController { + + private final TtBoxRecordsService boxRecordsService; + + public TtBoxRecordsController(TtBoxRecordsService boxRecordsService) { + this.boxRecordsService = boxRecordsService; + } + + @GetMapping("/list") + public PageDataInfo list(TtBoxRecordsBody ttBoxRecordsBody) { + startPage(); + List list = boxRecordsService.selectBoxRecordsList(ttBoxRecordsBody); + return getPageData(list); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxTypeController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxTypeController.java new file mode 100644 index 0000000..73a301b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtBoxTypeController.java @@ -0,0 +1,74 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtBoxTypeMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtBoxType; +import com.ruoyi.admin.service.TtBoxTypeService; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/boxType") +@Slf4j +public class TtBoxTypeController extends BaseController { + + private final TtBoxTypeService boxTypeService; + + public TtBoxTypeController(TtBoxTypeService boxTypeService) { + this.boxTypeService = boxTypeService; + } + + @GetMapping("/list") + public PageDataInfo list(@RequestParam(required = false) String isFightType) { + startPage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotEmpty(isFightType)) wrapper.eq(TtBoxType::getIsFightType, isFightType); + List list = boxTypeService.list(wrapper); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtBoxType boxType = boxTypeService.getById(id); + return R.ok(boxType); + } + + @Log(title = "宝箱分类", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtBoxType.class, mapperClass = TtBoxTypeMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtBoxType ttBoxType) { + if (StringUtils.isEmpty(ttBoxType.getIcon())) ttBoxType.setIcon(""); + else ttBoxType.setIcon(RuoYiConfig.getDomainName() + ttBoxType.getIcon()); + ttBoxType.setCreateTime(DateUtils.getNowDate()); + return toAjax(boxTypeService.save(ttBoxType)); + } + + @Log(title = "宝箱分类", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtBoxType.class, mapperClass = TtBoxTypeMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtBoxType ttBoxType) { + ttBoxType.setUpdateTime(DateUtils.getNowDate()); + String msg = boxTypeService.updateBoxTypeById(ttBoxType); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "宝箱分类", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(boxTypeService.removeByIds(Arrays.asList(ids))); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtCardMerchantController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtCardMerchantController.java new file mode 100644 index 0000000..7364dd7 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtCardMerchantController.java @@ -0,0 +1,255 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.user.TtUserVo; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@RestController +@RequestMapping("/admin/cardMerchant") +public class TtCardMerchantController extends BaseController { + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtUserBlendErcashMapper blendErcashMapper; + + @Autowired + private RedisLock redisLock; + + @ApiOperation("获取卡商列表") + @GetMapping("/list") + public PageDataInfo cardMerchantList( + @RequestParam(required = false) Integer userId, + @RequestParam(required = false) String phoneNumber) { + startPage(); + LambdaQueryChainWrapper query = new LambdaQueryChainWrapper<>(ttUserMapper) + .eq(TtUser::getUserType, UserType.CARD_MERCHANT.getCode()) + .and(w -> w.eq(TtUser::getDelFlag, "0").or().isNull(TtUser::getDelFlag)); + if (userId != null) { + query.eq(TtUser::getUserId, userId); + } + if (phoneNumber != null && !phoneNumber.isEmpty()) { + query.like(TtUser::getPhoneNumber, phoneNumber); + } + List merchants = query.orderByDesc(TtUser::getCreateTime).list(); + PageDataInfo pageData = new PageDataInfo<>(); + pageData.setCode(200); + pageData.setTotal(new com.github.pagehelper.PageInfo<>(merchants).getTotal()); + List voList = new ArrayList<>(); + for (TtUser m : merchants) { + TtUserVo vo = TtUserVo.builder() + .userId(m.getUserId()) + .userName(m.getUserName()) + .nickName(m.getNickName()) + .phoneNumber(m.getPhoneNumber()) + .merchantBalance(m.getMerchantBalance() != null ? m.getMerchantBalance() : BigDecimal.ZERO) + .status(m.getStatus()) + .createTime(m.getCreateTime()) + .build(); + voList.add(vo); + } + pageData.setRows(voList); + return pageData; + } + + @Log(title = "卡商充值", action = "确认充值", businessType = BusinessType.GRANT, + entityClass = TtUser.class, mapperClass = TtUserMapper.class, idField = "userId") + @ApiOperation("卡商充值(管理员给卡商增加余额)") + @PostMapping("/recharge") + public AjaxResult cardMerchantRecharge(@RequestBody Map params) { + Integer userId = (Integer) params.get("userId"); + String amountStr = String.valueOf(params.get("amount")); + if (userId == null || amountStr == null) { + return AjaxResult.error("参数不完整"); + } + BigDecimal amount; + try { + amount = new BigDecimal(amountStr); + } catch (NumberFormatException e) { + return AjaxResult.error("金额格式错误"); + } + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + return AjaxResult.error("充值金额必须大于0"); + } + // 防重:按卡商加锁(锁粒度=单卡商,不影响其他卡商)。充值走的是「加钱」语义, + // 同一笔充值被应用两次就是多加一次钱,前端 :loading 是异步渲染挡不住双击/弱网重试 + String lockKey = RedisConstants.CARD_MERCHANT_BALANCE_LOCK + userId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return AjaxResult.error("该卡商余额正在变更,请勿重复提交"); + } + try { + TtUser user = userService.getById(userId); + if (user == null) { + return AjaxResult.error("用户不存在"); + } + if (!UserType.CARD_MERCHANT.getCode().equals(user.getUserType())) { + return AjaxResult.error("该用户不是卡商"); + } + // 直接操作 merchant_balance 字段,不影响 account_amount/account_credits + int count = ttUserMapper.addMerchantBalance(Long.valueOf(userId), amount); + if (count <= 0) { + return AjaxResult.error("充值失败"); + } + BigDecimal newBalance = (user.getMerchantBalance() != null ? user.getMerchantBalance() : BigDecimal.ZERO).add(amount); + // 操作人 + 登录IP 记入备注,便于审计是谁给卡商充值的 + String operName = getUsername(); + String operIp = getLoginUser().getIpaddr(); + if (StringUtils.isEmpty(operIp)) { + operIp = IpUtils.getIpAddr(); + } + String remark = TtAccountRecordSource.CARD_MERCHANT_RECHARGE.getMsg() + + "(操作人:" + operName + ",登录IP:" + operIp + ")"; + // 记录流水到 blend_ercash(仅用于卡商充值记录展示) + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + .amount(BigDecimal.ZERO) + .finalAmount(user.getAccountAmount()) + .credits(amount) + .finalCredits(newBalance) + .total(amount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.CARD_MERCHANT_RECHARGE.getCode()) + .remark(remark) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + blendErcashMapper.insert(blendErcash); + return AjaxResult.success("充值成功,当前卡商余额:" + newBalance); + } finally { + redisLock.unlock(lockKey); + } + } + + @Log(title = "卡商充值", action = "确认修改", businessType = BusinessType.UPDATE, + entityClass = TtUser.class, mapperClass = TtUserMapper.class, idField = "userId") + @ApiOperation("修改卡商余额(直接设为指定值,按差额写审计流水)") + @PostMapping("/updateBalance") + @Transactional(rollbackFor = Exception.class) + public AjaxResult cardMerchantUpdateBalance(@RequestBody Map params) { + Integer userId = (Integer) params.get("userId"); + Object balanceObj = params.get("balance"); + if (userId == null || balanceObj == null) { + return AjaxResult.error("参数不完整"); + } + BigDecimal newBalance; + try { + newBalance = new BigDecimal(String.valueOf(balanceObj)); + } catch (NumberFormatException e) { + return AjaxResult.error("余额格式错误"); + } + if (newBalance.compareTo(BigDecimal.ZERO) < 0) { + return AjaxResult.error("卡商余额不能为负数"); + } + // 防重/防并发:按卡商加锁,锁覆盖「读旧余额 → 写新余额 → 记流水」全过程。 + // 否则两个并发请求会各自读到同一个旧余额、各自算出差额,导致差额被加两次 + String lockKey = RedisConstants.CARD_MERCHANT_BALANCE_LOCK + userId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return AjaxResult.error("该卡商余额正在变更,请勿重复提交"); + } + try { + TtUser user = userService.getById(userId); + if (user == null) { + return AjaxResult.error("用户不存在"); + } + if (!UserType.CARD_MERCHANT.getCode().equals(user.getUserType())) { + return AjaxResult.error("该用户不是卡商"); + } + + BigDecimal oldBalance = user.getMerchantBalance() != null ? user.getMerchantBalance() : BigDecimal.ZERO; + BigDecimal delta = newBalance.subtract(oldBalance); + if (delta.compareTo(BigDecimal.ZERO) == 0) { + return AjaxResult.error("新余额与当前卡商余额相同,无需修改"); + } + + // 按绝对值写入(而不是 += 差额):天然幂等,弱网重试/重复提交只会把余额写成同一个值; + // 只 set merchant_balance 一列,不触碰 account_amount / account_credits + LambdaUpdateWrapper balanceUpdate = new LambdaUpdateWrapper<>(); + balanceUpdate.eq(TtUser::getUserId, userId) + .set(TtUser::getMerchantBalance, newBalance); + if (ttUserMapper.update(null, balanceUpdate) <= 0) { + return AjaxResult.error("修改失败,请刷新后重试"); + } + + // 操作人 + 登录IP + 变更前后值 记入备注,便于审计是谁把卡商余额改成了多少 + String operName = getUsername(); + String operIp = getLoginUser().getIpaddr(); + if (StringUtils.isEmpty(operIp)) { + operIp = IpUtils.getIpAddr(); + } + String remark = TtAccountRecordSource.ADMIN_ADJUST_MERCHANT_BALANCE.getMsg() + + "(" + oldBalance.toPlainString() + "→" + newBalance.toPlainString() + + ",操作人:" + operName + ",登录IP:" + operIp + ")"; + // 与 45/46/55 同一记账约定:amount=0、credits 携带卡商余额变动、finalCredits 为变动后卡商余额 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + .amount(BigDecimal.ZERO) + .finalAmount(user.getAccountAmount()) + .credits(delta) + .finalCredits(newBalance) + .total(delta) + .type(delta.compareTo(BigDecimal.ZERO) > 0 + ? TtAccountRecordType.INPUT.getCode() + : TtAccountRecordType.OUTPUT.getCode()) + .source(TtAccountRecordSource.ADMIN_ADJUST_MERCHANT_BALANCE.getCode()) + .remark(remark) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + blendErcashMapper.insert(blendErcash); + return AjaxResult.success("修改成功,当前卡商余额:" + newBalance.toPlainString()); + } finally { + redisLock.unlock(lockKey); + } + } + + @ApiOperation("卡商充值记录") + @GetMapping("/rechargeLog") + public PageDataInfo cardMerchantRechargeLog(@RequestParam(required = false) Integer userId) { + LambdaQueryChainWrapper wrapper = new LambdaQueryChainWrapper<>(blendErcashMapper); + // 包含四种类型:充值、生成卡密消费、销毁退款、管理员调整余额 + wrapper.in(TtUserBlendErcash::getSource, + TtAccountRecordSource.CARD_MERCHANT_RECHARGE.getCode(), // 46 充值 + TtAccountRecordSource.CARD_MERCHANT_GENERATE.getCode(), // 45 生成卡密消费 + TtAccountRecordSource.CARD_MERCHANT_DESTROY_REFUND.getCode(), // 55 销毁退款 + TtAccountRecordSource.ADMIN_ADJUST_MERCHANT_BALANCE.getCode()); // 67 管理员调整余额 + if (userId != null) { + wrapper.eq(TtUserBlendErcash::getUserId, userId); + } + wrapper.orderByDesc(TtUserBlendErcash::getCreateTime); + startPage(); + List records = wrapper.list(); + return getPageData(records); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtContentController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtContentController.java new file mode 100644 index 0000000..a1b7a27 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtContentController.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtContentMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtContent; +import com.ruoyi.admin.service.TtContentService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/content") +public class TtContentController extends BaseController { + + private final TtContentService contentService; + + public TtContentController(TtContentService contentService) { + this.contentService = contentService; + } + + @GetMapping("/list") + public PageDataInfo list(TtContent ttContent) { + startPage(); + List list = contentService.queryList(ttContent); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Long id) { + return R.ok(contentService.getById(id)); + } + + @Log(title = "文章", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtContent.class, mapperClass = TtContentMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtContent ttContent) { + ttContent.setCreateTime(DateUtils.getNowDate()); + return toAjax(contentService.save(ttContent)); + } + + @Log(title = "文章", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtContent.class, mapperClass = TtContentMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtContent ttContent) { + ttContent.setUpdateTime(DateUtils.getNowDate()); + return toAjax(contentService.updateById(ttContent)); + } + + @Log(title = "文章", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(contentService.removeByIds(Arrays.asList(ids))); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtContentTypeController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtContentTypeController.java new file mode 100644 index 0000000..722487f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtContentTypeController.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtContentTypeMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtContentType; +import com.ruoyi.admin.service.TtContentTypeService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/contentType") +public class TtContentTypeController extends BaseController { + + private final TtContentTypeService contentTypeService; + + public TtContentTypeController(TtContentTypeService contentTypeService) { + this.contentTypeService = contentTypeService; + } + + @GetMapping("/list") + public PageDataInfo list(TtContentType ttContentType) { + startPage(); + List list = contentTypeService.queryList(ttContentType); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Long id) { + return R.ok(contentTypeService.getById(id)); + } + + @Log(title = "文章类型", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtContentType.class, mapperClass = TtContentTypeMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtContentType ttContentType) { + ttContentType.setCreateTime(DateUtils.getNowDate()); + return toAjax(contentTypeService.save(ttContentType)); + } + + @Log(title = "文章类型", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtContentType.class, mapperClass = TtContentTypeMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtContentType ttContentType) { + ttContentType.setUpdateTime(DateUtils.getNowDate()); + return toAjax(contentTypeService.updateById(ttContentType)); + } + + @Log(title = "文章类型", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(contentTypeService.removeByIds(Arrays.asList(ids))); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtDeliveryRecordController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtDeliveryRecordController.java new file mode 100644 index 0000000..50ef26e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtDeliveryRecordController.java @@ -0,0 +1,62 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.service.TtDeliveryRecordService; +import com.ruoyi.domain.other.TtDeliveryApplyBody; +import com.ruoyi.domain.vo.DeliveryApplyVO; +import com.ruoyi.domain.other.TtDeliveryRecordBody; +import com.ruoyi.domain.vo.TtDeliveryRecordDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Api(tags = "管理端 发货管理") +@RestController +@RequestMapping("/admin/deliverGoods") +public class TtDeliveryRecordController extends BaseController { + + private final TtDeliveryRecordService deliveryRecordService; + + public TtDeliveryRecordController(TtDeliveryRecordService deliveryRecordService) { + this.deliveryRecordService = deliveryRecordService; + } + + @ApiOperation("发货申请列表") + @GetMapping("/getDeliveryApplyList") + public PageDataInfo getDeliveryApplyList(TtDeliveryApplyBody deliveryApplyBody) { + startPage(); + List list = deliveryRecordService.getDeliveryApplyList(deliveryApplyBody); + return getPageData(list); + } + + @ApiOperation("退回发货申请") + @PostMapping("/deliveryFail") + public R deliveryFail(@RequestParam("deliveryRecordId") Integer deliveryRecordId, @RequestParam("message") String message) { + String msg = deliveryRecordService.deliveryFail(deliveryRecordId, message); + return StringUtils.isEmpty(msg) ? R.ok(true, "操作成功!") : R.fail(false, msg); + } + + @Log(title = "发货记录", action = "确认发货", businessType = BusinessType.GRANT) + @ApiOperation("审核通过发货申请") + @PostMapping("/deliveryApprove") + public R deliveryApprove(@RequestParam("deliveryRecordId") Integer deliveryRecordId) { + String msg = deliveryRecordService.deliveryApprove(deliveryRecordId); + return StringUtils.isEmpty(msg) ? R.ok(true, "操作成功!") : R.fail(false, msg); + } + + @ApiOperation("发货记录申请") + @GetMapping("/getDeliveryRecordList") + public PageDataInfo getDeliveryRecordList(TtDeliveryRecordBody deliveryRecordBody) { + startPage(); + List list = deliveryRecordService.getDeliveryRecordList(deliveryRecordBody); + return getPageData(list); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFightController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFightController.java new file mode 100644 index 0000000..746b1c2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFightController.java @@ -0,0 +1,39 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.admin.service.TtFightService; +import com.ruoyi.domain.vo.FightBoxDataVO; +import com.ruoyi.domain.other.TtFightBody; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.page.PageDataInfo; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/admin/fight") +public class TtFightController extends BaseController { + + private final TtFightService fightService; + + public TtFightController(TtFightService fightService) { + this.fightService = fightService; + } + + @GetMapping("/list") + public PageDataInfo list(TtFightBody ttFightBody) { + startPage(); + List list = fightService.selectFightList(ttFightBody); + return getPageData(list); + } + + @GetMapping("/getFightBoxList/{fightId}") + public PageDataInfo getFightBoxList(@PathVariable("fightId") Integer fightId) { + startPage(); + List list = fightService.selectFightBoxList(fightId); + return getPageData(list); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFightRankingRewardController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFightRankingRewardController.java new file mode 100644 index 0000000..e75bc57 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFightRankingRewardController.java @@ -0,0 +1,109 @@ +package com.ruoyi.admin.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; + +import com.ruoyi.common.utils.StringUtils; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.domain.other.TtFightRankingReward; +import com.ruoyi.admin.service.TtFightRankingRewardService; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 对战奖励金额Controller + * + * @author ruoyi + * @date 2024-07-04 + */ +@RestController +@RequestMapping("/admin/reward") +public class TtFightRankingRewardController extends BaseController +{ + @Autowired + private TtFightRankingRewardService ttFightRankingRewardService; + + /** + * 查询对战奖励金额列表 + */ + @PreAuthorize("@ss.hasPermi('admin:reward:list')") + @GetMapping("/list") + public TableDataInfo list(TtFightRankingReward ttFightRankingReward) + { + startPage(); + List list = ttFightRankingRewardService.selectTtFightRankingRewardList(ttFightRankingReward); + return getDataTable(list); + } + + /** + * 获取对战奖励金额详细信息 + */ + @PreAuthorize("@ss.hasPermi('admin:reward:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Integer id) + { + return success(ttFightRankingRewardService.selectTtFightRankingRewardById(id)); + } + + /** + * 新增对战奖励金额 + */ + // @PreAuthorize("@ss.hasPermi('admin:reward:add')") + // @Log(title = "对战奖励金额", businessType = BusinessType.INSERT) + // @PostMapping + // public AjaxResult add(@RequestBody TtFightRankingReward ttFightRankingReward) + // { + // return toAjax(ttFightRankingRewardService.insertTtFightRankingReward(ttFightRankingReward)); + // } + + @PreAuthorize("@ss.hasPermi('admin:reward:add')") + @Log(title = "对战排行榜奖励", action = "生成排名奖励", businessType = BusinessType.UPDATE) + @PostMapping("/generateRankingReward") + public AjaxResult generatePromotionLevel() { + String msg = ttFightRankingRewardService.generateRankingReward(); + return StringUtils.isEmpty(msg) ? AjaxResult.success("生成成功,请修改参数!") : AjaxResult.error(msg); + } + + /** + * 修改对战奖励金额 + */ + @PreAuthorize("@ss.hasPermi('admin:reward:edit')") + @Log(title = "对战排行榜奖励", action = "修改", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody TtFightRankingReward ttFightRankingReward) + { + return toAjax(ttFightRankingRewardService.updateTtFightRankingReward(ttFightRankingReward)); + } + + /** + * 删除对战奖励金额 + */ + // @PreAuthorize("@ss.hasPermi('admin:reward:remove')") + // @Log(title = "对战奖励金额", businessType = BusinessType.DELETE) + // @DeleteMapping("/{ids}") + // public AjaxResult remove(@PathVariable Integer[] ids) + // { + // return toAjax(ttFightRankingRewardService.deleteTtFightRankingRewardByIds(ids)); + // } + + @PreAuthorize("@ss.hasPermi('admin:reward:remove')") + @Log(title = "对战排行榜奖励", action = "重置", businessType = BusinessType.CLEAN) + @DeleteMapping("/truncateRankingReward") + public AjaxResult truncateRankingReward() { + ttFightRankingRewardService.truncateRankingReward(); + return AjaxResult.success("重置成功"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFightResultController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFightResultController.java new file mode 100644 index 0000000..8a82f3e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFightResultController.java @@ -0,0 +1,33 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.service.TtFightResultService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.vo.FightResultDataVO; +import com.ruoyi.domain.vo.fight.FightResultVO; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin/fightResult") +public class TtFightResultController { + + private final TtFightResultService fightResultService; + + public TtFightResultController(TtFightResultService fightResultService) { + this.fightResultService = fightResultService; + } + + // @GetMapping("/getFightResult/{fightId}") + // public R getFightResult(@PathVariable("fightId") Integer fightId) { + // FightResultDataVO fightResultDataVO = fightResultService.getFightResult(fightId); + // return R.ok(fightResultDataVO); + // } + + @GetMapping("/getFightResult/{fightId}") + public R getFightResult(@PathVariable("fightId") Integer fightId) { + FightResultVO fightResultVO = fightResultService.getFightResult(fightId); + return R.ok(fightResultVO); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFirstRechargeController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFirstRechargeController.java new file mode 100644 index 0000000..3a9ab7b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtFirstRechargeController.java @@ -0,0 +1,107 @@ +package com.ruoyi.admin.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.admin.mapper.TtFirstRechargeMapper; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.domain.other.TtFirstRecharge; +import com.ruoyi.admin.service.TtFirstRechargeService; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 首充赠送Controller + * + * @author ruoyi + * @date 2024-06-21 + */ +@RestController +@RequestMapping("/admin/recharge") +public class TtFirstRechargeController extends BaseController +{ + @Autowired + private TtFirstRechargeService ttFirstRechargeService; + + /** + * 查询首充赠送列表 + */ + @PreAuthorize("@ss.hasPermi('admin:recharge:list')") + @GetMapping("/list") + public TableDataInfo list(TtFirstRecharge ttFirstRecharge) + { + startPage(); + List list = ttFirstRechargeService.selectTtFirstRechargeList(ttFirstRecharge); + return getDataTable(list); + } + + /** + * 导出首充赠送列表 + */ + @PreAuthorize("@ss.hasPermi('admin:recharge:export')") + @Log(title = "首充赠送", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, TtFirstRecharge ttFirstRecharge) + { + List list = ttFirstRechargeService.selectTtFirstRechargeList(ttFirstRecharge); + ExcelUtil util = new ExcelUtil(TtFirstRecharge.class); + util.exportExcel(response, list, "首充赠送数据"); + } + + /** + * 获取首充赠送详细信息 + */ + @PreAuthorize("@ss.hasPermi('admin:recharge:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Integer id) + { + return success(ttFirstRechargeService.selectTtFirstRechargeById(id)); + } + + /** + * 新增首充赠送 + */ + @PreAuthorize("@ss.hasPermi('admin:recharge:add')") + @Log(title = "首充赠送", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtFirstRecharge.class, mapperClass = TtFirstRechargeMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtFirstRecharge ttFirstRecharge) + { + return toAjax(ttFirstRechargeService.insertTtFirstRecharge(ttFirstRecharge)); + } + + /** + * 修改首充赠送 + */ + @PreAuthorize("@ss.hasPermi('admin:recharge:edit')") + @Log(title = "首充赠送", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtFirstRecharge.class, mapperClass = TtFirstRechargeMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtFirstRecharge ttFirstRecharge) + { + return toAjax(ttFirstRechargeService.updateTtFirstRecharge(ttFirstRecharge)); + } + + /** + * 删除首充赠送 + */ + @PreAuthorize("@ss.hasPermi('admin:recharge:remove')") + @Log(title = "首充赠送", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) + { + return toAjax(ttFirstRechargeService.deleteTtFirstRechargeByIds(ids)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtMessageController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtMessageController.java new file mode 100644 index 0000000..3b91c67 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtMessageController.java @@ -0,0 +1,67 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.domain.other.TtMessage; +import com.ruoyi.admin.service.TtMessageService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/message") +public class TtMessageController extends BaseController { + + private final TtMessageService messageService; + + public TtMessageController(TtMessageService messageService) { + this.messageService = messageService; + } + + @GetMapping("/list") + public PageDataInfo list(TtMessage ttMessage) { + startPage(); + List list = messageService.queryList(ttMessage); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + return R.ok(messageService.getById(id)); + } + + @PostMapping + public AjaxResult add(@RequestBody TtMessage ttMessage) { + ttMessage.setCreateTime(DateUtils.getNowDate()); + return toAjax(messageService.save(ttMessage)); + } + + @PutMapping + public AjaxResult edit(@RequestBody TtMessage ttMessage) { + ttMessage.setUpdateTime(DateUtils.getNowDate()); + return toAjax(messageService.updateById(ttMessage)); + } + + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + String msg = messageService.delByIds(Arrays.asList(ids)); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @PostMapping("/singleMessage/{messageId}/{userIds}") + public AjaxResult singleMessage(@PathVariable("messageId") Integer messageId, @PathVariable Integer[] userIds){ + String msg = messageService.singleMessage(messageId, Arrays.asList(userIds)); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @PostMapping("/massMessaging/{messageId}") + public AjaxResult massMessaging(@PathVariable("messageId") Integer messageId){ + String msg = messageService.massMessaging(messageId); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOfficialRollConfigController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOfficialRollConfigController.java new file mode 100644 index 0000000..bd6396d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOfficialRollConfigController.java @@ -0,0 +1,96 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtOfficialRollConfigMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.service.TtOfficialRollConfigService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.roll.TtOfficialRollConfig; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Api(tags = "管理端 官方周期Roll配置") +@RestController +@RequestMapping("/admin/officialRollConfig") +public class TtOfficialRollConfigController extends BaseController { + + private final TtOfficialRollConfigService configService; + + public TtOfficialRollConfigController(TtOfficialRollConfigService configService) { + this.configService = configService; + } + + @ApiOperation("配置列表") + @GetMapping("/list") + public PageDataInfo list( + @RequestParam(value = "configName", required = false) String configName, + @RequestParam(value = "periodType", required = false) String periodType, + @RequestParam(value = "status", required = false) String status) { + startPage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotEmpty(configName)) wrapper.like(TtOfficialRollConfig::getConfigName, configName); + if (StringUtils.isNotEmpty(periodType)) wrapper.eq(TtOfficialRollConfig::getPeriodType, periodType); + if (StringUtils.isNotEmpty(status)) wrapper.eq(TtOfficialRollConfig::getStatus, status); + wrapper.orderByAsc(TtOfficialRollConfig::getSortBy).orderByDesc(TtOfficialRollConfig::getCreateTime); + List configList = configService.list(wrapper); + return getPageData(configList); + } + + @ApiOperation("配置详情") + @GetMapping("/{id}") + public R getInfo(@PathVariable("id") Integer id) { + return R.ok(configService.getById(id)); + } + + @Log(title = "官方周期Roll配置", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtOfficialRollConfig.class, mapperClass = TtOfficialRollConfigMapper.class) + @ApiOperation("新增配置") + @PostMapping + public AjaxResult add(@RequestBody TtOfficialRollConfig config) { + config.setCreateBy(getUsername()); + config.setCreateTime(DateUtils.getNowDate()); + return configService.addConfig(config); + } + + @Log(title = "官方周期Roll配置", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtOfficialRollConfig.class, mapperClass = TtOfficialRollConfigMapper.class) + @ApiOperation("编辑配置") + @PutMapping + public AjaxResult edit(@RequestBody TtOfficialRollConfig config) { + config.setUpdateBy(getUsername()); + config.setUpdateTime(DateUtils.getNowDate()); + return configService.editConfig(config); + } + + @Log(title = "官方周期Roll配置", action = "删除", businessType = BusinessType.DELETE) + @ApiOperation("删除配置") + @DeleteMapping("/{id}") + public AjaxResult remove(@PathVariable("id") Integer id) { + return configService.removeById(id) ? AjaxResult.success() : AjaxResult.error("删除失败"); + } + + @ApiOperation("启用/停用配置") + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody TtOfficialRollConfig config) { + if (config.getId() == null) return AjaxResult.error("配置ID不能为空"); + if (!"0".equals(config.getStatus()) && !"1".equals(config.getStatus())) { + return AjaxResult.error("状态不合法"); + } + TtOfficialRollConfig update = new TtOfficialRollConfig(); + update.setId(config.getId()); + update.setStatus(config.getStatus()); + update.setUpdateBy(getUsername()); + update.setUpdateTime(DateUtils.getNowDate()); + return configService.updateById(update) ? AjaxResult.success() : AjaxResult.error("操作失败"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOrderController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOrderController.java new file mode 100644 index 0000000..939dafe --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOrderController.java @@ -0,0 +1,33 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.service.TtOrderService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.dto.sys.OrderQueryCondition; +import com.ruoyi.domain.entity.TtOrder; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Api(tags = "管理端 订单管理") +@RestController +@RequestMapping("/admin/order") +public class TtOrderController extends BaseController { + + private final TtOrderService ttOrderService; + + public TtOrderController(TtOrderService ttOrderService) { + this.ttOrderService = ttOrderService; + } + + @ApiOperation("网站支付订单查询") + @PostMapping("/list") + public R list(@RequestBody @Validated OrderQueryCondition condition) { + return ttOrderService.adminList(condition); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOrnamentsController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOrnamentsController.java new file mode 100644 index 0000000..4aab26b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOrnamentsController.java @@ -0,0 +1,106 @@ +package com.ruoyi.admin.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.other.TtOrnamentsBody; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.domain.other.TtOrnamentsPrice; +import com.ruoyi.domain.vo.TtOrnamentVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; + +@Api(tags = "管理端 饰品管理") +@RestController +@RequestMapping("/admin/ornaments") +public class TtOrnamentsController extends BaseController { + + @Autowired + private TtOrnamentService ornamentsService; + + @Autowired + private TtOrnamentService ttOrnamentService; + + // 饰品列表多条件筛选 + @ApiOperation("饰品列表多条件筛选") + @GetMapping("/list") + public R list(TtOrnamentsBody param) { + Page ttOrnamentVOPage = ttOrnamentService.listByParam(param); + return R.ok(ttOrnamentVOPage); + } + + // TODO: 2024/3/29 !!!!!! + // 导出饰品数据 + @Log(title = "饰品数据", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, TtOrnamentsBody ttOrnamentsBody) { + List list = ttOrnamentService.listByParam(ttOrnamentsBody).getRecords(); + ExcelUtil util = new ExcelUtil<>(TtOrnamentVO.class); + util.exportExcel(response, list, "饰品数据数据"); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Long id) { + return R.ok(ornamentsService.getById(id)); + } + + @Log(title = "饰品列表", action = "发放饰品", businessType = BusinessType.GRANT) + @ApiOperation("饰品发放") + @PostMapping("/grantOrnaments/{userId}/{ornamentsId}/{num}") + public AjaxResult grantOrnaments(@PathVariable("userId") Integer userId, + @PathVariable("ornamentsId") Long ornamentsId, + @RequestParam(name = "ornamentsLevelId", required = false) Integer ornamentsLevelId, + @PathVariable("num") Integer num) { + return ornamentsService.grantOrnaments(userId, ornamentsId, ornamentsLevelId, num); + } + + @PutMapping("/updateOrnamentPrice") + public AjaxResult updateOrnamentPrice(@RequestBody TtOrnamentsPrice ttOrnamentsPrice) { + return ornamentsService.updateOrnamentPrice(ttOrnamentsPrice.getId(), ttOrnamentsPrice.getPrice()); + } + + @Log(title = "饰品列表", action = "新增饰品", businessType = BusinessType.INSERT, + entityClass = TtOrnament.class, mapperClass = TtOrnamentMapper.class) + @ApiOperation("新增饰品") + @PostMapping("/addTtOrnament") + public AjaxResult add(@RequestBody TtOrnament ttOrnament) { + if (ttOrnament.getId() == null) { + ttOrnament.setId(System.currentTimeMillis()); + } + return toAjax(ornamentsService.save(ttOrnament)); + } + + @Log(title = "饰品列表", action = "编辑", businessType = BusinessType.UPDATE, + entityClass = TtOrnament.class, mapperClass = TtOrnamentMapper.class) + @ApiOperation("修改饰品") + @PostMapping("/editTtOrnament") + public AjaxResult edit(@RequestBody TtOrnament ttOrnament) { + return toAjax(ornamentsService.updateById(ttOrnament)); + } + + @Log(title = "饰品列表", action = "删除", businessType = BusinessType.DELETE) + @ApiOperation("删除饰品(支持批量)") + @DeleteMapping("/{ids}") + public AjaxResult delete(@PathVariable("ids") List ids) { + if (ids == null || ids.isEmpty()) { + return AjaxResult.error("请选择要删除的饰品"); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(TtOrnament::getId, ids); + return toAjax(ornamentsService.remove(wrapper)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOrnamentsLevelController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOrnamentsLevelController.java new file mode 100644 index 0000000..f8e259f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtOrnamentsLevelController.java @@ -0,0 +1,65 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import com.ruoyi.admin.service.TtOrnamentsLevelService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Api(tags = "管理端 饰品等级信息") +@RestController +@RequestMapping("/admin/ornamentsLevel") +public class TtOrnamentsLevelController extends BaseController { + + private final TtOrnamentsLevelService ornamentsLevelService; + + public TtOrnamentsLevelController(TtOrnamentsLevelService ornamentsLevelService) { + this.ornamentsLevelService = ornamentsLevelService; + } + + @ApiOperation("获取所有饰品等级背景") + @GetMapping("/list") + public PageDataInfo list() { + startPage(); + List list = ornamentsLevelService.list(); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtOrnamentsLevel ornamentsLevel = ornamentsLevelService.getById(id); + ornamentsLevel.setLevelImg(""); + return R.ok(ornamentsLevel); + } + + @Log(title = "饰品等级", action = "新增", businessType = BusinessType.INSERT) + @PostMapping("generateOrnamentsLevel/{num}") + public AjaxResult generateOrnamentsLevel(@PathVariable("num") Integer num) { + String msg = ornamentsLevelService.generateOrnamentsLevel(num); + return StringUtils.isEmpty(msg) ? AjaxResult.success("生成成功,请修改参数!") : AjaxResult.error(msg); + } + + @Log(title = "饰品等级", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtOrnamentsLevel.class, mapperClass = TtOrnamentsLevelMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtOrnamentsLevel ttOrnamentsLevel) { + String msg = ornamentsLevelService.updateOrnamentsLevelById(ttOrnamentsLevel); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @PostMapping + public AjaxResult truncateOrnamentsLevel() { + ornamentsLevelService.truncateOrnamentsLevel(); + return AjaxResult.success("重置成功,请重新设置!"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtPromoTurnoverController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtPromoTurnoverController.java new file mode 100644 index 0000000..a184c96 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtPromoTurnoverController.java @@ -0,0 +1,47 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.service.TtPromoTurnoverService; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.param.promotion.AnchorDailyTurnoverParam; +import com.ruoyi.domain.param.promotion.AnchorRechargeParam; +import com.ruoyi.domain.vo.promotion.AnchorDailyTurnoverVo; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +@RequestMapping("/admin/promo") +public class TtPromoTurnoverController extends BaseController { + + @Autowired + private TtPromoTurnoverService ttPromoTurnoverService; + + @ApiOperation("获取主播推广数据") + @GetMapping("/getAnchorPromotionData") + public PageDataInfo getAnchorPromotionData(AnchorDailyTurnoverParam param) { + List list = ttPromoTurnoverService.getAnchorDailyTurnover(param); + PageDataInfo pageDataInfo = new PageDataInfo(); + pageDataInfo.setTotal(list.size()); + pageDataInfo.setCode(HttpStatus.SUCCESS); + pageDataInfo.setMsg("查询成功"); + int num = param.getNum() != null ? param.getNum() : 1; + int size = param.getSize() != null ? param.getSize() : 10; + int fromIndex = (num - 1) * size; + int toIndex = Math.min(fromIndex + size, list.size()); + pageDataInfo.setRows(list.subList(fromIndex, toIndex)); + return pageDataInfo; + } + + @ApiOperation("主播充值") + @PostMapping("/recharge") + public AjaxResult recharge(@RequestBody AnchorRechargeParam param) { + return ttPromoTurnoverService.recharge(param); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtPromotionLevelController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtPromotionLevelController.java new file mode 100644 index 0000000..d8854f5 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtPromotionLevelController.java @@ -0,0 +1,69 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtPromotionLevelMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.entity.TtPromotionLevel; +import com.ruoyi.admin.service.TtPromotionLevelService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/admin/promotionLevel") +public class TtPromotionLevelController extends BaseController { + + private final TtPromotionLevelService promotionLevelService; + + public TtPromotionLevelController(TtPromotionLevelService promotionLevelService) { + this.promotionLevelService = promotionLevelService; + } + + @GetMapping("/list") + public PageDataInfo list() { + // 推广等级总量不多,后台设置页未接入分页组件,改为全量返回 + List list = promotionLevelService.list(); + PageDataInfo data = new PageDataInfo<>(); + data.setRows(list); + data.setTotal(list == null ? 0 : list.size()); + data.setCode(200); + data.setMsg("查询成功"); + return data; + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtPromotionLevel ttPromotionLevel = promotionLevelService.getById(id); + ttPromotionLevel.setIcon(""); + return R.ok(ttPromotionLevel); + } + + @Log(title = "推广等级设置", action = "生成推广列表", businessType = BusinessType.INSERT) + @PostMapping("generatePromotionLevel/{num}") + public AjaxResult generatePromotionLevel(@PathVariable("num") Integer num) { + String msg = promotionLevelService.generateVipLevel(num); + return StringUtils.isEmpty(msg) ? AjaxResult.success("生成成功,请修改参数!") : AjaxResult.error(msg); + } + + @Log(title = "推广等级设置", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtPromotionLevel.class, mapperClass = TtPromotionLevelMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtPromotionLevel ttPromotionLevel) { + ttPromotionLevel.setUpdateTime(DateUtils.getNowDate()); + String msg = promotionLevelService.updatePromotionLevelById(ttPromotionLevel); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "推广等级设置", action = "重置", businessType = BusinessType.CLEAN) + @PostMapping + public AjaxResult truncateVipLevel() { + promotionLevelService.truncatePromotionLevel(); + return AjaxResult.success("重置成功,请重新设置!"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtPromotionRecordController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtPromotionRecordController.java new file mode 100644 index 0000000..c685fa1 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtPromotionRecordController.java @@ -0,0 +1,38 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.domain.entity.TtPromotionRecord; +import com.ruoyi.admin.service.TtPromotionRecordService; +import com.ruoyi.domain.vo.PromotionDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/admin/promotionRecord") +public class TtPromotionRecordController extends BaseController { + + private final TtPromotionRecordService promotionRecordService; + + public TtPromotionRecordController(TtPromotionRecordService promotionRecordService) { + this.promotionRecordService = promotionRecordService; + } + + @GetMapping("/getPromotionRecord") + public PageDataInfo getPromotionRecord(TtPromotionRecord ttPromotionRecord){ + startPage(); + List list = promotionRecordService.getPromotionRecord(ttPromotionRecord); + return getPageData(list); + } + + @GetMapping("/statisticsPromotionData/{userId}") + public R statisticsPromotionData(@PathVariable("userId") Integer userId){ + PromotionDataVO data = promotionRecordService.statisticsPromotionData(userId); + return R.ok(data); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeCardController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeCardController.java new file mode 100644 index 0000000..a68f35d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeCardController.java @@ -0,0 +1,83 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.domain.other.TtRechargeCard; +import com.ruoyi.admin.service.TtRechargeCardService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.redis.config.RedisLock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@RestController +@RequestMapping("/admin/rechargeCard") +public class TtRechargeCardController extends BaseController { + + private final TtRechargeCardService rechargeCardService; + + private final RedisLock redisLock; + + public TtRechargeCardController(TtRechargeCardService rechargeCardService, RedisLock redisLock) { + this.rechargeCardService = rechargeCardService; + this.redisLock = redisLock; + } + + @GetMapping("/list") + public PageDataInfo list(TtRechargeCard ttRechargeCard) { + startPage(); + List list = rechargeCardService.queryList(ttRechargeCard); + return getPageData(list); + } + + @PostMapping("/export") + public void export(HttpServletResponse response, TtRechargeCard ttRechargeCard) { + rechargeCardService.export(response, ttRechargeCard); + } + + @Log(title = "卡密", action = "生成卡密", businessType = BusinessType.INSERT) + @PostMapping("/generateCard/{rechargeListId}/{num}") + public R> generateCard(@PathVariable("rechargeListId") Integer rechargeListId, @PathVariable("num") Integer num) { + if (num == null || num <= 0) { + return R.fail("生成数量必须大于0"); + } + // 防重:按管理员 userId 加锁(锁粒度=单管理员,不影响其他管理员各自批量生成) + Long operId = getUserId(); + String lockKey = RedisConstants.RECHARGE_CARD_GENERATE_LOCK + operId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail("卡密生成中,请勿重复提交"); + } + try { + List cardList = rechargeCardService.generateCard(rechargeListId, num); + return R.ok(cardList); + } finally { + redisLock.unlock(lockKey); + } + } + + @Log(title = "卡密", action = "生成空白卡密", businessType = BusinessType.INSERT) + @PostMapping("/generateBlankCard/{rechargeListId}/{num}") + public R> generateBlankCard(@PathVariable("rechargeListId") Integer rechargeListId, @PathVariable("num") Integer num) { + if (num == null || num <= 0) { + return R.fail("生成数量必须大于0"); + } + // 防重:同一把管理员锁 + Long operId = getUserId(); + String lockKey = RedisConstants.RECHARGE_CARD_GENERATE_LOCK + operId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail("卡密生成中,请勿重复提交"); + } + try { + List cardList = rechargeCardService.generateBlankCard(rechargeListId, num); + return R.ok(cardList); + } finally { + redisLock.unlock(lockKey); + } + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeConfigController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeConfigController.java new file mode 100644 index 0000000..49757ce --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeConfigController.java @@ -0,0 +1,65 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtRechargeConfigMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.service.TtRechargeConfigService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.TtRechargeConfig; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/rechargeConfig") +public class TtRechargeConfigController extends BaseController { + + private final TtRechargeConfigService rechargeConfigService; + + public TtRechargeConfigController(TtRechargeConfigService rechargeConfigService) { + this.rechargeConfigService = rechargeConfigService; + } + + @GetMapping("/list") + public PageDataInfo list(@RequestParam(required = false) String status) { + startPage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotEmpty(status)) wrapper.eq(TtRechargeConfig::getStatus, status); + List list = rechargeConfigService.list(wrapper); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtRechargeConfig rechargeConfig = rechargeConfigService.getById(id); + return R.ok(rechargeConfig); + } + + @Log(title = "网站货币管理", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRechargeConfig.class, mapperClass = TtRechargeConfigMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtRechargeConfig ttRechargeConfig) { + return toAjax(rechargeConfigService.save(ttRechargeConfig)); + } + + @Log(title = "网站货币管理", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRechargeConfig.class, mapperClass = TtRechargeConfigMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtRechargeConfig ttRechargeConfig) { + return toAjax(rechargeConfigService.updateById(ttRechargeConfig)); + } + + @Log(title = "网站货币管理", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(rechargeConfigService.removeByIds(Arrays.asList(ids))); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeListController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeListController.java new file mode 100644 index 0000000..d889deb --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeListController.java @@ -0,0 +1,67 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.admin.service.TtRechargeProdService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/rechargeList") +public class TtRechargeListController extends BaseController { + + private final TtRechargeProdService rechargeListService; + + public TtRechargeListController(TtRechargeProdService rechargeListService) { + this.rechargeListService = rechargeListService; + } + + @GetMapping("/list") + public PageDataInfo list(@RequestParam(required = false) String status) { + startPage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotEmpty(status)) wrapper.eq(TtRechargeProd::getStatus, status); + List list = rechargeListService.list(wrapper); + return getPageData(list); + } + + @PreAuthorize("@ss.hasPermi('admin:rechargeList:query')") + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtRechargeProd rechargeList = rechargeListService.getById(id); + return R.ok(rechargeList); + } + + @Log(title = "货币列表", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRechargeProd.class, mapperClass = TtRechargeProdMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtRechargeProd ttRechargeProd) { + return toAjax(rechargeListService.save(ttRechargeProd)); + } + + @Log(title = "货币列表", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRechargeProd.class, mapperClass = TtRechargeProdMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtRechargeProd ttRechargeProd) { + return toAjax(rechargeListService.updateById(ttRechargeProd)); + } + + @Log(title = "货币列表", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(rechargeListService.removeByIds(Arrays.asList(ids))); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeRankingRewardController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeRankingRewardController.java new file mode 100644 index 0000000..569319c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeRankingRewardController.java @@ -0,0 +1,95 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.service.TtRechargeRankingRewardService; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.other.TtRechargeRankingReward; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 充值奖励金额Controller + * + * @author ruoyi + * @date 2024-07-04 + */ +@RestController +@RequestMapping("/admin/rechargeRankingReward") +public class TtRechargeRankingRewardController extends BaseController +{ + @Autowired + private TtRechargeRankingRewardService ttRechargeRankingRewardService; + + /** + * 查询充值奖励金额列表 + */ + @GetMapping("/list") + public TableDataInfo list(TtRechargeRankingReward ttRechargeRankingReward) + { + startPage(); + List list = ttRechargeRankingRewardService.selectTtRechargeRankingRewardList(ttRechargeRankingReward); + return getDataTable(list); + } + + /** + * 获取充值奖励金额详细信息 + */ + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Integer id) + { + return success(ttRechargeRankingRewardService.selectTtRechargeRankingRewardById(id)); + } + + /** + * 新增充值奖励金额 + */ + // @PreAuthorize("@ss.hasPermi('admin:reward:add')") + // @Log(title = "充值奖励金额", businessType = BusinessType.INSERT) + // @PostMapping + // public AjaxResult add(@RequestBody TtRechargeRankingReward ttRechargeRankingReward) + // { + // return toAjax(ttRechargeRankingRewardService.insertTtRechargeRankingReward(ttRechargeRankingReward)); + // } + + @Log(title = "消费排行榜奖励", action = "生成消费奖励", businessType = BusinessType.UPDATE) + @PostMapping("/generateRankingReward") + public AjaxResult generatePromotionLevel() { + String msg = ttRechargeRankingRewardService.generateRankingReward(); + return StringUtils.isEmpty(msg) ? AjaxResult.success("生成成功,请修改参数!") : AjaxResult.error(msg); + } + + /** + * 修改充值奖励金额 + */ + @Log(title = "消费排行榜奖励", action = "修改", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody TtRechargeRankingReward ttRechargeRankingReward) + { + return toAjax(ttRechargeRankingRewardService.updateTtRechargeRankingReward(ttRechargeRankingReward)); + } + + /** + * 删除充值奖励金额 + */ + // @PreAuthorize("@ss.hasPermi('admin:reward:remove')") + // @Log(title = "充值奖励金额", businessType = BusinessType.DELETE) + // @DeleteMapping("/{ids}") + // public AjaxResult remove(@PathVariable Integer[] ids) + // { + // return toAjax(ttRechargeRankingRewardService.deleteTtRechargeRankingRewardByIds(ids)); + // } + + @Log(title = "消费排行榜奖励", action = "重置", businessType = BusinessType.CLEAN) + @DeleteMapping("/truncateRankingReward") + public AjaxResult truncateRankingReward() { + ttRechargeRankingRewardService.truncateRankingReward(); + return AjaxResult.success("重置成功"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeRecordController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeRecordController.java new file mode 100644 index 0000000..b56667d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRechargeRecordController.java @@ -0,0 +1,470 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtRechargeConfigMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.mapper.TtDeliveryRecordMapper; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeConfig; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.admin.service.TtRechargeRecordService; +import com.ruoyi.domain.other.TtRechargeRecordBody; +import com.ruoyi.common.core.controller.BaseController; +import com.github.pagehelper.PageInfo; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.domain.other.TtRechargeRecordVo; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletResponse; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/admin/rechargeRecord") +public class TtRechargeRecordController extends BaseController { + + private final TtRechargeRecordService rechargeRecordService; + + public TtRechargeRecordController(TtRechargeRecordService rechargeRecordService) { + this.rechargeRecordService = rechargeRecordService; + } + + @Autowired + private com.ruoyi.admin.mapper.TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtOrderMapper ttOrderMapper; + + @Autowired + private TtRechargeConfigMapper ttRechargeConfigMapper; + + @Autowired + private TtDeliveryRecordMapper ttDeliveryRecordMapper; + + @Autowired + private com.ruoyi.admin.mapper.TtBoxRecordsMapper ttBoxRecordsMapper; + + @GetMapping("/list") + public PageDataInfo list(TtRechargeRecordBody rechargeRecordBody) { + startPage(); + List list = rechargeRecordService.queryList(rechargeRecordBody); + if (CollectionUtils.isNotEmpty(list)) { + Set userIds = new HashSet<>(); + Set orderIds = new HashSet<>(); + for (TtRechargeRecord record : list) { + userIds.add(record.getUserId()); + // 同时加载直属上级(主播/招商),用于推导招商ID = 上级的上级 + if (record.getParentId() != null) { + userIds.add(record.getParentId()); + } + orderIds.add(record.getOrderId()); + } + List ttUsers = ttUserMapper.selectBatchIds(userIds); + Map userMap = ttUsers.stream().collect(Collectors.toMap(TtUser::getUserId, Function.identity())); + List orders = new LambdaQueryChainWrapper<>(ttOrderMapper) + .in(TtOrder::getOrderId, orderIds).list(); + Map orderPicture = new HashMap<>(); + if (CollectionUtils.isNotEmpty(orders)) { + Set goodIds = orders.stream().map(TtOrder::getGoodsId).collect(Collectors.toSet()); + List configs = new LambdaQueryChainWrapper<>(ttRechargeConfigMapper).in(TtRechargeConfig::getId, goodIds).list(); + Map configMap = configs.stream().collect(Collectors.toMap(TtRechargeConfig::getId, Function.identity())); + for (TtOrder order : orders) { + TtRechargeConfig config = configMap.get(order.getGoodsId()); + if (config != null) { + orderPicture.put(order.getOrderId(), config.getPicture()); + } + } + } + + List vos = new ArrayList<>(); + for (TtRechargeRecord record : list) { + TtRechargeRecordVo vo = new TtRechargeRecordVo(); + BeanUtils.copyProperties(record, vo); + vos.add(vo); + // 招商ID = 充值用户的上级上级(直属上级[主播]的上级[招商]) + TtUser parentUser = userMap.get(record.getParentId()); + if (parentUser != null) { + vo.setMerchantId(parentUser.getParentId()); + } + TtUser ttUser = userMap.get(record.getUserId()); + if (ttUser == null) { + continue; + } + vo.setNickName(ttUser.getNickName()); + vo.setPhoneNumber(ttUser.getPhoneNumber()); + vo.setDeliveryAddress(ttUser.getDeliveryAddress()); + vo.setPicture(orderPicture.getOrDefault(vo.getOrderId(), "")); + } + PageDataInfo pageDataInfo = getPageData(vos); + pageDataInfo.setTotal(new PageInfo<>(list).getTotal()); + return pageDataInfo; + } else { + return getPageData(new ArrayList<>()); + } + } + + @PostMapping("/export") + public void export(HttpServletResponse response, TtRechargeRecordBody rechargeRecordBody) { + List list = rechargeRecordService.queryList(rechargeRecordBody); + ExcelUtil util = new ExcelUtil<>(TtRechargeRecord.class); + util.exportExcel(response, list, "充值记录信息列表"); + } + + /** + * 首页折线图数据:支持按小时/天/周维度统计充值、开箱流水、对战开箱流水等。 + * 优化:原先按时间桶循环执行 7(天)/24(小时)/4(周) 次数据库查询(N+1),现合并为 + * 单次 GROUP BY (source, 时间桶) 聚合查询,配合 (source, create_time) 复合索引命中范围扫描, + * 消除多次 DB 往返。 + * @param type 时间维度: hour(今日按小时), day(近7天按天), week(近4周按周) + */ + @GetMapping("/chartData") + public com.ruoyi.common.core.domain.R chartData(@RequestParam(defaultValue = "day") String type) { + List dateList = new ArrayList<>(); + List rechargeList = new ArrayList<>(); + List boxOpenList = new ArrayList<>(); + List fightBoxOpenList = new ArrayList<>(); + List luckyUpgradeList = new ArrayList<>(); + List westernDuelList = new ArrayList<>(); + List wheelList = new ArrayList<>(); + List roseladyList = new ArrayList<>(); + + List sources = Arrays.asList( + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordSource.GAME_TYPE_01.getCode(), + TtAccountRecordSource.GAME_TYPE_02.getCode(), + TtAccountRecordSource.GOLD_DEVICE.getCode(), + TtAccountRecordSource.BULLET_SPIN.getCode(), + TtAccountRecordSource.LUCKLY_WHEEL_SPIN.getCode(), + TtAccountRecordSource.WHEEL_BET_REFUND.getCode(), // 转盘兜底退款:折线按净口径冲减 + TtAccountRecordSource.OLYMPUS_SPIN.getCode()); + + LocalDate today = LocalDate.now(); + // bucketKey -> (source -> 金额):原值与绝对值两份 + Map> rawByBucket = new HashMap<>(); + Map> absByBucket = new HashMap<>(); + + if ("hour".equals(type)) { + // 按小时:今日 0-23 点,单次查询按小时聚合 + Timestamp start = Timestamp.valueOf(today.atStartOfDay()); + Timestamp end = Timestamp.valueOf(today.plusDays(1).atStartOfDay()); + indexRows(ttUserBlendErcashMapper.sumBySourcesGroupByHour(sources, start, end), rawByBucket, absByBucket); + for (int h = 0; h <= 23; h++) { + dateList.add(String.format("%02d:00", h)); + appendPoint(Collections.singletonList(String.valueOf(h)), rawByBucket, absByBucket, + rechargeList, boxOpenList, fightBoxOpenList, luckyUpgradeList, westernDuelList, wheelList, roseladyList); + } + } else if ("week".equals(type)) { + // 按周:近4周,单次查询按日聚合,再在内存中合并为每周 + LocalDate rangeStart = today.minusWeeks(3).with(java.time.DayOfWeek.MONDAY); + LocalDate rangeEnd = today.with(java.time.DayOfWeek.MONDAY).plusWeeks(1); + Timestamp start = Timestamp.valueOf(rangeStart.atStartOfDay()); + Timestamp end = Timestamp.valueOf(rangeEnd.atStartOfDay()); + indexRows(ttUserBlendErcashMapper.sumBySourcesGroupByDate(sources, start, end), rawByBucket, absByBucket); + for (int w = 3; w >= 0; w--) { + LocalDate weekStart = today.minusWeeks(w).with(java.time.DayOfWeek.MONDAY); + LocalDate weekEnd = weekStart.plusWeeks(1); + dateList.add(weekStart.getMonthValue() + "/" + weekStart.getDayOfMonth() + "-" + weekEnd.minusDays(1).getMonthValue() + "/" + weekEnd.minusDays(1).getDayOfMonth()); + List keys = new ArrayList<>(); + for (LocalDate d = weekStart; d.isBefore(weekEnd); d = d.plusDays(1)) { + keys.add(d.toString()); + } + appendPoint(keys, rawByBucket, absByBucket, + rechargeList, boxOpenList, fightBoxOpenList, luckyUpgradeList, westernDuelList, wheelList, roseladyList); + } + } else { + // 按天:近7天,单次查询按日聚合 + LocalDate rangeStart = today.minusDays(6); + Timestamp start = Timestamp.valueOf(rangeStart.atStartOfDay()); + Timestamp end = Timestamp.valueOf(today.plusDays(1).atStartOfDay()); + indexRows(ttUserBlendErcashMapper.sumBySourcesGroupByDate(sources, start, end), rawByBucket, absByBucket); + for (int i = 6; i >= 0; i--) { + LocalDate date = today.minusDays(i); + dateList.add(date.getMonthValue() + "/" + date.getDayOfMonth()); + appendPoint(Collections.singletonList(date.toString()), rawByBucket, absByBucket, + rechargeList, boxOpenList, fightBoxOpenList, luckyUpgradeList, westernDuelList, wheelList, roseladyList); + } + } + + Map result = new HashMap<>(); + result.put("dateList", dateList); + result.put("rechargeList", rechargeList); + result.put("boxOpenList", boxOpenList); + result.put("fightBoxOpenList", fightBoxOpenList); + result.put("luckyUpgradeList", luckyUpgradeList); + result.put("westernDuelList", westernDuelList); + result.put("wheelList", wheelList); + result.put("roseladyList", roseladyList); + return com.ruoyi.common.core.domain.R.ok(result); + } + + /** + * 把 GROUP BY (source, bucket) 的聚合结果整理为 bucketKey -> (source -> 金额) 的双层 Map。 + * bucket 对按天/周为 yyyy-MM-dd(DATE),对按小时为 0-23(HOUR)。 + */ + private void indexRows(List> rows, + Map> rawByBucket, + Map> absByBucket) { + if (rows == null) return; + for (Map row : rows) { + if (row == null || row.get("source") == null || row.get("bucket") == null) continue; + String bucket = row.get("bucket").toString(); + Integer src = Integer.parseInt(row.get("source").toString()); + BigDecimal amt = row.get("amt") != null ? new BigDecimal(row.get("amt").toString()) : BigDecimal.ZERO; + BigDecimal absAmt = row.get("absAmt") != null ? new BigDecimal(row.get("absAmt").toString()) : BigDecimal.ZERO; + rawByBucket.computeIfAbsent(bucket, k -> new HashMap<>()).put(src, amt); + absByBucket.computeIfAbsent(bucket, k -> new HashMap<>()).put(src, absAmt); + } + } + + /** + * 汇总给定时间桶(可跨多天,如按周则传入该周 7 天的 key)在各 source 上的金额,追加到对应折线图数据列。 + * 充值取原值(RECHARGE 为收入正值);各游戏消费取绝对值,与原 abs=true 语义一致。 + */ + private void appendPoint(List bucketKeys, + Map> rawByBucket, + Map> absByBucket, + List rechargeList, List boxOpenList, + List fightBoxOpenList, List luckyUpgradeList, + List westernDuelList, List wheelList, + List roseladyList) { + BigDecimal recharge = BigDecimal.ZERO; + BigDecimal box = BigDecimal.ZERO; + BigDecimal fightBox = BigDecimal.ZERO; + BigDecimal luckyUpgrade = BigDecimal.ZERO; + BigDecimal westernDuel = BigDecimal.ZERO; + BigDecimal wheel = BigDecimal.ZERO; + BigDecimal wheelRefund = BigDecimal.ZERO; + BigDecimal roselady = BigDecimal.ZERO; + for (String key : bucketKeys) { + Map raw = rawByBucket.getOrDefault(key, Collections.emptyMap()); + Map abs = absByBucket.getOrDefault(key, Collections.emptyMap()); + recharge = recharge.add(raw.getOrDefault(TtAccountRecordSource.RECHARGE.getCode(), BigDecimal.ZERO)); + box = box.add(abs.getOrDefault(TtAccountRecordSource.GAME_TYPE_01.getCode(), BigDecimal.ZERO)); + fightBox = fightBox.add(abs.getOrDefault(TtAccountRecordSource.GAME_TYPE_02.getCode(), BigDecimal.ZERO)); + luckyUpgrade = luckyUpgrade.add(abs.getOrDefault(TtAccountRecordSource.GOLD_DEVICE.getCode(), BigDecimal.ZERO)); + westernDuel = westernDuel.add(abs.getOrDefault(TtAccountRecordSource.BULLET_SPIN.getCode(), BigDecimal.ZERO)); + wheel = wheel.add(abs.getOrDefault(TtAccountRecordSource.LUCKLY_WHEEL_SPIN.getCode(), BigDecimal.ZERO)); + wheelRefund = wheelRefund.add(abs.getOrDefault(TtAccountRecordSource.WHEEL_BET_REFUND.getCode(), BigDecimal.ZERO)); + roselady = roselady.add(abs.getOrDefault(TtAccountRecordSource.OLYMPUS_SPIN.getCode(), BigDecimal.ZERO)); + } + rechargeList.add(recharge); + boxOpenList.add(box); + fightBoxOpenList.add(fightBox); + luckyUpgradeList.add(luckyUpgrade); + westernDuelList.add(westernDuel); + // 转盘取净口径:消费 − 兜底退款(退款局的注单没进奖池,毛口径会与奖池统计对不上) + wheelList.add(wheel.subtract(wheelRefund).max(BigDecimal.ZERO)); + roseladyList.add(roselady); + } + + /** + * 指定 source 的金额绝对值合计(可按时间窗口 + 有效用户过滤),用于把兜底退款从投注额里冲减。 + * start/end 为 null 表示不限时间。 + */ + private BigDecimal sumAbsBySource(Integer source, Timestamp start, Timestamp end, String validUserFilter) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(TtUserBlendErcash::getSource, source) + .inSql(TtUserBlendErcash::getUserId, validUserFilter); + if (start != null) wrapper.ge(TtUserBlendErcash::getCreateTime, start); + if (end != null) wrapper.lt(TtUserBlendErcash::getCreateTime, end); + return ttUserBlendErcashMapper.selectList(wrapper).stream() + .map(r -> r.getAmount() != null ? r.getAmount().abs() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + @GetMapping("/homeStats") + public com.ruoyi.common.core.domain.R homeStats() { + LocalDate today = LocalDate.now(); + Timestamp startOfDay = Timestamp.valueOf(today.atStartOfDay()); + Timestamp endOfDay = Timestamp.valueOf(today.plusDays(1).atStartOfDay()); + + String validUserFilter = "SELECT user_id FROM tt_user WHERE is_blank_account != 1 AND user_type != '03' AND status != '1' AND del_flag = '0'"; + + // 历史总充值(不限时间,统一排除:白号、机器人、封禁、逻辑删除、真删除用户;主表 + 归档表 UNION ALL) + BigDecimal totalRecharge = ttUserBlendErcashMapper.sumAmountBySourceAndTimeWithArchive( + TtAccountRecordSource.RECHARGE.getCode(), null, null); + + // 今日总充值:source = 1(统一排除:白号、机器人、封禁、逻辑删除、真删除用户;主表 + 归档表 UNION ALL) + BigDecimal todayRecharge = ttUserBlendErcashMapper.sumAmountBySourceAndTimeWithArchive( + TtAccountRecordSource.RECHARGE.getCode(), startOfDay, endOfDay); + + // 今日总投注:与今日总返奖口径对齐,覆盖全部7个游戏消费(含Roll房=6、荒野争锋=34) + // 统一排除:白号、机器人、封禁、逻辑删除、真删除用户 + List betSources = new ArrayList<>(TtAccountRecordSource.getGameBetCodes()); + LambdaQueryWrapper betWrapper = + Wrappers.lambdaQuery() + .in(TtUserBlendErcash::getSource, betSources) + .ge(TtUserBlendErcash::getCreateTime, startOfDay) + .lt(TtUserBlendErcash::getCreateTime, endOfDay) + .inSql(TtUserBlendErcash::getUserId, validUserFilter); + BigDecimal todayBet = ttUserBlendErcashMapper.selectList(betWrapper).stream() + .map(r -> r.getAmount() != null ? r.getAmount().abs() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + // 疯狂转盘净口径:扣掉兜底退款(source=64)。退款局从未开奖、也没进奖池,不扣会与奖池统计对不上 + todayBet = todayBet.subtract(sumAbsBySource( + TtAccountRecordSource.WHEEL_BET_REFUND.getCode(), startOfDay, endOfDay, validUserFilter)) + .max(BigDecimal.ZERO); + + // 今日总返奖:唯一化改造后所有游戏产出均先入背包(自动分解也先入背包再分解), + // 今日返奖仅按背包产出统计(tt_box_records,7个游戏,按 create_time,不看 status),不再与金币流水交叉统计。 + BigDecimal todayReward = ttBoxRecordsMapper.todayOrnamentRewardTotal(startOfDay, endOfDay); + + // 历史总返奖:与今日总返奖同口径(tt_box_records,7个游戏,不看 status、含已分解),仅不限时间范围; + // 不再叠加早期金币返奖流水与单独的Roll房统计,保证两个指标仅相差时间范围。 + BigDecimal historyReward = ttBoxRecordsMapper.historyOrnamentRewardTotal(); + + // 总发货金额:tt_delivery_record WHERE status=10(统一排除:白号、机器人、封禁、逻辑删除、真删除用户) + LambdaQueryWrapper deliveryWrapper = + Wrappers.lambdaQuery() + .eq(TtDeliveryRecord::getStatus, 10) + .inSql(TtDeliveryRecord::getUserId, validUserFilter); + BigDecimal totalDeliveryAmount = ttDeliveryRecordMapper.selectList(deliveryWrapper).stream() + .map(TtDeliveryRecord::getBuyPrice) + .filter(java.util.Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 总流水:所有用户(招商+主播+普通用户)的历史累计游戏消费总额(统一排除:白号、机器人、封禁、逻辑删除、真删除用户) + List gameBetSources = new ArrayList<>(TtAccountRecordSource.getGameBetCodes()); + LambdaQueryWrapper flowWrapper = + Wrappers.lambdaQuery() + .in(TtUserBlendErcash::getSource, gameBetSources) + .inSql(TtUserBlendErcash::getUserId, validUserFilter); + BigDecimal totalFlow = ttUserBlendErcashMapper.selectList(flowWrapper).stream() + .map(r -> r.getAmount() != null ? r.getAmount().abs() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + // 与今日总投注同口径:扣掉疯狂转盘兜底退款 + totalFlow = totalFlow.subtract(sumAbsBySource( + TtAccountRecordSource.WHEEL_BET_REFUND.getCode(), null, null, validUserFilter)) + .max(BigDecimal.ZERO); + + Map result = new HashMap<>(); + result.put("totalRecharge", totalRecharge); + result.put("todayRecharge", todayRecharge); + result.put("todayBet", todayBet); + result.put("todayReward", todayReward); + result.put("historyReward", historyReward); + result.put("totalDeliveryAmount", totalDeliveryAmount); + result.put("totalFlow", totalFlow); + return com.ruoyi.common.core.domain.R.ok(result); + } + + /** + * 实时RTP(Return To Player)统计:当日各游戏的投注/奖励/RTP百分比。 + * 唯一化改造后所有产出先入背包,返奖统一按 tt_box_records 今日产出(按 source 分组、不看 status)统计, + * 投注仍取 tt_user_blend_ercash 各游戏消费 source;覆盖全部 8 个产出游戏(含玫瑰女郎)。 + */ + @GetMapping("/realTimeRtp") + public AjaxResult realTimeRtp() { + LocalDate today = LocalDate.now(); + Timestamp startOfDay = Timestamp.valueOf(today.atStartOfDay()); + Timestamp endOfDay = Timestamp.valueOf(today.plusDays(1).atStartOfDay()); + + // 投注 source:7 个游戏消费(含 Roll房=6、荒野争锋=34)+ 玫瑰女郎=60 + 转盘兜底退款=64(净口径冲减用) + List betSources = Arrays.asList(2, 4, 6, 34, 38, 40, 50, 60, + TtAccountRecordSource.WHEEL_BET_REFUND.getCode()); + + // 一次查询获取所有投注 source 的聚合数据,避免 N+1 + List> sumResult = ttUserBlendErcashMapper.sumBySourcesAndTime(betSources, startOfDay, endOfDay); + + // 构建 source -> absAmt 的映射(各游戏今日投注额) + Map sourceAmtMap = new HashMap<>(); + for (Map row : sumResult) { + Integer source = Integer.parseInt(row.get("source").toString()); + BigDecimal absAmt = row.get("absAmt") != null ? new BigDecimal(row.get("absAmt").toString()) : BigDecimal.ZERO; + sourceAmtMap.put(source, absAmt); + } + + // 返奖:唯一化改造后所有产出先入背包,今日返奖仅按背包产出统计(按 tt_box_records.source 分组、不看 status) + List> ornamentResult = ttBoxRecordsMapper.todayOrnamentRewardBySource(startOfDay, endOfDay); + Map ornamentBySource = new HashMap<>(); + if (ornamentResult != null) { + for (Map row : ornamentResult) { + Integer src = Integer.parseInt(row.get("source").toString()); + BigDecimal totalPrice = row.get("totalPrice") != null ? new BigDecimal(row.get("totalPrice").toString()) : BigDecimal.ZERO; + ornamentBySource.put(src, totalPrice); + } + } + + // 游戏配置:[投注 source(ercash), 饰品 source(box_records)] + int[][] gameConfig = { + {50, 11}, // 极速糖果 + {38, 99}, // 疯狂转盘 + {40, 4}, // 幸运升级 + {2, 1}, // 经典开箱 + {4, 2}, // 对战 + {6, 3}, // Roll 房(索引 5) + {34, 12}, // 荒野争锋 + {60, 13} // 玫瑰女郎(索引 7,投注=60,背包=13) + }; + String[] gameNames = {"极速糖果", "疯狂转盘", "幸运升级", "经典开箱", "对战", "Roll 房", "荒野争锋", "玫瑰女郎"}; + + List> data = new ArrayList<>(); + BigDecimal rollRoomBet = BigDecimal.ZERO; + BigDecimal rollRoomReward = BigDecimal.ZERO; + + for (int i = 0; i < gameConfig.length; i++) { + BigDecimal bet = sourceAmtMap.getOrDefault(gameConfig[i][0], BigDecimal.ZERO); + // 疯狂转盘取净口径:消费额 − 兜底退款(source=64)。退款注单所在回合从未开奖、也没进奖池, + // 不冲减会让首页投注/RTP 与奖池统计(真实池累计)对不上 + if (gameConfig[i][0] == TtAccountRecordSource.LUCKLY_WHEEL_SPIN.getCode()) { + bet = bet.subtract(sourceAmtMap.getOrDefault( + TtAccountRecordSource.WHEEL_BET_REFUND.getCode(), BigDecimal.ZERO)); + if (bet.compareTo(BigDecimal.ZERO) < 0) bet = BigDecimal.ZERO; + } + // 返奖 = 今日背包产出(该游戏 box source 的 ornaments_price 之和) + BigDecimal reward = ornamentBySource.getOrDefault(gameConfig[i][1], BigDecimal.ZERO); + + // 累加 Roll 房汇总 + if (i == 5) { + rollRoomBet = bet; + rollRoomReward = reward; + } + + BigDecimal rtp = BigDecimal.ZERO; + if (bet.compareTo(BigDecimal.ZERO) > 0) { + rtp = reward.multiply(new BigDecimal("100")).divide(bet, 2, java.math.RoundingMode.HALF_UP); + } + + Map item = new LinkedHashMap<>(); + item.put("name", gameNames[i]); + item.put("bet", bet.setScale(2, java.math.RoundingMode.HALF_UP)); + item.put("reward", reward.setScale(2, java.math.RoundingMode.HALF_UP)); + item.put("rtp", rtp); + data.add(item); + } + + // 额外返回 Roll 房汇总(仅今日返奖,因为 Roll 房无下注,只统计返奖) + Map rollRoomSummary = new LinkedHashMap<>(); + rollRoomSummary.put("reward", rollRoomReward.setScale(2, java.math.RoundingMode.HALF_UP)); + + Map result = new HashMap<>(); + result.put("games", data); + result.put("rollRoom", rollRoomSummary); + + return AjaxResult.success(result); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRedPackController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRedPackController.java new file mode 100644 index 0000000..980448d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRedPackController.java @@ -0,0 +1,109 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtRedPackMapper; +import com.ruoyi.admin.service.TtRedPackService; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.domain.other.TtRedPack; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 【请填写功能名称】Controller + * + * @author ruoyi + * @date 2023-06-29 + */ +@RestController +@RequestMapping("/admin/redPack") +public class TtRedPackController extends BaseController +{ + @Autowired + private TtRedPackService ttRedPackService; + + /** + * 查询【请填写功能名称】列表 + */ + @PreAuthorize("@ss.hasPermi('skinsback:ttRedPack:list')") + @GetMapping("/list") + public TableDataInfo list(TtRedPack ttRedPack) + { + startPage(); + List list = ttRedPackService.selectTtRedPackList(ttRedPack); + return getDataTable(list); + } + @PreAuthorize("@ss.hasPermi('skinsback:ttRedPack:listAll')") + @GetMapping("/listAll") + public TableDataInfo listAll(TtRedPack ttRedPack) + { + List list = ttRedPackService.selectTtRedPackList(ttRedPack); + return getDataTable(list); + } + /** + * 导出【请填写功能名称】列表 + */ + @PreAuthorize("@ss.hasPermi('skinsback:ttRedPack:export')") + @Log(title = "【请填写功能名称】", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, TtRedPack ttRedPack) + { + List list = ttRedPackService.selectTtRedPackList(ttRedPack); + ExcelUtil util = new ExcelUtil(TtRedPack.class); + util.exportExcel(response, list, "【请填写功能名称】数据"); + } + + /** + * 获取【请填写功能名称】详细信息 + */ + @PreAuthorize("@ss.hasPermi('skinsback:ttRedPack:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) + { + return success(ttRedPackService.selectTtRedPackById(id)); + } + + /** + * 新增【请填写功能名称】 + */ + @PreAuthorize("@ss.hasPermi('skinsback:ttRedPack:add')") + @Log(title = "CDK管理", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRedPack.class, mapperClass = TtRedPackMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtRedPack ttRedPack) + { + ttRedPack.setValidCount(ttRedPack.getPackCount()); + ttRedPack.setStatus(0l); + return toAjax(ttRedPackService.insertTtRedPack(ttRedPack)); + } + + /** + * 修改【请填写功能名称】 + */ + @PreAuthorize("@ss.hasPermi('skinsback:ttRedPack:edit')") + @Log(title = "CDK管理", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRedPack.class, mapperClass = TtRedPackMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtRedPack ttRedPack) + { + return toAjax(ttRedPackService.updateTtRedPack(ttRedPack)); + } + + /** + * 删除【请填写功能名称】 + */ + @PreAuthorize("@ss.hasPermi('skinsback:ttRedPack:remove')") + @Log(title = "CDK管理", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(ttRedPackService.deleteTtRedPackByIds(ids)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRedPacketController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRedPacketController.java new file mode 100644 index 0000000..811b18c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRedPacketController.java @@ -0,0 +1,74 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtRedPacketMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.service.TtRedPacketService; +import com.ruoyi.domain.other.TtRedPacket; +import com.ruoyi.domain.other.TtRedPacketBody; +import com.ruoyi.domain.vo.TtRedPacketDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@Api(tags = "管理端 红包") +@RestController +@RequestMapping("/admin/redPacket") +public class TtRedPacketController extends BaseController { + + private final TtRedPacketService redPacketService; + + public TtRedPacketController(TtRedPacketService redPacketService) { + this.redPacketService = redPacketService; + } + + @GetMapping("/list") + public PageDataInfo list(TtRedPacketBody ttRedPacketBody) { + startPage(); + List list = redPacketService.queryList(ttRedPacketBody); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtRedPacketDataVO data = redPacketService.queryList(TtRedPacketBody.builder().id(id).build()).get(0); + return R.ok(data); + } + + @Log(title = "CDK口令", action = "新增", businessType = BusinessType.INSERT) + @ApiOperation("生成红包") + @PostMapping + public AjaxResult add(@RequestBody TtRedPacketDataVO ttRedPacketDataVO) { + List passwordList = redPacketService.insertRedPacket(ttRedPacketDataVO); + return StringUtils.isNotNull(passwordList) ? AjaxResult.success("生成成功!", passwordList) : AjaxResult.error("生成红包异常"); + } + + @Log(title = "CDK口令", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRedPacket.class, mapperClass = TtRedPacketMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtRedPacket redPacket) { + redPacket.setUpdateBy(getUsername()); + redPacket.setUpdateTime(DateUtils.getNowDate()); + return redPacketService.updateRedPacketById(redPacket); + // return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "CDK口令", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + + boolean b = redPacketService.removeByIds(Arrays.asList(ids)); + + if (b) return AjaxResult.success(); + return AjaxResult.error(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRedPacketRecordController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRedPacketRecordController.java new file mode 100644 index 0000000..ef16ead --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRedPacketRecordController.java @@ -0,0 +1,30 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.service.TtRedPacketRecordService; +import com.ruoyi.domain.other.TtRedPacketRecordBody; +import com.ruoyi.domain.vo.TtRedPacketRecordDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.page.PageDataInfo; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/admin/redPacketRecord") +public class TtRedPacketRecordController extends BaseController { + + private final TtRedPacketRecordService redPacketRecordService; + + public TtRedPacketRecordController(TtRedPacketRecordService redPacketRecordService) { + this.redPacketRecordService = redPacketRecordService; + } + + @GetMapping("/getRedPacketRecordList") + public PageDataInfo getRedPacketRecordList(TtRedPacketRecordBody ttRedPacketRecordBody) { + startPage(); + List list = redPacketRecordService.queryList(ttRedPacketRecordBody); + return getPageData(list); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtReplacementRecordController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtReplacementRecordController.java new file mode 100644 index 0000000..d637563 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtReplacementRecordController.java @@ -0,0 +1,101 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtReplacementRecordMapper; +import com.ruoyi.admin.service.TtReplacementRecordService; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.domain.other.TtReplacementRecord; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 汰换记录Controller + * + * @author junhai + * @date 2023-09-10 + */ +@RestController +@RequestMapping("/admin/replacementRecord") +public class TtReplacementRecordController extends BaseController +{ + @Autowired + private TtReplacementRecordService ttReplacementRecordService; + + /** + * 查询汰换记录列表 + */ + @PreAuthorize("@ss.hasPermi('skinsback:replacementRecord:list')") + @GetMapping("/list") + public TableDataInfo list(TtReplacementRecord ttReplacementRecord) + { + startPage(); + List list = ttReplacementRecordService.selectTtReplacementRecordList(ttReplacementRecord); + return getDataTable(list); + } + + /** + * 导出汰换记录列表 + */ + @PreAuthorize("@ss.hasPermi('skinsback:replacementRecord:export')") + @Log(title = "汰换记录", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, TtReplacementRecord ttReplacementRecord) + { + List list = ttReplacementRecordService.selectTtReplacementRecordList(ttReplacementRecord); + ExcelUtil util = new ExcelUtil(TtReplacementRecord.class); + util.exportExcel(response, list, "汰换记录数据"); + } + + /** + * 获取汰换记录详细信息 + */ + @PreAuthorize("@ss.hasPermi('skinsback:replacementRecord:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) + { + return success(ttReplacementRecordService.selectTtReplacementRecordById(id)); + } + + /** + * 新增汰换记录 + */ + @PreAuthorize("@ss.hasPermi('skinsback:replacementRecord:add')") + @Log(title = "汰换记录", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtReplacementRecord.class, mapperClass = TtReplacementRecordMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtReplacementRecord ttReplacementRecord) + { + return toAjax(ttReplacementRecordService.insertTtReplacementRecord(ttReplacementRecord)); + } + + /** + * 修改汰换记录 + */ + @PreAuthorize("@ss.hasPermi('skinsback:replacementRecord:edit')") + @Log(title = "汰换记录", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtReplacementRecord.class, mapperClass = TtReplacementRecordMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtReplacementRecord ttReplacementRecord) + { + return toAjax(ttReplacementRecordService.updateTtReplacementRecord(ttReplacementRecord)); + } + + /** + * 删除汰换记录 + */ + @PreAuthorize("@ss.hasPermi('skinsback:replacementRecord:remove')") + @Log(title = "汰换记录", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(ttReplacementRecordService.deleteTtReplacementRecordByIds(ids)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightGroupBoxController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightGroupBoxController.java new file mode 100644 index 0000000..a13b2c0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightGroupBoxController.java @@ -0,0 +1,200 @@ +package com.ruoyi.admin.controller; + +import java.math.BigDecimal; +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.admin.mapper.TtRobotFightGroupBoxMapper; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtRobotFightGroup; +import com.ruoyi.domain.other.TtRobotFightGroupBox; +import com.ruoyi.domain.other.TtRobotFightPriceConfig; +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtRobotFightGroupMapper; +import com.ruoyi.admin.mapper.TtRobotFightPriceConfigMapper; +import com.ruoyi.admin.service.TtRobotFightGroupBoxService; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 机器人对战分组宝箱Controller + * + * @author ruoyi + * @date 2024-06-28 + */ +@RestController +@RequestMapping("/admin/group/box") +public class TtRobotFightGroupBoxController extends BaseController +{ + @Autowired + private TtRobotFightGroupBoxService ttRobotFightGroupBoxService; + + @Autowired + private TtBoxMapper ttBoxMapper; + + @Autowired + private TtRobotFightGroupMapper ttRobotFightGroupMapper; + + @Autowired + private TtRobotFightPriceConfigMapper ttRobotFightPriceConfigMapper; + + /** + * 查询机器人对战分组宝箱列表 + */ + @PreAuthorize("@ss.hasPermi('admin:box:list')") + @GetMapping("/list/{groupId}") + public TableDataInfo list( + @PathVariable("groupId") Integer groupId, + TtRobotFightGroupBox ttRobotFightGroupBox + ) + { + startPage(); + List list = ttRobotFightGroupBoxService + .selectTtRobotFightGroupBoxList(groupId, ttRobotFightGroupBox); + return getDataTable(list); + } + + /** + * 获取机器人对战分组宝箱详细信息 + */ + @PreAuthorize("@ss.hasPermi('admin:box:query')") + @GetMapping(value = "/{groupId}/{boxId}") + public AjaxResult getInfo( + @PathVariable("groupId") Integer groupId, + @PathVariable("boxId") Integer boxId + ) + { + return success(ttRobotFightGroupBoxService.selectTtRobotFightGroupBoxByBoxId(groupId, boxId)); + } + + /** + * 新增机器人对战分组宝箱 + */ + @PreAuthorize("@ss.hasPermi('admin:box:add')") + @Log(title = "机器人对战分组宝箱", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRobotFightGroupBox.class, mapperClass = TtRobotFightGroupBoxMapper.class) + @PostMapping + public AjaxResult add(@Validated @RequestBody TtRobotFightGroupBox ttRobotFightGroupBox) + { + // 检查该分组下是否已存在该宝箱 + TtRobotFightGroupBox existing = ttRobotFightGroupBoxService + .selectTtRobotFightGroupBoxByBoxId(ttRobotFightGroupBox.getGroupId(), ttRobotFightGroupBox.getBoxId()); + if (existing != null) { + return AjaxResult.error("该宝箱已存在于当前分组中,请勿重复添加"); + } + // 校验新增后分组总价值是否超过档位上限 + AjaxResult check = validateTotalPrice(ttRobotFightGroupBox.getGroupId(), + ttRobotFightGroupBox.getBoxId(), ttRobotFightGroupBox.getBoxNum(), null, null); + if (check != null) return check; + return toAjax(ttRobotFightGroupBoxService.insertTtRobotFightGroupBox(ttRobotFightGroupBox)); + } + + /** + * 修改机器人对战分组宝箱 + */ + @PreAuthorize("@ss.hasPermi('admin:box:edit')") + @Log(title = "机器人对战分组宝箱", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRobotFightGroupBox.class, mapperClass = TtRobotFightGroupBoxMapper.class) + @PutMapping + public AjaxResult edit(@Validated @RequestBody TtRobotFightGroupBox ttRobotFightGroupBox) + { + // 校验修改后分组总价值是否超过档位上限(排除被修改的那条旧记录) + AjaxResult check = validateTotalPrice(ttRobotFightGroupBox.getGroupId(), + ttRobotFightGroupBox.getBoxId(), ttRobotFightGroupBox.getBoxNum(), + ttRobotFightGroupBox.getOldBoxId(), ttRobotFightGroupBox.getGroupId()); + if (check != null) return check; + return toAjax(ttRobotFightGroupBoxService.updateTtRobotFightGroupBox(ttRobotFightGroupBox)); + } + + /** + * 校验新增/修改后分组宝箱总价值是否超过档位上限,以及所有宝箱的总数量是否超过 15 + * + * @param groupId 分组ID + * @param newBoxId 本次操作的宝箱ID + * @param newBoxNum 本次操作的宝箱数量 + * @param excludeBoxId 修改时需排除的旧宝箱ID(新增传null) + * @param excludeGroupId 修改时需排除的旧分组ID(新增传null) + */ + private AjaxResult validateTotalPrice(Integer groupId, Integer newBoxId, Integer newBoxNum, + Integer excludeBoxId, Integer excludeGroupId) { + if (groupId == null) return null; + + // 查出该分组已有的所有宝箱记录 + List existingList = ttRobotFightGroupBoxService + .selectTtRobotFightGroupBoxList(groupId, new TtRobotFightGroupBox()); + + // 计算现有宝箱的总数量(排除被修改的旧记录) + int totalNum = 0; + BigDecimal total = BigDecimal.ZERO; + for (TtRobotFightGroupBox item : existingList) { + if (excludeBoxId != null && item.getBoxId().equals(excludeBoxId)) { + continue; // 修改时跳过旧记录 + } + if (item.getBoxNum() != null) { + totalNum += item.getBoxNum(); + } + TtBox itemBox = ttBoxMapper.selectById(item.getBoxId()); + if (itemBox != null && itemBox.getPrice() != null && item.getBoxNum() != null) { + total = total.add(itemBox.getPrice().multiply(BigDecimal.valueOf(item.getBoxNum()))); + } + } + + // 加上本次新增/修改的数量和价值 + if (newBoxNum != null) { + totalNum += newBoxNum; + } + if (newBoxId != null && newBoxNum != null) { + TtBox newBox = ttBoxMapper.selectById(newBoxId); + if (newBox == null) return AjaxResult.error("宝箱不存在"); + if (newBox.getPrice() != null) { + total = total.add(newBox.getPrice().multiply(BigDecimal.valueOf(newBoxNum))); + } + } + + // 校验总数量不超过 15 + if (totalNum > 15) { + return AjaxResult.error("操作后分组宝箱总数量为 " + totalNum + ",超过上限 15 个,请减少宝箱数量"); + } + + // 校验分组宝箱总价值不超过档位上限 + TtRobotFightGroup group = ttRobotFightGroupMapper.selectTtRobotFightGroupByGroupId(groupId); + if (group != null && group.getPriceConfigId() != null) { + TtRobotFightPriceConfig priceConfig = ttRobotFightPriceConfigMapper + .selectTtRobotFightPriceConfigById(group.getPriceConfigId()); + if (priceConfig != null && priceConfig.getPriceLimit() != null) { + BigDecimal priceLimit = priceConfig.getPriceLimit(); + if (total.compareTo(priceLimit) > 0) { + return AjaxResult.error("操作后分组宝箱总价值 ¥" + total.stripTrailingZeros().toPlainString() + + " 超过档位上限 ¥" + priceLimit.stripTrailingZeros().toPlainString() + + ",请调整宝箱数量或选择价格更低的宝箱"); + } + } + } + return null; + } + + /** + * 删除机器人对战分组宝箱 + */ + @PreAuthorize("@ss.hasPermi('admin:box:remove')") + @Log(title = "机器人对战分组宝箱", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{groupId}/{boxIds}") + public AjaxResult remove(@PathVariable Integer groupId, @PathVariable Integer[] boxIds) + { + return toAjax(ttRobotFightGroupBoxService.deleteTtRobotFightGroupBoxByGroupIds(groupId, boxIds)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightGroupController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightGroupController.java new file mode 100644 index 0000000..945cfa0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightGroupController.java @@ -0,0 +1,129 @@ +package com.ruoyi.admin.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; + +import com.ruoyi.admin.mapper.TtRobotFightGroupMapper; +import com.ruoyi.admin.mapper.TtRobotFightPriceConfigMapper; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.domain.other.TtRobotFightGroup; +import com.ruoyi.domain.other.TtRobotFightPriceConfig; +import com.ruoyi.admin.service.TtRobotFightGroupService; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 机器人对战分组Controller + * + * @author ruoyi + * @date 2024-06-28 + */ +@RestController +@RequestMapping("/admin/group") +public class TtRobotFightGroupController extends BaseController +{ + @Autowired + private TtRobotFightGroupService ttRobotFightGroupService; + + @Autowired + private TtRobotFightPriceConfigMapper ttRobotFightPriceConfigMapper; + + /** + * 查询机器人对战分组列表 + */ + @GetMapping("/list") + public TableDataInfo list(TtRobotFightGroup ttRobotFightGroup) + { + startPage(); + List list = ttRobotFightGroupService.selectTtRobotFightGroupList(ttRobotFightGroup); + return getDataTable(list); + } + + /** + * 导出机器人对战分组列表 + */ + @Log(title = "机器人对战分组", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, TtRobotFightGroup ttRobotFightGroup) + { + List list = ttRobotFightGroupService.selectTtRobotFightGroupList(ttRobotFightGroup); + ExcelUtil util = new ExcelUtil(TtRobotFightGroup.class); + util.exportExcel(response, list, "机器人对战分组数据"); + } + + /** + * 获取机器人对战分组详细信息 + */ + @GetMapping(value = "/{groupId}") + public AjaxResult getInfo(@PathVariable("groupId") Integer groupId) + { + return success(ttRobotFightGroupService.selectTtRobotFightGroupByGroupId(groupId)); + } + + /** + * 新增机器人对战分组 + */ + @Log(title = "机器人对战分组", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRobotFightGroup.class, mapperClass = TtRobotFightGroupMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtRobotFightGroup ttRobotFightGroup) + { + // 支持 2 人(1v1)、3 人(1v1v1)、4 人(2v2 组队)三种对战模式 + if (ttRobotFightGroup.getSeatNum() != 2 && ttRobotFightGroup.getSeatNum() != 3 && ttRobotFightGroup.getSeatNum() != 4) { + return AjaxResult.error("座位数量只能为 2、3 或 4"); + } + if (ttRobotFightGroup.getPriceConfigId() == null) { + return AjaxResult.error("请选择价格档位"); + } + TtRobotFightPriceConfig priceConfig = ttRobotFightPriceConfigMapper.selectTtRobotFightPriceConfigById(ttRobotFightGroup.getPriceConfigId()); + if (priceConfig == null || !"0".equals(priceConfig.getStatus())) { + return AjaxResult.error("价格档位不存在或已停用"); + } + return toAjax(ttRobotFightGroupService.insertTtRobotFightGroup(ttRobotFightGroup)); + } + + /** + * 修改机器人对战分组 + */ + @Log(title = "机器人对战分组", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRobotFightGroup.class, mapperClass = TtRobotFightGroupMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtRobotFightGroup ttRobotFightGroup) + { + // 支持 2 人(1v1)、3 人(1v1v1)、4 人(2v2 组队)三种对战模式 + if (ttRobotFightGroup.getSeatNum() != 2 && ttRobotFightGroup.getSeatNum() != 3 && ttRobotFightGroup.getSeatNum() != 4) { + return AjaxResult.error("座位数量只能为 2、3 或 4"); + } + if (ttRobotFightGroup.getPriceConfigId() == null) { + return AjaxResult.error("请选择价格档位"); + } + TtRobotFightPriceConfig priceConfig = ttRobotFightPriceConfigMapper.selectTtRobotFightPriceConfigById(ttRobotFightGroup.getPriceConfigId()); + if (priceConfig == null || !"0".equals(priceConfig.getStatus())) { + return AjaxResult.error("价格档位不存在或已停用"); + } + return toAjax(ttRobotFightGroupService.updateTtRobotFightGroup(ttRobotFightGroup)); + } + + /** + * 删除机器人对战分组 + */ + @Log(title = "机器人对战分组", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{groupIds}") + public AjaxResult remove(@PathVariable Integer[] groupIds) + { + return toAjax(ttRobotFightGroupService.deleteTtRobotFightGroupByGroupIds(groupIds)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightJoinConfigController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightJoinConfigController.java new file mode 100644 index 0000000..d52a008 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightJoinConfigController.java @@ -0,0 +1,147 @@ +package com.ruoyi.admin.controller; + +import java.math.BigDecimal; +import java.util.List; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.admin.mapper.TtRobotFightJoinConfigMapper; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.domain.other.TtRobotFightJoinConfig; +import com.ruoyi.admin.service.TtRobotFightJoinConfigService; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 机器人对战进房时间配置Controller + * + * @author ruoyi + * @date 2026-07-23 + */ +@RestController +@RequestMapping("/admin/robotFightConfig") +public class TtRobotFightJoinConfigController extends BaseController +{ + @Autowired + private TtRobotFightJoinConfigService ttRobotFightJoinConfigService; + + /** + * 查询机器人对战进房时间配置列表 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightConfig:list')") + @GetMapping("/list") + public TableDataInfo list(TtRobotFightJoinConfig ttRobotFightJoinConfig) + { + startPage(); + List list = ttRobotFightJoinConfigService.selectTtRobotFightJoinConfigList(ttRobotFightJoinConfig); + return getDataTable(list); + } + + /** + * 获取机器人对战进房时间配置详细信息 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightConfig:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Integer id) + { + return success(ttRobotFightJoinConfigService.selectTtRobotFightJoinConfigById(id)); + } + + /** + * 新增机器人对战进房时间配置 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightConfig:add')") + @Log(title = "机器人对战配置", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRobotFightJoinConfig.class, mapperClass = TtRobotFightJoinConfigMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtRobotFightJoinConfig ttRobotFightJoinConfig) + { + AjaxResult check = validate(ttRobotFightJoinConfig); + if (check != null) { + return check; + } + return toAjax(ttRobotFightJoinConfigService.insertTtRobotFightJoinConfig(ttRobotFightJoinConfig)); + } + + /** + * 修改机器人对战进房时间配置 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightConfig:edit')") + @Log(title = "机器人对战配置", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRobotFightJoinConfig.class, mapperClass = TtRobotFightJoinConfigMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtRobotFightJoinConfig ttRobotFightJoinConfig) + { + AjaxResult check = validate(ttRobotFightJoinConfig); + if (check != null) { + return check; + } + return toAjax(ttRobotFightJoinConfigService.updateTtRobotFightJoinConfig(ttRobotFightJoinConfig)); + } + + /** + * 删除机器人对战进房时间配置 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightConfig:remove')") + @Log(title = "机器人对战配置", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) + { + return toAjax(ttRobotFightJoinConfigService.deleteTtRobotFightJoinConfigByIds(ids)); + } + + /** + * 保存前校验:金额区间合法、进房秒数为正,且与其他启用档位区间不重叠 + */ + private AjaxResult validate(TtRobotFightJoinConfig config) + { + if (config.getMinAmount() == null || config.getMaxAmount() == null) { + return AjaxResult.error("金额区间不能为空"); + } + if (config.getMinAmount().compareTo(BigDecimal.ZERO) < 0) { + return AjaxResult.error("最小金额不能小于0"); + } + if (config.getMinAmount().compareTo(config.getMaxAmount()) >= 0) { + return AjaxResult.error("最小金额必须小于最大金额"); + } + if (config.getJoinSeconds() == null || config.getJoinSeconds() <= 0) { + return AjaxResult.error("进房秒数必须大于0"); + } + if (config.getRandomSeconds() == null || config.getRandomSeconds() < 0) { + return AjaxResult.error("随机秒数不能为空且必须大于等于0"); + } + // 停用的档位不参与匹配,无需重叠校验 + if ("1".equals(config.getStatus())) { + return null; + } + // 调度任务按 min_amount 升序取第一条命中档位,区间重叠会导致新档位永远不生效,这里直接拒绝保存 + TtRobotFightJoinConfig query = new TtRobotFightJoinConfig(); + query.setStatus("0"); + List enabledList = ttRobotFightJoinConfigService.selectTtRobotFightJoinConfigList(query); + for (TtRobotFightJoinConfig exist : enabledList) { + if (config.getId() != null && config.getId().equals(exist.getId())) { + continue; + } + if (exist.getMinAmount() == null || exist.getMaxAmount() == null) { + continue; + } + // 区间为 (min, max],重叠条件:newMin < exMax 且 exMin < newMax + if (config.getMinAmount().compareTo(exist.getMaxAmount()) < 0 + && exist.getMinAmount().compareTo(config.getMaxAmount()) < 0) { + return AjaxResult.error("金额区间与已启用档位[" + exist.getMinAmount().stripTrailingZeros().toPlainString() + + "-" + exist.getMaxAmount().stripTrailingZeros().toPlainString() + + "]重叠,请先修改或停用该档位"); + } + } + return null; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightPriceConfigController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightPriceConfigController.java new file mode 100644 index 0000000..e9e5135 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRobotFightPriceConfigController.java @@ -0,0 +1,143 @@ +package com.ruoyi.admin.controller; + +import java.math.BigDecimal; +import java.util.List; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.admin.mapper.TtRobotFightPriceConfigMapper; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.domain.other.TtRobotFightPriceConfig; +import com.ruoyi.admin.service.TtRobotFightPriceConfigService; +import com.ruoyi.admin.mapper.TtRobotFightGroupMapper; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 机器人对战价格档位配置Controller + * + * @author ruoyi + * @date 2026-08-13 + */ +@RestController +@RequestMapping("/admin/robotFightPriceConfig") +public class TtRobotFightPriceConfigController extends BaseController +{ + @Autowired + private TtRobotFightPriceConfigService ttRobotFightPriceConfigService; + + @Autowired + private TtRobotFightGroupMapper ttRobotFightGroupMapper; + + /** + * 查询机器人对战价格档位配置列表 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightPriceConfig:list')") + @GetMapping("/list") + public TableDataInfo list(TtRobotFightPriceConfig ttRobotFightPriceConfig) + { + startPage(); + List list = ttRobotFightPriceConfigService.selectTtRobotFightPriceConfigList(ttRobotFightPriceConfig); + return getDataTable(list); + } + + /** + * 获取机器人对战价格档位配置详细信息 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightPriceConfig:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Integer id) + { + return success(ttRobotFightPriceConfigService.selectTtRobotFightPriceConfigById(id)); + } + + /** + * 新增机器人对战价格档位配置 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightPriceConfig:add')") + @Log(title = "机器人对战配置", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRobotFightPriceConfig.class, mapperClass = TtRobotFightPriceConfigMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtRobotFightPriceConfig ttRobotFightPriceConfig) + { + AjaxResult check = validate(ttRobotFightPriceConfig); + if (check != null) { + return check; + } + return toAjax(ttRobotFightPriceConfigService.insertTtRobotFightPriceConfig(ttRobotFightPriceConfig)); + } + + /** + * 修改机器人对战价格档位配置 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightPriceConfig:edit')") + @Log(title = "机器人对战配置", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRobotFightPriceConfig.class, mapperClass = TtRobotFightPriceConfigMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtRobotFightPriceConfig ttRobotFightPriceConfig) + { + AjaxResult check = validate(ttRobotFightPriceConfig); + if (check != null) { + return check; + } + return toAjax(ttRobotFightPriceConfigService.updateTtRobotFightPriceConfig(ttRobotFightPriceConfig)); + } + + /** + * 删除机器人对战价格档位配置 + */ + @PreAuthorize("@ss.hasPermi('admin:robotFightPriceConfig:remove')") + @Log(title = "机器人对战配置", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) + { + for (Integer id : ids) { + int count = ttRobotFightGroupMapper.selectCountByPriceConfigId(id); + if (count > 0) { + return AjaxResult.error("删除失败,该价格档位已被" + count + "个机器人对战分组引用"); + } + } + return toAjax(ttRobotFightPriceConfigService.deleteTtRobotFightPriceConfigByIds(ids)); + } + + /** + * 保存前校验:价格上限、目标数量、排序必须为正,启用档位 price_limit 不能重复 + */ + private AjaxResult validate(TtRobotFightPriceConfig config) + { + if (config.getPriceLimit() == null || config.getPriceLimit().compareTo(BigDecimal.ZERO) <= 0) { + return AjaxResult.error("价格上限必须大于0"); + } + if (config.getTargetCount() == null || config.getTargetCount() <= 0) { + return AjaxResult.error("目标数量必须大于0"); + } + if (config.getSortOrder() == null || config.getSortOrder() <= 0) { + return AjaxResult.error("排序序号必须大于0"); + } + // 停用的档位不参与匹配,无需重复校验 + if ("1".equals(config.getStatus())) { + return null; + } + TtRobotFightPriceConfig query = new TtRobotFightPriceConfig(); + query.setStatus("0"); + List enabledList = ttRobotFightPriceConfigService.selectTtRobotFightPriceConfigList(query); + for (TtRobotFightPriceConfig exist : enabledList) { + if (config.getId() != null && config.getId().equals(exist.getId())) { + continue; + } + if (config.getPriceLimit().compareTo(exist.getPriceLimit()) == 0) { + return AjaxResult.error("价格上限与已启用档位[" + exist.getPriceLimit().stripTrailingZeros().toPlainString() + "]重复"); + } + } + return null; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRollController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRollController.java new file mode 100644 index 0000000..96f4b5e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRollController.java @@ -0,0 +1,198 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtRollMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.domain.body.AddRobotBody; +import com.ruoyi.admin.domain.body.AddUserBody; +import com.ruoyi.admin.service.TtRollService; +import com.ruoyi.admin.service.TtRollUserService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.domain.dto.roll.GetRollPrizePool; +import com.ruoyi.domain.dto.roll.InviteRollUser; +import com.ruoyi.domain.entity.roll.TtRoll; +import com.ruoyi.domain.entity.roll.TtRollBody; +import com.ruoyi.domain.entity.roll.TtRollUser; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.TtRollPrizeDataVO; +import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsByPageVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.Data; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +@Api(tags = "管理端 roll房") +@RestController +@RequestMapping("/admin/roll") +public class TtRollController extends BaseController { + + private final TtRollService rollService; + private final TtRollUserService rollUserService; + + public TtRollController(TtRollService rollService, + TtRollUserService rollUserService) { + this.rollService = rollService; + this.rollUserService = rollUserService; + } + + @GetMapping("/list") + public PageDataInfo list(TtRollBody ttRollBody) { + startPage(); + List list = rollService.queryList(ttRollBody); + return getPageData(list); + } + + /** + * 导出信息 + * @param response + * @param ttRollBody + */ + @PostMapping("/export") + public void export(HttpServletResponse response, TtRollBody ttRollBody) { + List list = rollService.queryList(ttRollBody); + ExcelUtil util = new ExcelUtil<>(TtRoll.class); + util.exportExcel(response, list, "Roll房信息列表"); + } + + @GetMapping(value = "/{rollId}") + public R getInfo(@PathVariable("rollId") Long rollId) { + TtRoll roll = rollService.getById(rollId); + return R.ok(roll); + } + + @Log(title = "Roll房列表", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRoll.class, mapperClass = TtRollMapper.class) + // 创建roll房 + @ApiOperation("创建roll房") + @PostMapping + public AjaxResult add(@RequestBody TtRoll ttRoll) { + + ttRoll.setCreateBy(getUsername()); + ttRoll.setCreateTime(DateUtils.getNowDate()); + return rollService.createRoll(ttRoll); + + } + + // roll房拉人 + @ApiOperation("roll房拉人") + @PostMapping("/inviteRollUser") + public R inviteRollUser(@RequestBody InviteRollUser InviteRollUser) { + + return rollService.inviteRollUser(InviteRollUser); + + } + + // 获取roll房成员信息 + @ApiOperation("获取roll房成员信息") + @GetMapping("getRollUsers/{rollId}") + public AjaxResult getRollUsers(@PathVariable("rollId") Integer rollId) { + return rollService.getRollUsers(rollId); + } + + + @Log(title = "Roll房列表", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRoll.class, mapperClass = TtRollMapper.class) + @ApiOperation("编辑roll房") + @PutMapping + public AjaxResult edit(@RequestBody TtRoll ttRoll) { + + ttRoll.setUpdateBy(getUsername()); + ttRoll.setUpdateTime(DateUtils.getNowDate()); + + return rollService.updateRollById(ttRoll); + } + + @Log(title = "Roll房列表", action = "删除", businessType = BusinessType.DELETE) + @ApiOperation("删除roll房") + @DeleteMapping("/remove/{rollId}") + public AjaxResult remove(@PathVariable Long rollId) { + return rollService.removeRollById(rollId, getUsername()); + } + + + // @ApiOperation("roll房奖品列表") + // @PostMapping("/getRollPrizeList") + // public R> getRollPrizeList(@RequestBody RollPrizesCondition condition) { + // + // List list = rollService.getRollPrizeList(condition); + // + // return R.ok(list); + // + // } + @ApiOperation("roll房奖品列表") + @GetMapping("/getRollPrizeList/{rollId}") + public R> getRollPrizeList(@PathVariable("rollId") Integer rollId) { + + List list = rollService.getRollPrizeList(rollId); + + return R.ok(list); + + } + + @ApiOperation("获取 roll房奖池详情") + @PostMapping("/getRollPrizePool") + public R getRollPrizePool(@RequestBody @Validated GetRollPrizePool param) { + return rollService.getRollPrizePool(param); + } + + @ApiOperation("roll房的指定获奖人名单") + @GetMapping("/rollWinners/{rollId}") + public R rollWinners(@PathVariable Integer rollId) { + return rollUserService.rollWinners(rollId); + } + + @ApiOperation("roll房实际中奖记录(开奖后参与用户中了什么饰品)") + @GetMapping("/winningRecords/{rollId}") + public R>> winningRecords(@PathVariable("rollId") Integer rollId) { + return rollService.winningRecords(rollId); + } + + @Log(title = "装备列表", action = "指定获奖者", businessType = BusinessType.INSERT) + @ApiOperation("指定roll房winner") + @PostMapping("/namedWinner") + public AjaxResult namedWinner(@RequestBody @Validated TtRollPrizeDataVO param) { + return rollService.namedWinner(param); + } + + @Log(title = "装备列表", action = "取消奖品", businessType = BusinessType.DELETE) + @ApiOperation("取消指定roll房奖品") + @DeleteMapping("/cancelNamedWinner") + public R cancelNamedWinner(@RequestBody List rollUserPrizeIds) { + return rollService.cancelNamedWinner(rollUserPrizeIds); + } + + + @ApiOperation("获取ROLL房用户") + @GetMapping("/getRollUser/{rollId}") + public AjaxResult getRollUser(@PathVariable("rollId") Integer rollId) { + List rollUserList = rollService.getRollUser(rollId); + return success(rollUserList); + } + + @Log(title = "房间用户", action = "加入机器人", businessType = BusinessType.INSERT) + @ApiOperation("加入机器人") + @PostMapping("/addRobot") + public AjaxResult addRobot(@RequestBody AddRobotBody addRobotBody) { + return rollService.addRobot(addRobotBody); + } + @Log(title = "房间用户", action = "加入用户", businessType = BusinessType.INSERT) + @ApiOperation("强制加入普通用户") + @PostMapping("/addUser") + public AjaxResult addUser(@RequestBody AddUserBody addUserBody) { + return rollService.addUser(addUserBody); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRollJackpotController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRollJackpotController.java new file mode 100644 index 0000000..c09c8c2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRollJackpotController.java @@ -0,0 +1,78 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtRollJackpotMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.entity.roll.TtRollJackpot; +import com.ruoyi.admin.service.TtRollJackpotService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; + +@Api(tags = "管理端 roll奖池") +@RestController +@RequestMapping("/admin/rollJackpot") +public class TtRollJackpotController extends BaseController { + + private final TtRollJackpotService rollJackpotService; + + public TtRollJackpotController(TtRollJackpotService rollJackpotService) { + this.rollJackpotService = rollJackpotService; + } + + @ApiOperation("奖池列表") + @GetMapping("/list") + public PageDataInfo list(@RequestParam(value = "jackpotName", required = false) String jackpotName) { + startPage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotEmpty(jackpotName)) { + wrapper.like(TtRollJackpot::getJackpotName, jackpotName); + } + List list = rollJackpotService.list(wrapper); + return getPageData(list); + } + + @GetMapping(value = "/{jackpotId}") + public R getInfo(@PathVariable("jackpotId") Long jackpotId) { + TtRollJackpot rollJackpot = rollJackpotService.getById(jackpotId); + return R.ok(rollJackpot); + } + + @Log(title = "奖池列表", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRollJackpot.class, mapperClass = TtRollJackpotMapper.class) + @ApiOperation("添加奖池") + @PostMapping + public AjaxResult add(@RequestBody TtRollJackpot ttRollJackpot) { + ttRollJackpot.setCreateBy(getUsername()); + ttRollJackpot.setCreateTime(DateUtils.getNowDate()); + ttRollJackpot.setTotalPrice(BigDecimal.ZERO); + return toAjax(rollJackpotService.save(ttRollJackpot)); + } + + @Log(title = "奖池列表", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRollJackpot.class, mapperClass = TtRollJackpotMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtRollJackpot ttRollJackpot) { + ttRollJackpot.setUpdateBy(getUsername()); + ttRollJackpot.setUpdateTime(DateUtils.getNowDate()); + return toAjax(rollJackpotService.updateById(ttRollJackpot)); + } + + @Log(title = "奖池列表", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/remove/{jackpotId}") + public AjaxResult remove(@PathVariable Long jackpotId) { + String msg = rollJackpotService.removeRollJackpotById(jackpotId); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRollJackpotOrnamentsController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRollJackpotOrnamentsController.java new file mode 100644 index 0000000..0f3fef6 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtRollJackpotOrnamentsController.java @@ -0,0 +1,91 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtRollJackpotOrnamentsMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.dto.rollJackpotOrnament.RollJOEdit; +import com.ruoyi.domain.entity.roll.TtRoll; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnaments; +import com.ruoyi.admin.service.TtRollJackpotOrnamentsService; +import com.ruoyi.admin.service.TtRollService; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnamentsBody; +import com.ruoyi.domain.vo.TtRollJackpotOrnamentsDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@Api(tags = "管理端 roll奖池物品管理") +@RestController +@RequestMapping("/admin/rollJackpotOrnaments") +public class TtRollJackpotOrnamentsController extends BaseController { + + private final TtRollService rollService; + private final TtRollJackpotOrnamentsService rollJackpotOrnamentsService; + + public TtRollJackpotOrnamentsController(TtRollService rollService, + TtRollJackpotOrnamentsService rollJackpotOrnamentsService) { + this.rollService = rollService; + this.rollJackpotOrnamentsService = rollJackpotOrnamentsService; + } + + @ApiOperation("物品列表") + @GetMapping("/list") + public PageDataInfo list(TtRollJackpotOrnamentsBody rollJackpotOrnamentsBody) { + startPage(); + List list = rollJackpotOrnamentsService.queryList(rollJackpotOrnamentsBody); + return getPageData(list); + } + + @GetMapping(value = "/{rollJackpotOrnamentsId}") + public R getInfo(@PathVariable("rollJackpotOrnamentsId") Long rollJackpotOrnamentsId) { + TtRollJackpotOrnaments rollJackpotOrnaments = rollJackpotOrnamentsService.getById(rollJackpotOrnamentsId); + return R.ok(rollJackpotOrnaments); + } + + @Log(title = "奖池列表", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtRollJackpotOrnaments.class, mapperClass = TtRollJackpotOrnamentsMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtRollJackpotOrnaments ttRollJackpotOrnaments) { + String msg = rollJackpotOrnamentsService.insertRollJackpotOrnaments(ttRollJackpotOrnaments); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "奖池列表", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtRollJackpotOrnaments.class, mapperClass = TtRollJackpotOrnamentsMapper.class) + @ApiOperation("修改奖池道具") + @PutMapping + public AjaxResult edit(@RequestBody TtRollJackpotOrnaments rollJOEdit) { + String msg = rollJackpotOrnamentsService.updateRollJackpotOrnamentsById(rollJOEdit); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "奖池列表", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{jackpotId}/{rollJackpotOrnamentsIds}") + public AjaxResult remove(@PathVariable("jackpotId") Integer jackpotId, + @PathVariable("rollJackpotOrnamentsIds") Long[] rollJackpotOrnamentsIds) { + List rollList = new LambdaQueryChainWrapper<>(rollService.getBaseMapper()).eq(TtRoll::getJackpotId, jackpotId) + .eq(TtRoll::getRollStatus, "0").eq(TtRoll::getDelFlag, "0").list(); + if (StringUtils.isNotNull(rollList) && !rollList.isEmpty()) + return AjaxResult.error("该奖池正在被进行中的Roll房使用,禁止删除奖品!"); + boolean ok = rollJackpotOrnamentsService.removeByIds(Arrays.asList(rollJackpotOrnamentsIds)); + // 删除饰品后重算奖池总价 + rollJackpotOrnamentsService.recalcJackpotTotalPrice(jackpotId); + return toAjax(ok); + } + + @ApiOperation("奖池批量填货") + @PostMapping("/batchAdd/{rollJackpotId}/{OrnamentsIds}") + public AjaxResult batchAdd(@PathVariable Integer rollJackpotId, @PathVariable Long[] OrnamentsIds) { + String msg = rollJackpotOrnamentsService.batchAdd(rollJackpotId, Arrays.asList(OrnamentsIds)); + return StringUtils.isEmpty(msg) ? AjaxResult.success("批量填货成功,请手动修改饰品数量!") : AjaxResult.error(msg); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtTaskCenterController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtTaskCenterController.java new file mode 100644 index 0000000..0875263 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtTaskCenterController.java @@ -0,0 +1,97 @@ +package com.ruoyi.admin.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.admin.mapper.TtTaskCenterMapper; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.domain.other.TtTaskCenter; +import com.ruoyi.admin.service.TtTaskCenterService; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 任务中心Controller + * + * @author ruoyi + * @date 2024-05-25 + */ +@RestController +@RequestMapping("/admin/taskCenter") +public class TtTaskCenterController extends BaseController +{ + @Autowired + private TtTaskCenterService ttTaskCenterService; + + /** + * 查询任务列表 + */ + @PreAuthorize("isAuthenticated()") + @GetMapping("/list") + public TableDataInfo list(TtTaskCenter ttTaskCenter) + { + startPage(); + List list = ttTaskCenterService.selectTtTaskCenterList(ttTaskCenter); + return getDataTable(list); + } + + /** + * 获取任务详细信息 + */ + @PreAuthorize("isAuthenticated()") + @GetMapping(value = "/{taskId}") + public AjaxResult getInfo(@PathVariable("taskId") Integer taskId) + { + return success(ttTaskCenterService.selectTtTaskCenterByTaskId(taskId)); + } + + /** + * 新增任务 + */ + @PreAuthorize("isAuthenticated()") + @Log(title = "任务列表", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtTaskCenter.class, mapperClass = TtTaskCenterMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtTaskCenter ttTaskCenter) + { + ttTaskCenter.setCreateBy(getUsername()); + return toAjax(ttTaskCenterService.insertTtTaskCenter(ttTaskCenter)); + } + + /** + * 修改任务 + */ + @PreAuthorize("isAuthenticated()") + @Log(title = "任务列表", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtTaskCenter.class, mapperClass = TtTaskCenterMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtTaskCenter ttTaskCenter) + { + ttTaskCenter.setUpdateBy(getUsername()); + return toAjax(ttTaskCenterService.updateTtTaskCenter(ttTaskCenter)); + } + + /** + * 删除任务 + */ + @PreAuthorize("isAuthenticated()") + @Log(title = "任务列表", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{taskIds}") + public AjaxResult remove(@PathVariable Integer[] taskIds) + { + return error("任务创建后无法删除"); + // return toAjax(ttTaskCenterService.deleteTtTaskCenterByTaskIds(taskIds)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtTimeRollController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtTimeRollController.java new file mode 100644 index 0000000..afa9773 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtTimeRollController.java @@ -0,0 +1,89 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtTimeRollMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.entity.roll.TtTimeRoll; +import com.ruoyi.admin.service.TtTimeRollService; +import com.ruoyi.domain.vo.TtRollPrizeDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import org.quartz.SchedulerException; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/admin/timeRoll") +public class TtTimeRollController extends BaseController { + + private final TtTimeRollService timeRollService; + + public TtTimeRollController(TtTimeRollService timeRollService) { + this.timeRollService = timeRollService; + } + + @GetMapping("/list") + public PageDataInfo list(){ + startPage(); + List list = timeRollService.list(); + return getPageData(list); + } + + @GetMapping(value = "/{timeRollId}") + public R getInfo(@PathVariable("timeRollId") Integer timeRollId) { + TtTimeRoll timeRoll = timeRollService.getById(timeRollId); + return R.ok(timeRoll); + } + + @Log(title = "时间roll房", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtTimeRoll.class, mapperClass = TtTimeRollMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtTimeRoll ttTimeRoll) throws SchedulerException, TaskException { + ttTimeRoll.setCreateBy(getUsername()); + ttTimeRoll.setCreateTime(DateUtils.getNowDate()); + String msg = timeRollService.insertTimeRoll(ttTimeRoll); + return StringUtils.isEmpty(msg) ? AjaxResult.success("新增成功!") : AjaxResult.error(msg); + } + + @Log(title = "时间roll房", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtTimeRoll.class, mapperClass = TtTimeRollMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtTimeRoll ttTimeRoll) { + ttTimeRoll.setUpdateBy(getUsername()); + ttTimeRoll.setUpdateTime(DateUtils.getNowDate()); + return toAjax(timeRollService.updateById(ttTimeRoll)); + } + + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody TtTimeRoll ttTimeRoll) throws SchedulerException { + String msg = timeRollService.changeStatus(ttTimeRoll); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "时间roll房", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/remove/{id}") + public AjaxResult remove(@PathVariable Integer id) throws SchedulerException { + return timeRollService.removeTimeRollById(id); + } + + @GetMapping("/getTimeRollPrizeList/{id}") + public R> getTimeRollPrizeList(@PathVariable("id") Integer id) { + List list = timeRollService.getTimeRollPrizeList(id); + List sortedList = list.stream().sorted((a, b) -> b.getUsePrice().compareTo(a.getUsePrice())).collect(Collectors.toList()); + return R.ok(sortedList); + } + + @PostMapping("/namedWinner") + public AjaxResult namedWinner(@RequestBody TtRollPrizeDataVO rollPrizeData) { + String msg = timeRollService.namedWinner(rollPrizeData); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtTransferApplyController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtTransferApplyController.java new file mode 100644 index 0000000..a67c7db --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtTransferApplyController.java @@ -0,0 +1,116 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.service.TtTransferApplyService; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.other.TransferApplyBody; +import com.ruoyi.domain.vo.transfer.TransferApplyBatchVO; +import com.ruoyi.domain.vo.transfer.TransferApplyVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Api(tags = "管理端 转赠申请管理") +@RestController +@RequestMapping("/admin/transferApply") +public class TtTransferApplyController extends BaseController { + + private final TtTransferApplyService transferApplyService; + private final RedisLock redisLock; + + public TtTransferApplyController(TtTransferApplyService transferApplyService, + RedisLock redisLock) { + this.transferApplyService = transferApplyService; + this.redisLock = redisLock; + } + + @ApiOperation("转赠申请列表(按饰品逐条)") + @GetMapping("/list") + public PageDataInfo list(TransferApplyBody body) { + startPage(); + List list = transferApplyService.getTransferApplyList(body); + return getPageData(list); + } + + @ApiOperation("转赠申请批次列表(按批次聚合,含饰品明细)") + @GetMapping("/batchList") + public PageDataInfo batchList(TransferApplyBody body) { + startPage(); + List list = transferApplyService.getTransferApplyBatchList(body); + return getPageData(list); + } + + @ApiOperation("审核通过(单条,兼容旧接口)") + @Log(title = "转赠申请", action = "同意", businessType = BusinessType.GRANT) + @PostMapping("/approve") + public R approve(@RequestParam("id") Integer id) { + String lockKey = "admin:transfer:apply:" + id; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail(false, "正在处理该申请,请勿重复操作"); + } + try { + String msg = transferApplyService.approve(id); + return StringUtils.isEmpty(msg) ? R.ok(true, "审核通过成功!") : R.fail(false, msg); + } finally { + redisLock.unlock(lockKey); + } + } + + @ApiOperation("审核驳回(单条,兼容旧接口)") + @Log(title = "转赠申请", action = "驳回", businessType = BusinessType.GRANT) + @PostMapping("/reject") + public R reject(@RequestParam("id") Integer id, + @RequestParam("rejectReason") String rejectReason) { + String lockKey = "admin:transfer:apply:" + id; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail(false, "正在处理该申请,请勿重复操作"); + } + try { + String msg = transferApplyService.reject(id, rejectReason); + return StringUtils.isEmpty(msg) ? R.ok(true, "已驳回该申请!") : R.fail(false, msg); + } finally { + redisLock.unlock(lockKey); + } + } + + @ApiOperation("批次审核通过(整个批次一起通过)") + @Log(title = "转赠申请", action = "确定同意", businessType = BusinessType.GRANT) + @PostMapping("/approveBatch") + public R approveBatch(@RequestParam("batchId") String batchId) { + String lockKey = "admin:transfer:batch:" + batchId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail(false, "正在处理该批次,请勿重复操作"); + } + try { + String msg = transferApplyService.approveBatch(batchId); + return StringUtils.isEmpty(msg) ? R.ok(true, "批次审核通过成功!") : R.fail(false, msg); + } finally { + redisLock.unlock(lockKey); + } + } + + @ApiOperation("批次审核驳回(整个批次一起驳回)") + @Log(title = "转赠申请", action = "驳回", businessType = BusinessType.GRANT) + @PostMapping("/rejectBatch") + public R rejectBatch(@RequestParam("batchId") String batchId, + @RequestParam("rejectReason") String rejectReason) { + String lockKey = "admin:transfer:batch:" + batchId; + if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) { + return R.fail(false, "正在处理该批次,请勿重复操作"); + } + try { + String msg = transferApplyService.rejectBatch(batchId, rejectReason); + return StringUtils.isEmpty(msg) ? R.ok(true, "批次已驳回!") : R.fail(false, msg); + } finally { + redisLock.unlock(lockKey); + } + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUpgradeFailOrnamentsController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUpgradeFailOrnamentsController.java new file mode 100644 index 0000000..85c0fd6 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUpgradeFailOrnamentsController.java @@ -0,0 +1,88 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.service.TtUpgradeFailOrnamentsService; +import com.ruoyi.domain.other.TtUpgradeFailOrnaments; +import com.ruoyi.domain.vo.TtUpgradeFailOrnamentsDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Api(tags = "管理端 失败物品奖励") +@RestController +@RequestMapping("/admin/upgradeFailOrnaments") +public class TtUpgradeFailOrnamentsController extends BaseController { + + private final TtUpgradeFailOrnamentsService upgradeFailOrnamentsService; + + public TtUpgradeFailOrnamentsController(TtUpgradeFailOrnamentsService upgradeFailOrnamentsService) { + this.upgradeFailOrnamentsService = upgradeFailOrnamentsService; + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class listParam { + @NotNull(message = "参与升级物品id不能为空") + private Integer upgradeOrnamentId; + } + @ApiOperation("失败奖励列表") + @PostMapping("/list") + public PageDataInfo list(@RequestBody listParam param) { + startPage(); + List list = upgradeFailOrnamentsService.queryList(param); + return getPageData(list); + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class BatchAddParam{ + @NotNull(message = "升级物品id不能为空") + private Integer upgradeId; +// @NotEmpty(message = "失败物品列表不能为空") + private Map ornamentsInfo; + + @NotEmpty(message = "失败物品列表不能为空") + private List ornamentsIds; + + //是否应用所有 + private Boolean useAllFlag; + } + @Log(title = "失败奖励", action = "新增", businessType = BusinessType.INSERT) + @ApiOperation("批量添加失败奖励") + @PostMapping("/batchAdd") + public AjaxResult batchAdd(@RequestBody @Validated BatchAddParam param) { + return upgradeFailOrnamentsService.batchAdd(param); + } + + @Log(title = "失败奖励", action = "修改", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody TtUpgradeFailOrnamentsDataVO ttUpgradeFailOrnamentsDataVO) { + String msg = upgradeFailOrnamentsService.updateUpgradeFailOrnamentsById(ttUpgradeFailOrnamentsDataVO); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "失败奖励", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(upgradeFailOrnamentsService.removeByIds(Arrays.asList(ids))); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUpgradeOrnamentsController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUpgradeOrnamentsController.java new file mode 100644 index 0000000..0a20c70 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUpgradeOrnamentsController.java @@ -0,0 +1,86 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtUpgradeFailOrnaments; +import com.ruoyi.domain.other.TtUpgradeOrnaments; +import com.ruoyi.admin.service.TtUpgradeFailOrnamentsService; +import com.ruoyi.admin.service.TtUpgradeOrnamentsService; +import com.ruoyi.domain.other.TtUpgradeOrnamentsBody; +import com.ruoyi.domain.vo.TtUpgradeOrnamentsDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Api(tags = "管理端 幸运升级") +@RestController +@RequestMapping("/admin/upgradeOrnaments") +public class TtUpgradeOrnamentsController extends BaseController { + + private final TtUpgradeFailOrnamentsService upgradeFailOrnamentsService; + private final TtUpgradeOrnamentsService upgradeOrnamentsService; + + public TtUpgradeOrnamentsController(TtUpgradeFailOrnamentsService upgradeFailOrnamentsService, + TtUpgradeOrnamentsService upgradeOrnamentsService) { + this.upgradeFailOrnamentsService = upgradeFailOrnamentsService; + this.upgradeOrnamentsService = upgradeOrnamentsService; + } + + @ApiOperation("获取参与幸运升级的物品") + @GetMapping("/list") + public PageDataInfo list(TtUpgradeOrnamentsBody ttUpgradeOrnamentsBody){ + startPage(); + List list = upgradeOrnamentsService.queryList(ttUpgradeOrnamentsBody); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtUpgradeOrnaments ttUpgradeOrnaments = upgradeOrnamentsService.getById(id); + return R.ok(ttUpgradeOrnaments); + } + + @Log(title = "饰品列表", action = "新增", businessType = BusinessType.INSERT) + @ApiOperation("批量填货") + @PostMapping("/batchAdd/{ornamentsIds}") + public AjaxResult batchAdd(@PathVariable Long[] ornamentsIds) { + String msg = upgradeOrnamentsService.batchAdd(Arrays.asList(ornamentsIds)); + return StringUtils.isEmpty(msg) ? AjaxResult.success("批量上架成功,请手动修改幸运区间!") : AjaxResult.error(msg); + } + + @Log(title = "饰品列表", action = "修改", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody TtUpgradeOrnamentsDataVO ttUpgradeOrnamentsDataVO) { + String msg = upgradeOrnamentsService.updateUpgradeOrnamentsById(ttUpgradeOrnamentsDataVO); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "饰品列表", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + List upgradeFailOrnamentsList = new LambdaQueryChainWrapper<>(upgradeFailOrnamentsService.getBaseMapper()) + .in(TtUpgradeFailOrnaments::getUpgradeId, Arrays.asList(ids)).list(); + List list = upgradeFailOrnamentsList.stream().map(TtUpgradeFailOrnaments::getId).collect(Collectors.toList()); + upgradeFailOrnamentsService.removeByIds(list); + return toAjax(upgradeOrnamentsService.removeByIds(Arrays.asList(ids))); + } + + @GetMapping("/getUpgradeProfitStatistics/{id}") + public R getUpgradeProfitStatistics(@PathVariable("id") Integer id){ + Map map = upgradeOrnamentsService.getUpgradeProfitStatistics(id); + return R.ok(map); + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUpgradeRecordController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUpgradeRecordController.java new file mode 100644 index 0000000..0c39cc2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUpgradeRecordController.java @@ -0,0 +1,36 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.service.TtUpgradeRecordService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.upgrade.UpgradeCondition; +import com.ruoyi.domain.other.TtUpgradeRecord; +import com.ruoyi.domain.other.TtUpgradeRecordBody; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.vo.upgrade.UpgradeRecordVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Api(tags = "管理端 幸运升级日志") +@RestController +@RequestMapping("/admin/upgradeRecord") +public class TtUpgradeRecordController extends BaseController { + + private final TtUpgradeRecordService upgradeRecordService; + + public TtUpgradeRecordController(TtUpgradeRecordService upgradeRecordService) { + this.upgradeRecordService = upgradeRecordService; + } + + @ApiOperation("幸运升级日志") + @PostMapping("/list") + public R> list(@RequestBody @Validated UpgradeCondition param){ + return upgradeRecordService.adminGetLog(param); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserAmountRecordsController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserAmountRecordsController.java new file mode 100644 index 0000000..bbc30c1 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserAmountRecordsController.java @@ -0,0 +1,125 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.recorde.TtUserAmountRecords; +import com.ruoyi.admin.service.TtUserAmountRecordsService; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.system.config.ArchiveDaysConfig; +import com.ruoyi.domain.other.TtUserAmountRecordsBody; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.page.PageDataInfo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; + +@Slf4j +@Api(tags = "管理端 金币账户") +@RestController +@RequestMapping("/admin/userAmountRecords") +public class TtUserAmountRecordsController extends BaseController { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final TtUserAmountRecordsService userAmountRecordsService; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + public TtUserAmountRecordsController(TtUserAmountRecordsService userAmountRecordsService) { + this.userAmountRecordsService = userAmountRecordsService; + } + + @ApiOperation("获取列表") + @PostMapping("/list") + public R list(@RequestBody @Validated TtUserAmountRecordsBody ttUserAmountRecordsBody) { + return userAmountRecordsService.queryList(ttUserAmountRecordsBody); + } + + /** 聚合类排序字段集合(这些字段需要全局排序,不能用当页排序) */ + private static final java.util.Set AGG_SORT_FIELDS = java.util.Set.of( + "totalRecharge", "totalTurnover", "classicBox", "fightGame", + "luckyUpgrade", "wheelGame", "olympusGame", "rollRoom"); + + /** + * 用户流水统计列表 + * 路由策略: + * - userId 排序 → 快速模式(先分页用户再聚合当页数据,Java内存排序) + * - 聚合字段排序 → 全局排序模式(先聚合所有用户→SQL排序→分页),确保全局排序正确 + */ + @ApiOperation("用户流水统计") + @GetMapping("/userTurnover") + public R userTurnover( + @RequestParam(required = false) Integer userId, + @RequestParam(required = false) String nickName, + @RequestParam(required = false) String phone, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime, + @RequestParam(required = false, defaultValue = "totalTurnover") String sortField, + @RequestParam(required = false, defaultValue = "desc") String sortOrder, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize) { + int offset = (pageNum - 1) * pageSize; + int total = ttUserBlendErcashMapper.userTurnoverCount(userId, nickName, phone); + + // 统一走流水明细/归档表:日报表只有 source+total,无法区分 source=56 的金币退款与积分退款。 + // 统计口径要求按 amount(金币字段)计算 roll房净消费 + boolean includeArchive = shouldIncludeArchive(endTime); + List> rows; + if (AGG_SORT_FIELDS.contains(sortField)) { + rows = ttUserBlendErcashMapper.userTurnoverListSorted( + userId, nickName, phone, startTime, endTime, includeArchive, sortField, sortOrder, offset, pageSize); + } else { + rows = ttUserBlendErcashMapper.userTurnoverList( + userId, nickName, phone, startTime, endTime, includeArchive, offset, pageSize); + sortRowsInMemory(rows, sortField, sortOrder); + } + return R.ok(Map.of("rows", rows, "total", total)); + } + + /** + * 判断 UNION ALL 时是否需要包含归档表: + * 归档任务只迁移 7 天前的数据,若查询结束日期在最近 7 天内,归档表无数据,可跳过。 + */ + private boolean shouldIncludeArchive(String endTime) { + if (endTime == null || endTime.isEmpty()) { + return true; + } + try { + LocalDate endDate = LocalDate.parse(endTime.substring(0, 10), DATE_FMT); + LocalDate archiveThreshold = LocalDate.now().minusDays(ArchiveDaysConfig.getRetainDays()); + return endDate.isBefore(archiveThreshold); + } catch (Exception e) { + return true; + } + } + + /** + * 对查询结果做内存排序(pageSize 通常 5~50 条,排序零耗时) + */ + private void sortRowsInMemory(List> rows, String sortField, String sortOrder) { + if (rows == null || rows.size() <= 1 || sortField == null) return; + boolean asc = "asc".equalsIgnoreCase(sortOrder); + rows.sort((a, b) -> { + Object va = a.get(sortField); + Object vb = b.get(sortField); + if (va == null && vb == null) return 0; + if (va == null) return asc ? -1 : 1; + if (vb == null) return asc ? 1 : -1; + if (va instanceof Number && vb instanceof Number) { + double da = ((Number) va).doubleValue(); + double db = ((Number) vb).doubleValue(); + return asc ? Double.compare(da, db) : Double.compare(db, da); + } + return asc ? va.toString().compareTo(vb.toString()) : vb.toString().compareTo(va.toString()); + }); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserAvatarController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserAvatarController.java new file mode 100644 index 0000000..e777e04 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserAvatarController.java @@ -0,0 +1,67 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtUserAvatarMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtUserAvatar; +import com.ruoyi.admin.service.TtUserAvatarService; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/admin/userAvatar") +@Slf4j +public class TtUserAvatarController extends BaseController { + + private final TtUserAvatarService userAvatarService; + + public TtUserAvatarController(TtUserAvatarService userAvatarService) { + this.userAvatarService = userAvatarService; + } + + @GetMapping("/list") + public PageDataInfo list() { + startPage(); + List list = userAvatarService.list(); + return getPageData(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtUserAvatar ttUserAvatar = userAvatarService.getById(id); + ttUserAvatar.setAvatar(""); + return R.ok(ttUserAvatar); + } + + @Log(title = "头像设置", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtUserAvatar.class, mapperClass = TtUserAvatarMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtUserAvatar ttUserAvatar) { + if (StringUtils.isEmpty(ttUserAvatar.getAvatar())) ttUserAvatar.setAvatar(""); + else ttUserAvatar.setAvatar(RuoYiConfig.getDomainName() + ttUserAvatar.getAvatar()); + return toAjax(userAvatarService.save(ttUserAvatar)); + } + + @Log(title = "头像设置", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtUserAvatar.class, mapperClass = TtUserAvatarMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtUserAvatar ttUserAvatar) { + String msg = userAvatarService.updateUserAvatarById(ttUserAvatar); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "头像设置", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(userAvatarService.removeByIds(Arrays.asList(ids))); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserBlendErcashController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserBlendErcashController.java new file mode 100644 index 0000000..38bb7bc --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserBlendErcashController.java @@ -0,0 +1,38 @@ +package com.ruoyi.admin.controller; + + +import com.ruoyi.admin.service.TtUserBlendErcashService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.userRecord.BlendErcashCondition; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Api(tags = "管理端 收支明细") +@RestController +@RequestMapping("/admin/blendErcash") +@Slf4j +public class TtUserBlendErcashController extends BaseController { + + @Autowired + private TtUserBlendErcashService ttUserBlendErcashService; + + @ApiOperation("收支明细") + @PostMapping("/list") + public R list(@RequestBody @Validated BlendErcashCondition condition) { + return ttUserBlendErcashService.byCondition(condition); + } + + @ApiOperation("收支明细-仅归档表") + @PostMapping("/archive/list") + public R archiveList(@RequestBody @Validated BlendErcashCondition condition) { + return ttUserBlendErcashService.byConditionArchive(condition); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserController.java new file mode 100644 index 0000000..80f2655 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserController.java @@ -0,0 +1,276 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.sys.TtPromotionUpdate; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.ApiUserOnline; +import com.ruoyi.domain.other.TtUserBody; +import com.ruoyi.domain.other.TtUserPackSackBody; +import com.ruoyi.domain.vo.TtUserPackSackDataVO; +import com.ruoyi.domain.vo.user.TtUserVo; +import io.swagger.annotations.ApiOperation; +import jakarta.servlet.http.HttpServletResponse; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@RequestMapping("/admin/user") +public class TtUserController extends BaseController { + + private final TtUserService userService; + private final RedisCache redisCache; + private final TtBoxRecordsService boxRecordsService; + + public TtUserController(TtUserService ttUserService, + RedisCache redisCache, + TtBoxRecordsService boxRecordsService) { + this.userService = ttUserService; + this.redisCache = redisCache; + this.boxRecordsService = boxRecordsService; + } + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtPromotionUpdateMapper promotionUpdateMapper; + + @GetMapping("/list") + public PageDataInfo list(TtUserBody ttUserBody) { + startPage(); + Pair> list = userService.queryList(ttUserBody); + list.getValue().forEach(ttUser -> ttUser.setPassword("")); + PageDataInfo pageDataInfo = getPageData(list.getValue()); + pageDataInfo.setTotal(list.getKey()); + return pageDataInfo; + } + + @PostMapping("/export") + public void export(HttpServletResponse response, TtUserBody ttUserBody) { + Pair> list = userService.queryList(ttUserBody); + ExcelUtil util = new ExcelUtil<>(TtUserVo.class); + util.exportExcel(response, list.getValue(), "用户信息列表"); + } + + @GetMapping(value = "/{userId}") + public R getInfo(@PathVariable("userId") Long userId) { + TtUser user = userService.getById(userId); + user.setPassword(null); + user.setSafePassword(null); + if (user.getRemark() != null && user.getRemark().length() > 5) user.setRemark(user.getRemark().substring(5)); + return R.ok(user); + } + + @Log(title = "用户列表", action = "生成招商账号", businessType = BusinessType.INSERT) + @ApiOperation("生成招商账号") + @PostMapping("/generateAccount") + public void generateAccount(HttpServletResponse response, @RequestBody GenerateAccountBody body) { + List userList = userService.getAccountList(body.getNum(), body.getCustomPhoneNumbers()); + userService.generateAccount(response, userList); + } + + @ApiOperation("生成账号") + @GetMapping("/generateAccountByPhoneNum") + public R generateAccountByPhoneNum(HttpServletResponse response, @RequestParam("phoneNum") String phoneNum, + @RequestParam("password") String password) { + TtUser user = userService.getAccountList(phoneNum, password); + return R.ok(user); + } + + @Log(title = "用户列表", action = "生成机器人", businessType = BusinessType.INSERT) + @ApiOperation("生成机器人") + @PostMapping("/generateRobot/{num}") + public void generateRobot(HttpServletResponse response, @PathVariable("num") Integer num) { + List userList = userService.getRobotList(num); + userService.generateAccount(response, userList); + } + + @Log(title = "用户列表", action = "生成白号", businessType = BusinessType.INSERT) + @ApiOperation("生成白号") + @PostMapping("/generateBlankAccount") + public void generateBlankAccount(HttpServletResponse response, @RequestBody GenerateAccountBody body) { + List userList = userService.getBlankAccountList(body.getNum(), body.getCustomPhoneNumbers()); + userService.generateAccount(response, userList); + } + + @Log(title = "用户列表", action = "生成卡商账号", businessType = BusinessType.INSERT) + @ApiOperation("生成卡商账号") + @PostMapping("/generateCardMerchant") + public void generateCardMerchant(HttpServletResponse response, @RequestBody GenerateAccountBody body) { + List userList = userService.getCardMerchantList(body.getNum(), body.getCustomPhoneNumbers()); + userService.generateAccount(response, userList); + } + + @Log(title = "用户列表", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtUser.class, mapperClass = TtUserMapper.class) + @ApiOperation("修改用户信息") + @PutMapping + public AjaxResult edit(@RequestBody TtUser ttUser) { + ttUser.setUpdateBy(getUsername()); + ttUser.setUpdateTime(DateUtils.getNowDate()); + // 余额/积分变动写入流水(source=65 管理员调整余额 / source=66 管理员调整积分),操作人+登录IP记入备注 + String operIp = getLoginUser().getIpaddr(); + if (StringUtils.isEmpty(operIp)) { + operIp = IpUtils.getIpAddr(); + } + userService.recordAdminBalanceCreditChange(ttUser, getUsername(), operIp); + return userService.updateUserById(ttUser); + } + + @Log(title = "用户列表", action = "修改邀请码", businessType = BusinessType.UPDATE, + entityClass = TtUser.class, mapperClass = TtUserMapper.class) + @ApiOperation("修改邀请码") + @PostMapping("/editInvitationCode") + public AjaxResult editInvitationCode(@RequestBody TtUser ttUser) { + ttUser.setUpdateBy(getUsername()); + ttUser.setUpdateTime(DateUtils.getNowDate()); + return userService.updateUserInvitationCodeById(ttUser); + } + + @Log(title = "用户列表", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{userIds}") + public AjaxResult remove(@PathVariable Long[] userIds) { + // 逻辑删除前级联解绑:将每个用户的所有下级 parent_id 置空 + for (Long userId : userIds) { + try { + userService.cascadeUnbindChildren(userId.intValue()); + } catch (Exception e) { + log.error("[逻辑删除] userId={} 级联解绑失败", userId, e); + } + } + return toAjax(userService.removeByIds(Arrays.asList(userIds))); + } + + @GetMapping("/online/list") + public PageDataInfo list(String ipaddr, String userName) { + startPage(); + Collection redisKeys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); + List userOnlineList = redisKeys.stream().map(redisKey -> { + LoginUser user = redisCache.getCacheObject(redisKey); + if (StringUtils.isNull(user)) { + return null; + } + // 仅保留有活跃心跳的用户(心跳由 JwtAuthenticationTokenFilter 随请求写入,TTL 3分钟), + // 避免把所有未过期 token(30天)都算作在线导致首页在线用户虚高 + if (!redisCache.hasKey(CacheConstants.ONLINE_HEARTBEAT_KEY + user.getToken())) { + return null; + } + if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) { + return userService.selectOnlineByInfo(ipaddr, userName, user); + } else if (StringUtils.isNotEmpty(ipaddr)) { + return userService.selectOnlineByIpaddr(ipaddr, user); + } else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser())) { + return userService.selectOnlineByUserName(userName, user); + } else { + return userService.loginUserToUserOnline(user); + } + }).collect(Collectors.toList()); + userOnlineList.removeAll(Collections.singleton(null)); + // 按最新登录时间降序排列 + userOnlineList.sort(Comparator.comparing(ApiUserOnline::getLoginTime, + Comparator.nullsLast(Comparator.reverseOrder()))); + return getPageData(userOnlineList); + } + + @GetMapping("/getPackSack") + public PageDataInfo getPackSack(TtUserPackSackBody ttUserPackSackBody) { + startPage(); + List list = userService.getPackSack(ttUserPackSackBody); + return getPageData(list); + } + + @Log(title = "背包列表", action = "删除", businessType = BusinessType.DELETE) + @PostMapping("/removeUserPackSackData/{ids}") + public AjaxResult removeUserPackSackData(@PathVariable Long[] ids) { + List boxRecordsList = boxRecordsService.getBaseMapper().selectBatchIds(Arrays.asList(ids)); + boxRecordsList = boxRecordsList.stream().peek(ttBoxRecords -> { + ttBoxRecords.setStatus(TtboxRecordStatus.ADMIN_DELETE.getCode()); + ttBoxRecords.setUpdateTime(DateUtils.getNowDate()); + }).collect(Collectors.toList()); + return toAjax(boxRecordsService.updateBatchById(boxRecordsList, 1)); + } + + @GetMapping("/getUserProfitStatistics/{userId}") + public R getUserProfitStatistics(@PathVariable("userId") Integer userId) { + Map map = userService.getUserProfitStatistics(userId); + return R.ok(map); + } + + @Log(title = "用户列表", action = "重置余额积分", businessType = BusinessType.CLEAN, + entityClass = TtUser.class, mapperClass = TtUserMapper.class, idField = "userId") + @ApiOperation("重置用户余额和积分") + @PostMapping("/resetBalance/{userId}") + public AjaxResult resetUserBalance(@PathVariable("userId") Integer userId){ + TtUser ttUser = new TtUser(); + ttUser.setUserId(userId); + ttUser.setAccountAmount(BigDecimal.ZERO); + ttUser.setAccountCredits(BigDecimal.ZERO); + ttUser.setMerchantBalance(BigDecimal.ZERO); + ttUser.setUpdateBy(getUsername()); + ttUser.setUpdateTime(DateUtils.getNowDate()); + return userService.updateUserById(ttUser); + } + + @Log(title = "用户列表", action = "真删除", businessType = BusinessType.FORCE) + @ApiOperation("真删除用户(数据库删除,不可恢复)") + @DeleteMapping("/forceDelete/{userId}") + public AjaxResult forceDelete(@PathVariable("userId") Integer userId) { + // 0. 先清登录态:物理删除后 tt_user 查不到该用户,若不清 Redis Token, + // 已登录客户端仍能带旧 Token 调接口(/api/getAccountAmount 会一直空指针且无法登出) + userService.kickOutLoginUsers(Collections.singletonList(userId.longValue()), "您的账号已被删除"); + // 1. 级联解绑:将该用户的所有下级(含下级的下级)parent_id 置空 + try { + userService.cascadeUnbindChildren(userId); + } catch (Exception e) { + log.error("[真删除] userId={} 级联解绑失败", userId, e); + } + // 2. 清空该用户背包中的饰品记录(holder_user_id = userId) + boxRecordsService.remove(new LambdaQueryWrapper() + .eq(TtBoxRecords::getHolderUserId, userId)); + // 3. 清理推广更新记录,避免该 userId 被重新分配后注册冲突 + promotionUpdateMapper.delete(new LambdaQueryWrapper() + .eq(TtPromotionUpdate::getEmployeeId, userId)); + // 4. 清理副号绑定关系,避免该 userId 被重新分配后绑定冲突 + ttUserMapper.removeUserSubAccountByUserId(userId); + // 5. 删除用户 + ttUserMapper.removeByUserId(userId); + return AjaxResult.success("用户已彻底删除,背包已清空,下级已解绑"); + } + + /** + * 生成账号请求体:num-数量;customPhoneNumbers-自定义手机号(按位置对应,留空则随机生成) + */ + @Data + public static class GenerateAccountBody { + private Integer num; + private List customPhoneNumbers; + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserCreditsRecordsController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserCreditsRecordsController.java new file mode 100644 index 0000000..ba237c4 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtUserCreditsRecordsController.java @@ -0,0 +1,30 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords; +import com.ruoyi.admin.service.TtUserCreditsRecordsService; +import com.ruoyi.domain.other.TtUserCreditsRecordsBody; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.page.PageDataInfo; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/admin/userCreditsRecords") +public class TtUserCreditsRecordsController extends BaseController { + + private final TtUserCreditsRecordsService userCreditsRecordsService; + + public TtUserCreditsRecordsController(TtUserCreditsRecordsService userCreditsRecordsService) { + this.userCreditsRecordsService = userCreditsRecordsService; + } + + @GetMapping("/list") + public PageDataInfo list(TtUserCreditsRecordsBody ttUserCreditsRecordsBody) { + startPage(); + List list = userCreditsRecordsService.queryList(ttUserCreditsRecordsBody); + return getPageData(list); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtVipLevelController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtVipLevelController.java new file mode 100644 index 0000000..88e081e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtVipLevelController.java @@ -0,0 +1,69 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.mapper.TtVipLevelMapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.domain.other.TtVipLevel; +import com.ruoyi.admin.service.TtVipLevelService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/admin/vipLevel") +public class TtVipLevelController extends BaseController { + + private final TtVipLevelService vipLevelService; + + public TtVipLevelController(TtVipLevelService vipLevelService) { + this.vipLevelService = vipLevelService; + } + + @GetMapping("/list") + public PageDataInfo list() { + // VIP 等级总量不多,后台设置页也未接入分页组件,改为全量返回 + List list = vipLevelService.list(); + PageDataInfo data = new PageDataInfo<>(); + data.setRows(list); + data.setTotal(list == null ? 0 : list.size()); + data.setCode(200); + data.setMsg("查询成功"); + return data; + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + TtVipLevel ttVipLevel = vipLevelService.getById(id); + ttVipLevel.setIcon(""); + return R.ok(ttVipLevel); + } + + @Log(title = "VIP等级设置", action = "生成VIP等级", businessType = BusinessType.INSERT) + @PostMapping("generateVipLevel/{num}") + public AjaxResult generateVipLevel(@PathVariable("num") Integer num) { + String msg = vipLevelService.generateVipLevel(num); + return StringUtils.isEmpty(msg) ? AjaxResult.success("生成成功,请修改参数!") : AjaxResult.error(msg); + } + + @Log(title = "VIP等级设置", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtVipLevel.class, mapperClass = TtVipLevelMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtVipLevel ttVipLevel) { + ttVipLevel.setUpdateTime(DateUtils.getNowDate()); + String msg = vipLevelService.updateVipLevelById(ttVipLevel); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "VIP等级设置", action = "重置", businessType = BusinessType.CLEAN) + @PostMapping + public AjaxResult truncateVipLevel() { + vipLevelService.truncateVipLevel(); + return AjaxResult.success("重置成功,请重新设置!"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtWelfareController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtWelfareController.java new file mode 100644 index 0000000..641226d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtWelfareController.java @@ -0,0 +1,148 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.mapper.TtWelfareMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtWelfareService; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.other.TtBoxRecordsBody; +import com.ruoyi.domain.other.TtWelfare; +import com.ruoyi.domain.vo.TtBoxRecordsDataVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.List; + +/** + * 福利列表Controller + * + * @author ruoyi + * @date 2024-05-11 + */ +@RestController +@RequestMapping("/admin/welfare") +public class TtWelfareController extends BaseController +{ + @Autowired + private TtWelfareService ttWelfareService; + + @Autowired + private TtBoxRecordsService ttBoxRecordsService; + + /** + * 查询福利列表 + */ + @PreAuthorize("isAuthenticated()") + @GetMapping("/list") + public TableDataInfo list(TtWelfare ttWelfare) + { + startPage(); + List list = ttWelfareService.selectTtWelfareList(ttWelfare); + return getDataTable(list); + } + + /** + * 导出福利列表 + */ + @PreAuthorize("isAuthenticated()") + @Log(title = "福利列表", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, TtWelfare ttWelfare) + { + List list = ttWelfareService.selectTtWelfareList(ttWelfare); + ExcelUtil util = new ExcelUtil(TtWelfare.class); + util.exportExcel(response, list, "福利列表数据"); + } + + /** + * 获取福利列表详细信息 + */ + @PreAuthorize("isAuthenticated()") + @GetMapping(value = "/{welfareId}") + public AjaxResult getInfo(@PathVariable("welfareId") Integer welfareId) + { + return success(ttWelfareService.selectTtWelfareByWelfareId(welfareId)); + } + + /** + * 新增福利列表 + */ + @PreAuthorize("isAuthenticated()") + @Log(title = "福利列表", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtWelfare.class, mapperClass = TtWelfareMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtWelfare ttWelfare) + { + return toAjax(ttWelfareService.insertTtWelfare(ttWelfare)); + } + + /** + * 修改福利列表 + */ + @PreAuthorize("isAuthenticated()") + @Log(title = "福利列表", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtWelfare.class, mapperClass = TtWelfareMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtWelfare ttWelfare) + { + return toAjax(ttWelfareService.updateTtWelfare(ttWelfare)); + } + + /** + * 删除福利列表 + */ + @PreAuthorize("isAuthenticated()") + @Log(title = "福利列表", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{welfareIds}") + public AjaxResult remove(@PathVariable Integer[] welfareIds) + { + return toAjax(ttWelfareService.deleteTtWelfareByWelfareIds(welfareIds)); + } + + /** + * 查询福利箱子开箱记录 + */ + @PreAuthorize("isAuthenticated()") + @GetMapping("/boxRecords") + public TableDataInfo boxRecordsList(TtBoxRecordsBody ttBoxRecordsBody) + { + startPage(); + List list = ttBoxRecordsService.selectWelfareBoxRecordsList(ttBoxRecordsBody); + return getDataTable(list); + } + + /** + * 删除福利箱子开箱记录(支持批量) + */ + @PreAuthorize("isAuthenticated()") + @Log(title = "福利开箱记录", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/boxRecords/{ids}") + public AjaxResult removeBoxRecords(@PathVariable Long[] ids) + { + return toAjax(ttBoxRecordsService.removeByIds(Arrays.asList(ids))); + } + + /** + * 清空全部福利开箱记录(source=10) + */ + @PreAuthorize("isAuthenticated()") + @Log(title = "福利开箱记录", action = "清空全部记录", businessType = BusinessType.CLEAN) + @DeleteMapping("/boxRecords/clear") + public AjaxResult clearBoxRecords() + { + return toAjax(ttBoxRecordsService.remove( + new LambdaQueryWrapper() + .eq(TtBoxRecords::getSource, 10) + )); + } +} + diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtWelfareMonthlyRechargesController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtWelfareMonthlyRechargesController.java new file mode 100644 index 0000000..018448e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TtWelfareMonthlyRechargesController.java @@ -0,0 +1,107 @@ +package com.ruoyi.admin.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.admin.mapper.TtWelfareMonthlyRechargesMapper; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.domain.other.TtWelfareMonthlyRecharges; +import com.ruoyi.admin.service.ITtWelfareMonthlyRechargesService; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 每月充值福利Controller + * + * @author ruoyi + * @date 2024-06-24 + */ +@RestController +@RequestMapping("/admin/recharges") +public class TtWelfareMonthlyRechargesController extends BaseController +{ + @Autowired + private ITtWelfareMonthlyRechargesService ttWelfareMonthlyRechargesService; + + /** + * 查询每月充值福利列表 + */ + @PreAuthorize("@ss.hasPermi('admin:recharges:list')") + @GetMapping("/list") + public TableDataInfo list(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges) + { + startPage(); + List list = ttWelfareMonthlyRechargesService.selectTtWelfareMonthlyRechargesList(ttWelfareMonthlyRecharges); + return getDataTable(list); + } + + /** + * 导出每月充值福利列表 + */ + @PreAuthorize("@ss.hasPermi('admin:recharges:export')") + @Log(title = "每月充值福利", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges) + { + List list = ttWelfareMonthlyRechargesService.selectTtWelfareMonthlyRechargesList(ttWelfareMonthlyRecharges); + ExcelUtil util = new ExcelUtil(TtWelfareMonthlyRecharges.class); + util.exportExcel(response, list, "每月充值福利数据"); + } + + /** + * 获取每月充值福利详细信息 + */ + @PreAuthorize("@ss.hasPermi('admin:recharges:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Integer id) + { + return success(ttWelfareMonthlyRechargesService.selectTtWelfareMonthlyRechargesById(id)); + } + + /** + * 新增每月充值福利 + */ + @PreAuthorize("@ss.hasPermi('admin:recharges:add')") + @Log(title = "每月充值福利", action = "新增", businessType = BusinessType.INSERT, + entityClass = TtWelfareMonthlyRecharges.class, mapperClass = TtWelfareMonthlyRechargesMapper.class) + @PostMapping + public AjaxResult add(@RequestBody TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges) + { + return toAjax(ttWelfareMonthlyRechargesService.insertTtWelfareMonthlyRecharges(ttWelfareMonthlyRecharges)); + } + + /** + * 修改每月充值福利 + */ + @PreAuthorize("@ss.hasPermi('admin:recharges:edit')") + @Log(title = "每月充值福利", action = "修改", businessType = BusinessType.UPDATE, + entityClass = TtWelfareMonthlyRecharges.class, mapperClass = TtWelfareMonthlyRechargesMapper.class) + @PutMapping + public AjaxResult edit(@RequestBody TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges) + { + return toAjax(ttWelfareMonthlyRechargesService.updateTtWelfareMonthlyRecharges(ttWelfareMonthlyRecharges)); + } + + /** + * 删除每月充值福利 + */ + @PreAuthorize("@ss.hasPermi('admin:recharges:remove')") + @Log(title = "每月充值福利", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) + { + return toAjax(ttWelfareMonthlyRechargesService.deleteTtWelfareMonthlyRechargesByIds(ids)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryAdminController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryAdminController.java new file mode 100644 index 0000000..83358d0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryAdminController.java @@ -0,0 +1,250 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.TurnoverLotteryMapper; +import com.ruoyi.admin.mapper.TurnoverLotteryPrizeMapper; +import com.ruoyi.admin.mapper.TurnoverLotteryRecordMapper; +import com.ruoyi.admin.service.TurnoverLotteryService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.lottery.TtTurnoverLottery; +import com.ruoyi.domain.entity.lottery.TtTurnoverLotteryPrize; +import com.ruoyi.domain.entity.lottery.TtTurnoverLotteryRecord; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 后台管理 - 流水抽奖配置 + */ +@Api(tags = "后台流水抽奖管理") +@RestController +@RequestMapping("/admin/turnoverLottery") +public class TurnoverLotteryAdminController extends BaseController { + + @Autowired + private TurnoverLotteryService lotteryService; + + @Autowired + private TurnoverLotteryMapper lotteryMapper; + + @Autowired + private TurnoverLotteryPrizeMapper prizeMapper; + + @Autowired + private TtOrnamentMapper ornamentMapper; + + @Autowired + private TurnoverLotteryRecordMapper recordMapper; + + @Autowired + private RedisLock redisLock; + + /** + * 获取抽奖活动列表 + */ + @ApiOperation("获取抽奖活动列表") + @GetMapping("/list") + public PageDataInfo list( + @RequestParam(required = false) String lotteryName, + @RequestParam(required = false) Integer status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(StringUtils.isNotBlank(lotteryName), TtTurnoverLottery::getLotteryName, lotteryName) + .eq(status != null, TtTurnoverLottery::getStatus, status) + .orderByAsc(TtTurnoverLottery::getSort, TtTurnoverLottery::getId); + + startPage(); + List list = lotteryMapper.selectList(wrapper); + + // 动态计算奖品总价值(按初始真实数量 × 单价累加,与用户端 myStats 口径一致) + for (TtTurnoverLottery lottery : list) { + LambdaQueryWrapper prizeWrapper = new LambdaQueryWrapper<>(); + prizeWrapper.eq(TtTurnoverLotteryPrize::getLotteryId, lottery.getId()); + List prizes = prizeMapper.selectList(prizeWrapper); + BigDecimal totalValue = prizes.stream() + .map(p -> { + BigDecimal price = p.getPrice() != null ? p.getPrice() : BigDecimal.ZERO; + int count = p.getInitialRealCount() != null ? p.getInitialRealCount() + : (p.getRealCount() != null ? p.getRealCount() : 0); + return price.multiply(new BigDecimal(count)); + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + lottery.setTotalValue(totalValue); + } + + return getPageData(list); + } + + /** + * 保存抽奖奖品配置 + */ + @ApiOperation("保存抽奖奖品") + @PostMapping("/save/{lotteryId}") + public R savePrizes( + @PathVariable("lotteryId") Long lotteryId, + @RequestBody List> prizeList) { + String lockKey = RedisConstants.TURNOVER_LOTTERY_DRAW_BOX_LOCK + lotteryId; + boolean locked = redisLock.tryLock(lockKey, 0L, -1L, TimeUnit.SECONDS); + if (!locked) { + return R.fail("奖品配置保存中,请稍后再试"); + } + try { + return lotteryService.savePrizes(lotteryId, prizeList); + } finally { + redisLock.unlock(lockKey); + } + } + + /** + * 更新抽奖活动状态 + */ + @ApiOperation("更新抽奖状态") + @PostMapping("/toggleStatus") + public R toggleStatus(@RequestBody Map body) { + Long lotteryId = body.get("lotteryId") != null ? ((Number) body.get("lotteryId")).longValue() : null; + Integer status = body.get("status") != null ? ((Number) body.get("status")).intValue() : null; + + if (lotteryId == null || status == null) { + return R.fail("参数错误"); + } + + // 加宝箱锁,串行"关闭活动"与"抽奖中"的竞态(避免关闭后仍抽出一两单) + String lockKey = RedisConstants.TURNOVER_LOTTERY_DRAW_BOX_LOCK + lotteryId; + boolean locked = redisLock.tryLock(lockKey, 0L, -1L, TimeUnit.SECONDS); + if (!locked) { + return R.fail("操作处理中,请稍后再试"); + } + try { + return lotteryService.toggleStatus(lotteryId, status); + } finally { + redisLock.unlock(lockKey); + } + } + + @Log(title = "抽奖活动", action = "新增抽奖活动", businessType = BusinessType.INSERT) + /** + * 创建新的抽奖活动 + */ + @ApiOperation("创建抽奖活动") + @PostMapping("/create") + public R create(@RequestBody Map body) { + String lotteryName = body.get("lotteryName") != null ? body.get("lotteryName").toString() : null; + Integer turnoverThreshold = body.get("turnoverThreshold") != null + ? ((Number) body.get("turnoverThreshold")).intValue() : 3000; + + if (lotteryName == null || lotteryName.trim().isEmpty()) { + return R.fail("抽奖名称不能为空"); + } + if (turnoverThreshold == null || turnoverThreshold <= 0) { + return R.fail("流水门槛必须大于0"); + } + + TtTurnoverLottery lottery = new TtTurnoverLottery(); + lottery.setLotteryName(lotteryName); + lottery.setTurnoverThreshold(turnoverThreshold); + lottery.setLotteryType(1); // 流水抽奖类型 + lottery.setStatus(0); // 默认关闭 + lottery.setSort(1); + lottery.setCreateBy(getUsername()); + lottery.setCreateTime(new java.util.Date()); + + lotteryMapper.insert(lottery); + return R.ok(lottery); + } + + @Log(title = "抽奖活动", action = "删除", businessType = BusinessType.DELETE) + /** + * 删除抽奖活动 + */ + @ApiOperation("删除抽奖活动") + @DeleteMapping("/delete/{lotteryId}") + public R delete(@PathVariable("lotteryId") Long lotteryId) { + lotteryMapper.deleteById(lotteryId); + return R.ok("删除成功"); + } + + /** + * 搜索饰品 (用于奖品配置时调用) + */ + @ApiOperation("搜索饰品") + @GetMapping("/searchOrnaments") + public R> searchOrnaments( + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "limit", defaultValue = "50") Integer limit) { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StringUtils.isNotEmpty(name)) { + wrapper.and(w -> w + .like(TtOrnament::getName, name) + .or().like(TtOrnament::getShortName, name) + .or().like(TtOrnament::getMarketHashName, name)); + } + + wrapper.orderByAsc(TtOrnament::getUsePrice) + .last("LIMIT " + Math.min(limit, 100)); + + return R.ok(ornamentMapper.selectList(wrapper)); + } + + @Log(title = "抽奖活动", action = "修改", businessType = BusinessType.UPDATE) + /** + * 更新抽奖活动基础信息 + */ + @ApiOperation("更新抽奖活动") + @PostMapping("/update") + public R update(@RequestBody Map body) { + Long lotteryId = body.get("lotteryId") != null ? ((Number) body.get("lotteryId")).longValue() : null; + if (lotteryId == null) { + lotteryId = body.get("id") != null ? ((Number) body.get("id")).longValue() : null; + } + String lotteryName = body.get("lotteryName") != null ? body.get("lotteryName").toString() : null; + Integer turnoverThreshold = body.get("turnoverThreshold") != null + ? ((Number) body.get("turnoverThreshold")).intValue() : null; + Integer sort = body.get("sort") != null ? ((Number) body.get("sort")).intValue() : null; + + if (lotteryId == null) { + return R.fail("抽奖ID不能为空"); + } + if (turnoverThreshold != null && turnoverThreshold <= 0) { + return R.fail("流水门槛必须大于0"); + } + + TtTurnoverLottery lottery = new TtTurnoverLottery(); + lottery.setId(lotteryId); + if (StringUtils.isNotEmpty(lotteryName)) { + lottery.setLotteryName(lotteryName); + } + if (turnoverThreshold != null) { + lottery.setTurnoverThreshold(turnoverThreshold); + } + if (sort != null) { + lottery.setSort(sort); + } + lottery.setUpdateTime(new java.util.Date()); + + lotteryMapper.updateById(lottery); + return R.ok("更新成功"); + } + + public String getUsername() { + var loginUser = getLoginUser(); + return loginUser != null && loginUser.getUserData() != null + ? loginUser.getUserData().getUserName() + : "admin"; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryPrizeController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryPrizeController.java new file mode 100644 index 0000000..5cf0eb8 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryPrizeController.java @@ -0,0 +1,258 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.mapper.TurnoverLotteryMapper; +import com.ruoyi.admin.mapper.TurnoverLotteryPrizeMapper; +import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; +import com.ruoyi.admin.service.TurnoverLotteryService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.lottery.TtTurnoverLottery; +import com.ruoyi.domain.entity.lottery.TtTurnoverLotteryPrize; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 流水抽奖奖品管理 (仿宝箱饰品管理 boxOrnaments) + */ +@Api(tags = "流水抽奖奖品管理") +@RestController +@RequestMapping("/admin/turnoverLotteryPrize") +public class TurnoverLotteryPrizeController extends BaseController { + + @Autowired + private TurnoverLotteryPrizeMapper prizeMapper; + + @Autowired + private TurnoverLotteryMapper lotteryMapper; + + @Autowired + private TurnoverLotteryService lotteryService; + + @Autowired + private TtOrnamentsLevelMapper ornamentsLevelMapper; + + @Autowired + private RedisLock redisLock; + + /** + * 获取奖品列表 (分页,含爆率百分比计算) + */ + @ApiOperation("奖品列表") + @GetMapping("/list/{lotteryId}") + public PageDataInfo list(@PathVariable("lotteryId") Long lotteryId) { + // 先算总数(避免 PageHelper 干扰百分比计算) + int[] totals = sumOdds(lotteryId); + startPage(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtTurnoverLotteryPrize::getLotteryId, lotteryId); + wrapper.orderByAsc(TtTurnoverLotteryPrize::getSort, TtTurnoverLotteryPrize::getId); + List list = prizeMapper.selectList(wrapper); + + // 填充计算字段 + int totalDisplay = Math.max(totals[0], 1); + int totalReal = Math.max(totals[1], 1); + + // 批量获取等级图片 + List levelList = ornamentsLevelMapper.selectList(null); + Map levelImgMap = levelList.stream() + .collect(Collectors.toMap(TtOrnamentsLevel::getId, TtOrnamentsLevel::getLevelImg, (a, b) -> a)); + + for (TtTurnoverLotteryPrize prize : list) { + int dc = prize.getDisplayCount() != null ? prize.getDisplayCount() : 0; + int rc = prize.getRealCount() != null ? prize.getRealCount() : 0; + int tc = prize.getTotalCount() != null ? prize.getTotalCount() : 0; + + // 显示爆率百分比 + double displayPct = (double) dc / totalDisplay * 100; + prize.setTheoreticalRate(Math.round(displayPct * 100.0) / 100.0); + prize.setDisplayPercent(String.format("%.2f%%", displayPct)); + + // 真实爆率百分比(基于当前剩余库存 realCount,与抽奖实际权重、用户端 myStats 一致) + double realPct = (double) rc / totalReal * 100; + prize.setRealRate(Math.round(realPct * 100.0) / 100.0); + prize.setRealPercent(String.format("%.2f%%", realPct)); + + // 当前剩余库存 = realCount(当前剩余) + prize.setRealRemaining(Math.max(rc, 0)); + + // 等级图片 + Integer levelId = prize.getOrnamentLevel(); + if (levelId != null) { + prize.setLevelImg(levelImgMap.get(levelId)); + } + } + + return getPageData(list); + } + + /** + * 获取单个奖品详情 + */ + @ApiOperation("奖品详情") + @GetMapping("/{id}") + public R getInfo(@PathVariable("id") Long id) { + return R.ok(prizeMapper.selectById(id)); + } + + @Log(title = "奖项配置", action = "新增", businessType = BusinessType.INSERT) + /** + * 新增奖品 + */ + @ApiOperation("新增奖品") + @PostMapping + public AjaxResult add(@RequestBody TtTurnoverLotteryPrize prize) { + if (prize == null || prize.getLotteryId() == null) { + return AjaxResult.error("抽奖活动ID不能为空"); + } + if (prize.getOrnamentName() == null || prize.getOrnamentName().trim().isEmpty()) { + return AjaxResult.error("奖品名称不能为空"); + } + if (prize.getPrice() == null || prize.getPrice().compareTo(BigDecimal.ZERO) < 0) { + return AjaxResult.error("奖品金额不能小于0"); + } + + // 兼容旧前端:未传 initialRealCount 时取 realCount + if (prize.getInitialRealCount() == null) { + prize.setInitialRealCount(prize.getRealCount()); + } + // 新建时当前剩余等于初始配置 + if (prize.getRealCount() == null) { + prize.setRealCount(prize.getInitialRealCount()); + } + + // 初始真实数量(奖池权重)必须大于 0,否则该奖品永远不会被抽中 + if (prize.getInitialRealCount() == null || prize.getInitialRealCount() <= 0) { + return AjaxResult.error("真实数量必须大于0"); + } + if (prize.getDisplayCount() != null && prize.getDisplayCount() < 0) { + return AjaxResult.error("展示数量不能小于0"); + } + + // 校验奖品数量上限:每个宝箱最多 8 个奖品 + LambdaQueryWrapper countWrapper = new LambdaQueryWrapper<>(); + countWrapper.eq(TtTurnoverLotteryPrize::getLotteryId, prize.getLotteryId()); + Long count = prizeMapper.selectCount(countWrapper); + if (count != null && count >= 8) { + return AjaxResult.error("每个宝箱最多配置 8 个奖品,当前已满"); + } + + prize.setCreateTime(DateUtils.getNowDate()); + prizeMapper.insert(prize); + return AjaxResult.success(); + } + + @Log(title = "奖项配置", action = "修改", businessType = BusinessType.UPDATE) + /** + * 修改奖品 + */ + @ApiOperation("修改奖品") + @PutMapping + public AjaxResult edit(@RequestBody TtTurnoverLotteryPrize prize) { + Long lotteryId = prize.getLotteryId(); + String lockKey = lotteryId != null ? RedisConstants.TURNOVER_LOTTERY_DRAW_BOX_LOCK + lotteryId : null; + boolean locked = false; + if (lockKey != null) { + locked = redisLock.tryLock(lockKey, 0L, -1L, TimeUnit.SECONDS); + if (!locked) { + return AjaxResult.error("奖品配置处理中,请稍后再试"); + } + } + try { + prize.setUpdateTime(DateUtils.getNowDate()); + // 兼容旧前端:只传了 realCount 未传 initialRealCount 时,视为修改初始配置 + if (prize.getInitialRealCount() == null && prize.getRealCount() != null) { + prize.setInitialRealCount(prize.getRealCount()); + } + + // 参照经典开箱:修改“数量(initialRealCount)”时, + // “当前剩余库存(realCount)”同步增减相同差值 + if (prize.getId() != null && prize.getInitialRealCount() != null) { + TtTurnoverLotteryPrize old = prizeMapper.selectById(prize.getId()); + if (old != null) { + int oldInitial = old.getInitialRealCount() != null ? old.getInitialRealCount() : 0; + int delta = prize.getInitialRealCount() - oldInitial; + if (delta != 0) { + int oldReal = old.getRealCount() != null ? old.getRealCount() : 0; + prize.setRealCount(Math.max(oldReal + delta, 0)); + } + } + } + + prizeMapper.updateById(prize); + return AjaxResult.success(); + } finally { + if (locked) { + redisLock.unlock(lockKey); + } + } + } + + @Log(title = "奖项配置", action = "删除", businessType = BusinessType.DELETE) + /** + * 删除奖品 + */ + @ApiOperation("删除奖品") + @DeleteMapping("/{lotteryId}/{ids}") + public AjaxResult remove(@PathVariable Long lotteryId, @PathVariable Long[] ids) { + String lockKey = RedisConstants.TURNOVER_LOTTERY_DRAW_BOX_LOCK + lotteryId; + boolean locked = redisLock.tryLock(lockKey, 0L, -1L, TimeUnit.SECONDS); + if (!locked) { + return AjaxResult.error("奖品配置处理中,请稍后再试"); + } + try { + // 校验不能删光奖品:至少保留一个,否则抽奖会报"奖品配置为空" + Long totalCount = prizeMapper.selectCount( + new LambdaQueryWrapper() + .eq(TtTurnoverLotteryPrize::getLotteryId, lotteryId)); + int deleteCount = ids == null ? 0 : ids.length; + if (totalCount != null && totalCount - deleteCount <= 0) { + return AjaxResult.error("不能删除全部奖品,至少保留一个"); + } + prizeMapper.deleteBatchIds(Arrays.asList(ids)); + return AjaxResult.success(); + } finally { + redisLock.unlock(lockKey); + } + } + + + /** + * 计算显示数量和真实数量的总和 + */ + private int[] sumOdds(Long lotteryId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtTurnoverLotteryPrize::getLotteryId, lotteryId); + wrapper.select( + TtTurnoverLotteryPrize::getDisplayCount, + TtTurnoverLotteryPrize::getRealCount); + List list = prizeMapper.selectList(wrapper); + + int totalDisplay = 0; + int totalReal = 0; + for (TtTurnoverLotteryPrize p : list) { + totalDisplay += p.getDisplayCount() != null ? p.getDisplayCount() : 0; + totalReal += p.getRealCount() != null ? p.getRealCount() : 0; + } + return new int[]{totalDisplay, totalReal}; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryRecordController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryRecordController.java new file mode 100644 index 0000000..6921f99 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryRecordController.java @@ -0,0 +1,69 @@ +package com.ruoyi.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.mapper.TurnoverLotteryRecordMapper; +import com.ruoyi.admin.mapper.TurnoverLotteryMapper; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.lottery.TtTurnoverLotteryRecord; +import com.ruoyi.domain.entity.lottery.TtTurnoverLottery; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 后台管理 - 抽奖记录查询 + */ +@Api(tags = "后台抽奖记录管理") +@RestController +@RequestMapping("/admin/turnoverLotteryRecord") +public class TurnoverLotteryRecordController extends BaseController { + + @Autowired + private TurnoverLotteryRecordMapper recordMapper; + + @Autowired + private TurnoverLotteryMapper lotteryMapper; + + /** + * 查询抽奖记录列表 + */ + @ApiOperation("查询抽奖记录") + @GetMapping("/list") + public PageDataInfo list( + @RequestParam(value = "userId", required = false) Long userId, + @RequestParam(value = "lotteryId", required = false) Long lotteryId) { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (userId != null) { + wrapper.eq(TtTurnoverLotteryRecord::getUserId, userId); + } + if (lotteryId != null) { + wrapper.eq(TtTurnoverLotteryRecord::getLotteryId, lotteryId); + } + + wrapper.orderByDesc(TtTurnoverLotteryRecord::getCreateTime); + + startPage(); + List list = recordMapper.selectList(wrapper); + return getPageData(list); + } + + /** + * 获取所有抽奖活动(用于下拉选择) + */ + @ApiOperation("获取抽奖活动选项") + @GetMapping("/lotteryOptions") + public R> lotteryOptions() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByAsc(TtTurnoverLottery::getSort, TtTurnoverLottery::getId); + List list = lotteryMapper.selectList(wrapper); + return R.ok(list); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryUserController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryUserController.java new file mode 100644 index 0000000..c22c333 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryUserController.java @@ -0,0 +1,139 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.service.TurnoverLotteryService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; +import java.math.BigDecimal; +import java.util.concurrent.TimeUnit; + +/** + * 用户端 - 流水抽奖 + */ +@Api(tags = "用户流水抽奖") +@RestController +@RequestMapping("/api/turnoverLottery") +public class TurnoverLotteryUserController extends BaseController { + + @Autowired + private TurnoverLotteryService lotteryService; + + @Autowired + private RedisLock redisLock; + + /** + * 获取用户今日流水统计和可参与的抽奖宝箱 + */ + @ApiOperation("获取抽奖信息") + @PostMapping("/myStats") + public R> getMyStats() { + Integer userId = getUserId().intValue(); + return lotteryService.getUserStats(userId); + } + + /** + * 执行抽奖 + */ + @ApiOperation("执行抽奖") + @PostMapping("/draw") + public R> draw(@RequestBody Map params) { + Integer userId = getUserId().intValue(); + if (params == null || params.get("lotteryId") == null) { + return R.fail("参数错误:lotteryId 不能为空"); + } + Long lotteryId; + try { + lotteryId = Long.valueOf(params.get("lotteryId").toString()); + } catch (NumberFormatException e) { + return R.fail("参数错误:lotteryId 格式不正确"); + } + + // 1. 先按 userId 加锁,串行同一用户的并发点击 + String userLockKey = RedisConstants.TURNOVER_LOTTERY_DRAW_LOCK + userId; + boolean userLocked = redisLock.tryLock(userLockKey, 0L, -1L, TimeUnit.SECONDS); + if (!userLocked) { + return R.fail("抽奖处理中,请勿重复操作"); + } + try { + // 2. 再按宝箱 lotteryId 加锁,串行不同用户对同一宝箱的并发抽奖,防止奖品超发 + String boxLockKey = RedisConstants.TURNOVER_LOTTERY_DRAW_BOX_LOCK + lotteryId; + boolean boxLocked = redisLock.tryLock(boxLockKey, 0L, -1L, TimeUnit.SECONDS); + if (!boxLocked) { + return R.fail("抽奖处理中,请稍后再试"); + } + try { + return lotteryService.draw(userId, lotteryId); + } finally { + redisLock.unlock(boxLockKey); + } + } finally { + redisLock.unlock(userLockKey); + } + } + + /** + * 查询我的抽奖记录(分页) + */ + @ApiOperation("我的抽奖记录") + @PostMapping("/myRecords") + public R> myRecords(@RequestBody(required = false) Map params) { + Integer userId = getUserId().intValue(); + Long lotteryId = null; + Integer pageNum = null; + Integer pageSize = null; + if (params != null) { + Object lotteryIdVal = params.get("lotteryId"); + if (lotteryIdVal != null && StringUtils.isNotBlank(lotteryIdVal.toString())) { + lotteryId = Long.valueOf(lotteryIdVal.toString()); + } + Object pageNumVal = params.get("pageNum"); + if (pageNumVal != null && StringUtils.isNotBlank(pageNumVal.toString())) { + pageNum = Integer.valueOf(pageNumVal.toString()); + } + Object pageSizeVal = params.get("pageSize"); + if (pageSizeVal != null && StringUtils.isNotBlank(pageSizeVal.toString())) { + pageSize = Integer.valueOf(pageSizeVal.toString()); + } + } + return lotteryService.getUserRecords(userId, lotteryId, pageNum, pageSize); + } + + /** + * 查询全服中奖记录(分页,已排除白号/机器人/封禁/已删除用户) + */ + @ApiOperation("全服中奖记录") + @PostMapping("/globalRecords") + public R> globalRecords(@RequestBody(required = false) Map params) { + Long lotteryId = null; + BigDecimal minPrice = null; + Integer pageNum = null; + Integer pageSize = null; + if (params != null) { + Object lotteryIdVal = params.get("lotteryId"); + if (lotteryIdVal != null && StringUtils.isNotBlank(lotteryIdVal.toString())) { + lotteryId = Long.valueOf(lotteryIdVal.toString()); + } + Object minPriceVal = params.get("minPrice"); + if (minPriceVal != null && StringUtils.isNotBlank(minPriceVal.toString())) { + minPrice = new BigDecimal(minPriceVal.toString()); + } + Object pageNumVal = params.get("pageNum"); + if (pageNumVal != null && StringUtils.isNotBlank(pageNumVal.toString())) { + pageNum = Integer.valueOf(pageNumVal.toString()); + } + Object pageSizeVal = params.get("pageSize"); + if (pageSizeVal != null && StringUtils.isNotBlank(pageSizeVal.toString())) { + pageSize = Integer.valueOf(pageSizeVal.toString()); + } + } + return lotteryService.getGlobalRecords(lotteryId, minPrice, pageNum, pageSize); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/WebsitePropertyController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/WebsitePropertyController.java new file mode 100644 index 0000000..5dde4e7 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/WebsitePropertyController.java @@ -0,0 +1,82 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.admin.service.WebsitePropertyService; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.domain.dto.queryCondition.OrnamentCondition; +import com.ruoyi.domain.vo.WebsitePropertyDataVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@Api(tags = "管理端 道具管理") +@RestController +@RequestMapping("/admin/websiteProperty") +@Slf4j +public class WebsitePropertyController extends BaseController { + + private final WebsitePropertyService websitePropertyService; + private final TtOrnamentService ornamentsService; + + public WebsitePropertyController(WebsitePropertyService websitePropertyService, + TtOrnamentService ornamentsService) { + this.websitePropertyService = websitePropertyService; + this.ornamentsService = ornamentsService; + } + + // @ApiOperation("获取道具列表") + // @GetMapping("/list") + // public PageDataInfo list() { + // startPage(); + // List list = websitePropertyService.list(); + // return getPageData(Arrays.asList()); + // } + + @ApiOperation("获取道具列表") + @GetMapping("/list") + public TableDataInfo list(OrnamentCondition condition) { + startPage(); + List list = ornamentsService.byCondition(condition); + return getDataTable(list); + } + + @GetMapping(value = "/{id}") + public R getInfo(@PathVariable("id") Integer id) { + WebsitePropertyDataVO websitePropertyDataVO = websitePropertyService.getById(id); + websitePropertyDataVO.setImageUrl(""); + return R.ok(websitePropertyDataVO); + } + + @Log(title = "网站道具", action = "新增", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody WebsitePropertyDataVO websitePropertyDataVO) { + String msg = websitePropertyService.save(websitePropertyDataVO); + return StringUtils.isEmpty(msg) ? AjaxResult.success("新增网站道具成功!") : AjaxResult.error(msg); + } + + @Log(title = "网站道具", action = "修改", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody WebsitePropertyDataVO websitePropertyDataVO) { + String msg = websitePropertyService.updateWebsitePropertyById(websitePropertyDataVO); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } + + @Log(title = "网站道具", action = "删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + // return toAjax(ornamentsService.removeByIds(Arrays.asList(ids))); + return toAjax(websitePropertyService.deleteWebsitePropertyByIds(ids)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/WebsiteSetupController.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/WebsiteSetupController.java new file mode 100644 index 0000000..8263a16 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/WebsiteSetupController.java @@ -0,0 +1,43 @@ +package com.ruoyi.admin.controller; + +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.admin.service.WebsiteSetupService; +import com.ruoyi.domain.other.OperationalStatistics; +import com.ruoyi.domain.other.ParameterSettingBody; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/admin/websiteSetup") +public class WebsiteSetupController extends BaseController { + + private final WebsiteSetupService websiteSetupService; + + public WebsiteSetupController(WebsiteSetupService websiteSetupService) { + this.websiteSetupService = websiteSetupService; + } + + @GetMapping("getOperationalStatistics") + public R> getOperationalStatistics() { + List list = websiteSetupService.getOperationalStatistics(); + return R.ok(list); + } + + @PostMapping("/getParameterSetting") + public R getParameterSetting() { + return R.ok(websiteSetupService.getParameterSetting()); + } + + @Log(title = "网站设置", action = "保存", businessType = BusinessType.UPDATE) + @PostMapping("/updateParameterSetting") + public AjaxResult updateParameterSetting(@RequestBody ParameterSettingBody parameterSettingBody) { + String msg = websiteSetupService.updateParameterSetting(parameterSettingBody); + return StringUtils.isEmpty(msg) ? AjaxResult.success() : AjaxResult.error(msg); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/domain/body/AddRobotBody.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/domain/body/AddRobotBody.java new file mode 100644 index 0000000..453baf9 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/domain/body/AddRobotBody.java @@ -0,0 +1,14 @@ +package com.ruoyi.admin.domain.body; + +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Data +public class AddRobotBody { + + @NotNull(message = "ROLL房ID不能为空") + private Integer rollId; + + private Integer robotNum; +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/domain/body/AddUserBody.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/domain/body/AddUserBody.java new file mode 100644 index 0000000..1ec76f8 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/domain/body/AddUserBody.java @@ -0,0 +1,14 @@ +package com.ruoyi.admin.domain.body; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class AddUserBody { + + @NotNull(message = "rollId不能为空") + private Integer rollId; + + @NotNull(message = "userId不能为空") + private Integer userId; +} \ No newline at end of file diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/domain/body/BoxMockRequest.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/domain/body/BoxMockRequest.java new file mode 100644 index 0000000..5266f51 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/domain/body/BoxMockRequest.java @@ -0,0 +1,24 @@ +package com.ruoyi.admin.domain.body; + +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +//设置开箱临时爆率请求参数 +@Data +public class BoxMockRequest { + private Integer uid; + + //临时爆率列表 + private List items; + + @Data + public static class MockItem implements Serializable { + //最低价格(包含) + private BigDecimal lowPrice; + //最高价格(包含) + private BigDecimal highPrice; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/enums/BoxPoolType.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/enums/BoxPoolType.java new file mode 100644 index 0000000..e5e6c13 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/enums/BoxPoolType.java @@ -0,0 +1,36 @@ +package com.ruoyi.admin.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@AllArgsConstructor +@Getter +public enum BoxPoolType { + ANCHOR_PLAYER("01", "主播用户奖池"), + NORMAL_PLAYER("02", "普通用户奖池"), + COMPENSATION("03", "补偿奖池"), + THIRD_EXPLOSIVE("04", "三级爆率奖池"), + ROBOT_PLAYER("05", "机器人用户奖池"), + ROBOT_PLAYER_LUCKY("06", "机器人用户奖池(欧皇)"), + ROBOT_PLAYER_UNLUCKY("07", "机器人用户奖池(非酋)"), + WHITE_PLAYER("08", "白号用户奖池"); + private final String id; + private final String desc; + + public static List allTypes() { + return Arrays.stream(values()).collect(Collectors.toList()); + } + + public static BoxPoolType fromId(String id) { + for (BoxPoolType type : values()) { + if (type.getId().equals(id)) { + return type; + } + } + return null; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/event/MaintenanceEnabledEvent.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/event/MaintenanceEnabledEvent.java new file mode 100644 index 0000000..17cffa1 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/event/MaintenanceEnabledEvent.java @@ -0,0 +1,23 @@ +package com.ruoyi.admin.event; + +import org.springframework.context.ApplicationEvent; + +/** + * 维护开关开启事件。 + * 当管理后台将 websiteMaintenance 或 wheelMaintenance 设为 "1" 时发布, + * 各游戏服务监听此事件并强制结算所有活跃游戏,避免已扣款用户在维护期间丢失奖励。 + */ +public class MaintenanceEnabledEvent extends ApplicationEvent { + + /** 触发来源:"website" 或 "wheel" */ + private final String source_type; + + public MaintenanceEnabledEvent(Object source, String sourceType) { + super(source); + this.source_type = sourceType; + } + + public String getSourceType() { + return source_type; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/AdminCdkeyRedeemMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/AdminCdkeyRedeemMapper.java new file mode 100644 index 0000000..21f01a1 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/AdminCdkeyRedeemMapper.java @@ -0,0 +1,31 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.PromoCdkeyRedeem; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface AdminCdkeyRedeemMapper extends BaseMapper { + + @Select("") + List selectRedeemListWithCreator(@Param("cdkeyCode") String cdkeyCode, + @Param("redeemerId") Long redeemerId, + @Param("redeemerName") String redeemerName, + @Param("creatorId") Long creatorId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/ApiUserSubAccountMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/ApiUserSubAccountMapper.java new file mode 100644 index 0000000..2b517c7 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/ApiUserSubAccountMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtUserSubAccount; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ApiUserSubAccountMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/BoxTransferRecordAdminMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/BoxTransferRecordAdminMapper.java new file mode 100644 index 0000000..88073d7 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/BoxTransferRecordAdminMapper.java @@ -0,0 +1,12 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.BoxTransferRecord; +import org.apache.ibatis.annotations.Mapper; + +/** + * 转赠记录 Mapper(admin 端使用,用于审核通过后写入转赠记录) + */ +@Mapper +public interface BoxTransferRecordAdminMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/ConfigMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/ConfigMapper.java new file mode 100644 index 0000000..c3a0786 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/ConfigMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.ConfigData; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ConfigMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/PromoAdminMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/PromoAdminMapper.java new file mode 100644 index 0000000..d725954 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/PromoAdminMapper.java @@ -0,0 +1,20 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.sys.PromoAdmin; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface PromoAdminMapper extends BaseMapper { + + @Select("SELECT * FROM promo_admin WHERE username = #{username} AND status = '0' LIMIT 1") + PromoAdmin findByUsername(@Param("username") String username); + + /** + * 按登录账号或手机号查询(推广后台超管用手机号登录) + */ + @Select("SELECT * FROM promo_admin WHERE status = '0' AND (username = #{account} OR phone_number = #{account}) LIMIT 1") + PromoAdmin findByAccount(@Param("account") String account); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtAdvertisementMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtAdvertisementMapper.java new file mode 100644 index 0000000..2ba699d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtAdvertisementMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtAdvertisement; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtAdvertisementMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtAnnouncementMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtAnnouncementMapper.java new file mode 100644 index 0000000..73ce43b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtAnnouncementMapper.java @@ -0,0 +1,22 @@ +package com.ruoyi.admin.mapper; + +import com.ruoyi.domain.other.TtAnnouncement; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface TtAnnouncementMapper { + + List getAnnouncementList(TtAnnouncement query); + + TtAnnouncement getAnnouncementByAnnouncementId(Integer announcementId); + + int addAnnouncement(TtAnnouncement ttAnnouncement); + + int countSystemAnnouncement(); + + int editAnnouncement(TtAnnouncement ttAnnouncement); + + int removeAnnouncementByAnnouncementId(Integer announcementId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBannerMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBannerMapper.java new file mode 100644 index 0000000..83c87cc --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBannerMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtBanner; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtBannerMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBlankCardKeyMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBlankCardKeyMapper.java new file mode 100644 index 0000000..17fdabf --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBlankCardKeyMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtBlankCardKey; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtBlankCardKeyMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBonusMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBonusMapper.java new file mode 100644 index 0000000..5ea8570 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBonusMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtBonus; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtBonusMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBonusReceiveRecordMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBonusReceiveRecordMapper.java new file mode 100644 index 0000000..550d749 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBonusReceiveRecordMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtBonusReceiveRecord; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtBonusReceiveRecordMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxMapper.java new file mode 100644 index 0000000..8dd76fc --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxMapper.java @@ -0,0 +1,18 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtBoxBody; +import com.ruoyi.domain.vo.TtBoxDataVO; +import com.ruoyi.domain.vo.box.BoxGlobalData; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtBoxMapper extends BaseMapper { + List selectTtBoxList(TtBoxBody ttBoxBody); + + BoxGlobalData globalData(@Param("boxId") Integer boxId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxOpenChanceMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxOpenChanceMapper.java new file mode 100644 index 0000000..f777333 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxOpenChanceMapper.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.mapper; + +import java.util.List; +import com.ruoyi.admin.domain.TtBoxOpenChance; + +/** + * 开箱机会Mapper接口 + * + * @author ruoyi + * @date 2024-07-09 + */ +public interface TtBoxOpenChanceMapper +{ + /** + * 查询开箱机会 + * + * @param ornamentId 开箱机会主键 + * @return 开箱机会 + */ + public TtBoxOpenChance selectTtBoxOpenChanceByOrnamentId(Integer ornamentId); + + /** + * 查询开箱机会列表 + * + * @param ttBoxOpenChance 开箱机会 + * @return 开箱机会集合 + */ + public List selectTtBoxOpenChanceList(TtBoxOpenChance ttBoxOpenChance); + + /** + * 新增开箱机会 + * + * @param ttBoxOpenChance 开箱机会 + * @return 结果 + */ + public int insertTtBoxOpenChance(TtBoxOpenChance ttBoxOpenChance); + + /** + * 修改开箱机会 + * + * @param ttBoxOpenChance 开箱机会 + * @return 结果 + */ + public int updateTtBoxOpenChance(TtBoxOpenChance ttBoxOpenChance); + + /** + * 删除开箱机会 + * + * @param ornamentId 开箱机会主键 + * @return 结果 + */ + public int deleteTtBoxOpenChanceByOrnamentId(Integer ornamentId); + + /** + * 批量删除开箱机会 + * + * @param ornamentIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtBoxOpenChanceByOrnamentIds(Integer[] ornamentIds); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxOrnamentsMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxOrnamentsMapper.java new file mode 100644 index 0000000..47da0ba --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxOrnamentsMapper.java @@ -0,0 +1,53 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtBoxLevelA; +import com.ruoyi.domain.other.TtBoxOrnaments; +import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtBoxOrnamentsMapper extends BaseMapper { + + List selectBoxIdList(); + + /** 简单查询(仅 boxId,百分比使用子查询,分页可能不准) */ + List selectTtBoxOrnamentsList(@Param("boxId") Integer boxId); + + /** 精确查询(传入各类型总数,百分比计算准确) */ + List selectTtBoxOrnamentsListWithTotals(@Param("boxId") Integer boxId, + @Param("totalOdds") Integer totalOdds, + @Param("totalRealOdds") Integer totalRealOdds, + @Param("totalAnchorOdds") Integer totalAnchorOdds, + @Param("totalRobotOdds") Integer totalRobotOdds, + @Param("totalRobotOddsLucky") Integer totalRobotOddsLucky, + @Param("totalRobotOddsUnlucky") Integer totalRobotOddsUnlucky, + @Param("totalWhiteOdds") Integer totalWhiteOdds, + @Param("totalThirdExplosiveOdds") Integer totalThirdExplosiveOdds); + + List ornametBelongbox(Integer integer); + + List simpleBoxDetail(Integer boxId); + + List allOrnId(@Param("boxId") Integer boxId); + + /** + * 开箱后扣减对应奖池类型的库存数量 + * @param boxId 宝箱ID + * @param ornamentId 饰品ID + * @param poolType 奖池类型:01主播 02普通 03补偿 04三级爆率 05机器人 + */ + int decrementOdds(@Param("boxId") Integer boxId, @Param("ornamentId") Long ornamentId, @Param("poolType") String poolType); + + /** 按饰品品级汇总箱子概率 */ + List getProbabilityDistribution(@Param("boxId") Integer boxId); + + /** + * 批量查询多个箱子的饰品数据(精简版,无百分比计算) + */ + List selectTtBoxOrnamentsListByBoxIds(@Param("boxIds") List boxIds); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxRecordsMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxRecordsMapper.java new file mode 100644 index 0000000..2fed7c1 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxRecordsMapper.java @@ -0,0 +1,133 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.dto.boxRecords.TtBoxRecordsNum; +import com.ruoyi.domain.dto.boxRecords.TtBoxRecordsPoolStat; +import com.ruoyi.domain.dto.packSack.DecomposeLogCondition; +import com.ruoyi.domain.dto.roll.GetRollOpenPrizeParam; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.other.TtBoxRecordsBody; +import com.ruoyi.domain.other.TtBoxUserIdPrice; +import com.ruoyi.domain.vo.TtBoxRecordsDataVO; +import com.ruoyi.domain.vo.TtUserPackSackDataVO; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; + +@Mapper +public interface TtBoxRecordsMapper extends BaseMapper { + + List selectBoxRecordsList(TtBoxRecordsBody ttBoxRecordsBody); + + /** + * 查询福利箱子开箱记录(source=10) + */ + List selectWelfareBoxRecordsList(TtBoxRecordsBody ttBoxRecordsBody); + + /** + * 某宝箱某天的开箱记录按开箱人类型(奖池)汇总:开箱次数 + 爆出饰品总价值 + * + * @param sources 计入的流水来源(经典宝箱=常规开箱,对战宝箱=急速对战) + */ + TtBoxRecordsPoolStat selectBoxRecordsPoolStat(@Param("boxId") Integer boxId, + @Param("formattedDate") String formattedDate, + @Param("sources") List sources); + + List selectBoxRecordsByOrnamentIds(@Param("boxId") Integer boxId, @Param("ornamentIds") List ornamentIds, @Param("beginTime") String beginTime); + + List decomposeLog(DecomposeLogCondition param); + + List myOwnFights(@Param("playerId") Integer playerId); + + List byCondition(@Param("userId") Integer userId, + @Param("limit") Integer limit, + @Param("size") Integer size, + @Param("orderByFie") Integer orderByFie, + @Param("boxRecordId") Long boxRecordId, + @Param("boxId") Integer boxId, + @Param("userType") String userType, + @Param("source") List source, + @Param("status") List status, + @Param("ornamentPriceMin") BigDecimal ornamentPriceMin, + @Param("ornamentPriceMax") BigDecimal ornamentPriceMax, + @Param("ornamentLevelIds") List ornamentLevelIds + ); + + List tenTopQuery( + @Param("limit") Integer limit, + @Param("size") Integer size, + @Param("source") List source, + @Param("status") List status + ); + + List topHundredQuery( + @Param("size") Integer size, + @Param("status") List status, + @Param("source") List source, + @Param("todayOnly") boolean todayOnly + ); + + List propRankOfDay(@Param("beginT") String beginT, + @Param("endT") String endT, + @Param("sources") Integer[] sources, + @Param("number") Integer number); + + List rollOpenPrize(GetRollOpenPrizeParam param); + + Integer checkDeliveryAble(@Param("packSackIds") List packSackIds); + + Integer checkAllDeliveryAble(@Param("userId") Integer userId); + + /** + * 批量插入背包记录(疯狂转盘结算发奖用,避免逐条 insert)。 + * 调用方需自行分批,单批建议 ≤500 条。 + */ + @Insert("") + int insertBatch(@Param("list") List records); + + /** + * 汰换后的状态更新 + */ + int updateStatusByIds(@Param("list") List list, @Param("status") String status); + + List boxUserIdPrice(@Param("userIds") List userIds, @Param("startDate") String startDate, @Param("endDate") String endDate); + + /** + * 统计今日游戏饰品返奖总额(玩家选择保留饰品的 ornaments_price) + * 游戏 source:1=经典开箱, 2=对战, 3=roll房, 4=幸运升级, 11=极速糖果, 12=荒野争锋, 99=疯狂转盘 + */ + BigDecimal todayOrnamentRewardTotal(@Param("startOfDay") java.sql.Timestamp startOfDay, + @Param("endOfDay") java.sql.Timestamp endOfDay); + + /** + * 统计历史游戏饰品返奖总额:与今日总返奖(todayOrnamentRewardTotal)同口径,仅不限时间范围 + * 游戏 source:1=经典开箱, 2=对战, 3=roll房, 4=幸运升级, 11=极速糖果, 12=荒野争锋, 99=疯狂转盘;不看 status(含已分解) + */ + BigDecimal historyOrnamentRewardTotal(); + + /** + * 按source分组统计今日饰品返奖(status=0 保留在背包中的饰品价值) + * 返回 List,每个Map包含 source(Integer) 和 totalPrice(BigDecimal) + */ + List> todayOrnamentRewardBySource(@Param("startOfDay") Timestamp startOfDay, + @Param("endOfDay") Timestamp endOfDay); + + /** + * 跨天分解污染值:非今日产出但今日被分解的饰品总价值,按游戏source分组。 + * 用于从Part A奖励中扣除跨天分解产生的虚高流水。 + */ + List> crossDayDecomposeBySource(@Param("startOfDay") Timestamp startOfDay, + @Param("endOfDay") Timestamp endOfDay); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxThirdExplosiveUserMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxThirdExplosiveUserMapper.java new file mode 100644 index 0000000..d754366 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxThirdExplosiveUserMapper.java @@ -0,0 +1,10 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.box.TtBoxThirdExplosiveUser; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtBoxThirdExplosiveUserMapper extends BaseMapper { + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxTypeMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxTypeMapper.java new file mode 100644 index 0000000..8c37408 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtBoxTypeMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtBoxType; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtBoxTypeMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtCompRecordMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtCompRecordMapper.java new file mode 100644 index 0000000..35aa148 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtCompRecordMapper.java @@ -0,0 +1,23 @@ +package com.ruoyi.admin.mapper; + +import com.ruoyi.domain.other.TtCompRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface TtCompRecordMapper { + + public TtCompRecord selectTtCompRecord( + @Param("userId") Integer userId, + @Param("boxId") Integer boxId + ); + + public int insertTtCompRecord(TtCompRecord ttCompRecord); + + public int updateTtCompRecord(TtCompRecord ttCompRecord); + + public int deleteTtCompRecord( + @Param("userId") Integer userId, + @Param("boxId") Integer boxId + ); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtContentMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtContentMapper.java new file mode 100644 index 0000000..041ca44 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtContentMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtContent; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtContentMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtContentTypeMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtContentTypeMapper.java new file mode 100644 index 0000000..c16a343 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtContentTypeMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtContentType; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtContentTypeMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtDeliveryRecordMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtDeliveryRecordMapper.java new file mode 100644 index 0000000..c574621 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtDeliveryRecordMapper.java @@ -0,0 +1,34 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.dto.userRecord.DeliveryRecordsConfition; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.domain.other.TtDeliveryApplyBody; +import com.ruoyi.domain.other.TtUidTotalRechargeRecord; +import com.ruoyi.domain.vo.DeliveryApplyVO; +import com.ruoyi.domain.other.TtDeliveryRecordBody; +import com.ruoyi.domain.vo.TtDeliveryRecordDataVO; +import com.ruoyi.domain.vo.delivery.DeliveryRecordVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtDeliveryRecordMapper extends BaseMapper { + List getDeliveryApplyList(TtDeliveryApplyBody deliveryApplyBody); + + List getDeliveryRecordList(TtDeliveryRecordBody deliveryRecordBody); + + List getDeliveryRecordByUserList(TtDeliveryRecordBody deliveryRecordBody); + + List byCondition(@Param("statusList") List statusList, + @Param("uidList") List uidList, + @Param("limit") Integer limit, + @Param("size") Integer size); + + /** + * 批量查询用户CS340提货成功总额(status=10 且 thirdparty_delivery=1) + */ + List getTotalDeliveryByUserIds(@Param("userIds") List userIds); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtExponentMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtExponentMapper.java new file mode 100644 index 0000000..a6c6967 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtExponentMapper.java @@ -0,0 +1,17 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.dto.exponent.ExponentOpenBoxVo; +import com.ruoyi.domain.entity.exponent.TtExponent; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtExponentMapper extends BaseMapper { + /** + * 获取开箱记录 + */ + List exponentOpenBox(@Param("userId") Integer userId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtExponentUserBoxMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtExponentUserBoxMapper.java new file mode 100644 index 0000000..231e8df --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtExponentUserBoxMapper.java @@ -0,0 +1,10 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.exponent.TtExponentUserBox; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtExponentUserBoxMapper extends BaseMapper { + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtExponentUserMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtExponentUserMapper.java new file mode 100644 index 0000000..99430ea --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtExponentUserMapper.java @@ -0,0 +1,10 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.exponent.TtExponentUser; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtExponentUserMapper extends BaseMapper { + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightMapper.java new file mode 100644 index 0000000..f4a12d2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.fight.TtFight; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtFightMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightRankingRewardMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightRankingRewardMapper.java new file mode 100644 index 0000000..99cd146 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightRankingRewardMapper.java @@ -0,0 +1,63 @@ +package com.ruoyi.admin.mapper; + +import java.util.List; +import com.ruoyi.domain.other.TtFightRankingReward; + +/** + * 对战奖励金额Mapper接口 + * + * @author ruoyi + * @date 2024-07-04 + */ +public interface TtFightRankingRewardMapper +{ + /** + * 查询对战奖励金额 + * + * @param id 对战奖励金额主键 + * @return 对战奖励金额 + */ + public TtFightRankingReward selectTtFightRankingRewardById(Integer id); + + /** + * 查询对战奖励金额列表 + * + * @param ttFightRankingReward 对战奖励金额 + * @return 对战奖励金额集合 + */ + public List selectTtFightRankingRewardList(TtFightRankingReward ttFightRankingReward); + + /** + * 新增对战奖励金额 + * + * @param ttFightRankingReward 对战奖励金额 + * @return 结果 + */ + public int insertTtFightRankingReward(TtFightRankingReward ttFightRankingReward); + + /** + * 修改对战奖励金额 + * + * @param ttFightRankingReward 对战奖励金额 + * @return 结果 + */ + public int updateTtFightRankingReward(TtFightRankingReward ttFightRankingReward); + + /** + * 删除对战奖励金额 + * + * @param id 对战奖励金额主键 + * @return 结果 + */ + public int deleteTtFightRankingRewardById(Integer id); + + /** + * 批量删除对战奖励金额 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtFightRankingRewardByIds(Integer[] ids); + + public void truncateRankingReward(); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightResultMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightResultMapper.java new file mode 100644 index 0000000..3ab724b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightResultMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtFightResult; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtFightResultMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightUserMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightUserMapper.java new file mode 100644 index 0000000..8786422 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFightUserMapper.java @@ -0,0 +1,13 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtFightUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtFightUserMapper extends BaseMapper { + List myOwnFights(@Param("playerId") Integer playerId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFirstRechargeMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFirstRechargeMapper.java new file mode 100644 index 0000000..47d75fe --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtFirstRechargeMapper.java @@ -0,0 +1,68 @@ +package com.ruoyi.admin.mapper; + +import com.ruoyi.domain.other.TtFirstRecharge; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 首充赠送Mapper接口 + * + * @author ruoyi + * @date 2024-06-21 + */ +public interface TtFirstRechargeMapper +{ + /** + * 查询首充赠送 + * + * @param id 首充赠送主键 + * @return 首充赠送 + */ + public TtFirstRecharge selectTtFirstRechargeById(Integer id); + + /** + * 查询首充赠送列表 + * + * @param ttFirstRecharge 首充赠送 + * @return 首充赠送集合 + */ + public List selectTtFirstRechargeList(TtFirstRecharge ttFirstRecharge); + + /** + * 新增首充赠送 + * + * @param ttFirstRecharge 首充赠送 + * @return 结果 + */ + public int insertTtFirstRecharge(TtFirstRecharge ttFirstRecharge); + + /** + * 修改首充赠送 + * + * @param ttFirstRecharge 首充赠送 + * @return 结果 + */ + public int updateTtFirstRecharge(TtFirstRecharge ttFirstRecharge); + + /** + * 删除首充赠送 + * + * @param id 首充赠送主键 + * @return 结果 + */ + public int deleteTtFirstRechargeById(Integer id); + + /** + * 批量删除首充赠送 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtFirstRechargeByIds(Integer[] ids); + + /** + * 查询赠送比例 + */ + public BigDecimal selectRatioByMinAmount(BigDecimal minAmount); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtLossRebateMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtLossRebateMapper.java new file mode 100644 index 0000000..a14c6cb --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtLossRebateMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtLossRebate; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtLossRebateMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtMessageMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtMessageMapper.java new file mode 100644 index 0000000..bdd956d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtMessageMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtMessage; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtMessageMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtMessageSendMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtMessageSendMapper.java new file mode 100644 index 0000000..01f20cd --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtMessageSendMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtMessageSend; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtMessageSendMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtNoticeMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtNoticeMapper.java new file mode 100644 index 0000000..8d1e049 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtNoticeMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtNotice; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtNoticeMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOfficialRollConfigMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOfficialRollConfigMapper.java new file mode 100644 index 0000000..7fe95af --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOfficialRollConfigMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtOfficialRollConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtOfficialRollConfigMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrderMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrderMapper.java new file mode 100644 index 0000000..e3e0d62 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrderMapper.java @@ -0,0 +1,32 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.dto.userRecord.OrderCondition; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.vo.order.TtOrderVO; +import com.ruoyi.domain.vo.sys.SimpleTtUserVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.sql.Timestamp; +import java.util.List; + +@Mapper +public interface TtOrderMapper extends BaseMapper { + List byCondition(OrderCondition param); + + List batchRechargeTotal(@Param("userIdList") List userIdList, + @Param("beginTime") String beginTime, + @Param("endTime") String endTime, + @Param("orderByType") Integer orderbyType, + @Param("limit") Integer limit, + @Param("size") Integer size); + + SimpleTtUserVO rechargeTotalOfBoss( + @Param("employeeId") Integer employeeId, + @Param("beginTime") Timestamp beginTime, + @Param("endTime") String endTime, + @Param("orderType") Integer orderType, + @Param("limit") int limit, + @Param("size") Integer size); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrnamentMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrnamentMapper.java new file mode 100644 index 0000000..93cbae2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrnamentMapper.java @@ -0,0 +1,50 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.domain.dto.queryCondition.OrnamentCondition; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.TtOrnamentsYY; +import com.ruoyi.domain.vo.WebsitePropertyDataVO; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; +import io.swagger.models.auth.In; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface TtOrnamentMapper extends BaseMapper { + + @Select("SELECT * FROM tt_ornament WHERE id = #{id}") + TtOrnament selectOrnamentById(@Param("id") Long id); + + List selectOrnamentsItemIdList(); + + List list(); + + List simpleOrnamentInfo(@Param("idList") List idList); + + List byCondition(OrnamentCondition condition); + + Integer countByCondition(OrnamentCondition condition); + + List selectOrnamentsIdList(); + + List selectOrnamentsMarketHashNameList(); + + int updateWebsiteProperty(WebsitePropertyDataVO websitePropertyDataVO); + + int deleteWebsitePropertyByIds(Integer[] ids); + + //根据id列表批量查询饰品 + List selectByOrnamentIds(@Param("idList") List ornamentIdList); + + /** + * 根据倍率批量更新所有饰品的usePrice + */ + int batchUpdateUsePriceByRate(@Param("zbtParities") BigDecimal zbtParities, + @Param("usePricePremiumRate") BigDecimal usePricePremiumRate); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrnamentsLevelMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrnamentsLevelMapper.java new file mode 100644 index 0000000..3e1ea8f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrnamentsLevelMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtOrnamentsLevelMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrnamentsYYMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrnamentsYYMapper.java new file mode 100644 index 0000000..31179f9 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtOrnamentsYYMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtOrnamentsYY; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtOrnamentsYYMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromoCardKeyMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromoCardKeyMapper.java new file mode 100644 index 0000000..87c4c2b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromoCardKeyMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtPromoCardKey; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtPromoCardKeyMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromotionLevelMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromotionLevelMapper.java new file mode 100644 index 0000000..55abb32 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromotionLevelMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtPromotionLevel; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtPromotionLevelMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromotionRecordMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromotionRecordMapper.java new file mode 100644 index 0000000..7d9eb18 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromotionRecordMapper.java @@ -0,0 +1,12 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtPromotionRecord; +import com.ruoyi.domain.vo.PromotionDataVO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtPromotionRecordMapper extends BaseMapper { + + PromotionDataVO statisticsPromotionData(Integer userId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromotionUpdateMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromotionUpdateMapper.java new file mode 100644 index 0000000..d8f305a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtPromotionUpdateMapper.java @@ -0,0 +1,19 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.sys.TtPromotionUpdate; +import com.ruoyi.domain.vo.TeamDetailVO; +import org.apache.ibatis.annotations.MapKey; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; + +@Mapper +public interface TtPromotionUpdateMapper extends BaseMapper { + + // @MapKey("employee_id") + List latelyUpdate(@Param("allEmployeesId") List allEmployeesId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeCardMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeCardMapper.java new file mode 100644 index 0000000..72a34ce --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeCardMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtRechargeCard; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtRechargeCardMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeConfigMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeConfigMapper.java new file mode 100644 index 0000000..640c02a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeConfigMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtRechargeConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtRechargeConfigMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeProdMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeProdMapper.java new file mode 100644 index 0000000..6c2961f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeProdMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtRechargeProd; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtRechargeProdMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeRankingRewardMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeRankingRewardMapper.java new file mode 100644 index 0000000..70bb3d1 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeRankingRewardMapper.java @@ -0,0 +1,66 @@ +package com.ruoyi.admin.mapper; + +import com.ruoyi.domain.other.TtRechargeRankingReward; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 充值奖励金额Mapper接口 + * + * @author ruoyi + * @date 2024-07-04 + */ +@Mapper +public interface TtRechargeRankingRewardMapper +{ + /** + * 查询充值奖励金额 + * + * @param id 充值奖励金额主键 + * @return 充值奖励金额 + */ + public TtRechargeRankingReward selectTtRechargeRankingRewardById(Integer id); + + /** + * 查询充值奖励金额列表 + * + * @param ttRechargeRankingReward 充值奖励金额 + * @return 充值奖励金额集合 + */ + public List selectTtRechargeRankingRewardList(TtRechargeRankingReward ttRechargeRankingReward); + + /** + * 新增充值奖励金额 + * + * @param ttRechargeRankingReward 充值奖励金额 + * @return 结果 + */ + public int insertTtRechargeRankingReward(TtRechargeRankingReward ttRechargeRankingReward); + + /** + * 修改充值奖励金额 + * + * @param ttRechargeRankingReward 充值奖励金额 + * @return 结果 + */ + public int updateTtRechargeRankingReward(TtRechargeRankingReward ttRechargeRankingReward); + + /** + * 删除充值奖励金额 + * + * @param id 充值奖励金额主键 + * @return 结果 + */ + public int deleteTtRechargeRankingRewardById(Integer id); + + /** + * 批量删除充值奖励金额 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtRechargeRankingRewardByIds(Integer[] ids); + + public void truncateRankingReward(); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeRecordMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeRecordMapper.java new file mode 100644 index 0000000..e58e804 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRechargeRecordMapper.java @@ -0,0 +1,24 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.domain.other.TtUidTotalRechargeRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface TtRechargeRecordMapper extends BaseMapper { + + List getLastHourRechargeUserIds(BigDecimal minRecharge); + + List getLastDayRechargeUserIds(BigDecimal minRecharge); + + List getLastWeekRechargeUserIds(BigDecimal minRecharge); + + List getLastMonthRechargeUserIds(BigDecimal minRecharge); + + List getTotalPriceByUserIds(@Param("userIds") List userIds, @Param("startDate") String startDate, @Param("endDate") String endDate); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRedPackMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRedPackMapper.java new file mode 100644 index 0000000..8b917fe --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRedPackMapper.java @@ -0,0 +1,62 @@ +package com.ruoyi.admin.mapper; + +import com.ruoyi.domain.other.TtRedPack; + +import java.util.List; + +/** + * 【请填写功能名称】Mapper接口 + * + * @author ruoyi + * @date 2023-06-29 + */ +public interface TtRedPackMapper +{ + /** + * 查询【请填写功能名称】 + * + * @param id 【请填写功能名称】主键 + * @return 【请填写功能名称】 + */ + public TtRedPack selectTtRedPackById(Long id); + + /** + * 查询【请填写功能名称】列表 + * + * @param ttRedPack 【请填写功能名称】 + * @return 【请填写功能名称】集合 + */ + public List selectTtRedPackList(TtRedPack ttRedPack); + + /** + * 新增【请填写功能名称】 + * + * @param ttRedPack 【请填写功能名称】 + * @return 结果 + */ + public int insertTtRedPack(TtRedPack ttRedPack); + + /** + * 修改【请填写功能名称】 + * + * @param ttRedPack 【请填写功能名称】 + * @return 结果 + */ + public int updateTtRedPack(TtRedPack ttRedPack); + + /** + * 删除【请填写功能名称】 + * + * @param id 【请填写功能名称】主键 + * @return 结果 + */ + public int deleteTtRedPackById(Long id); + + /** + * 批量删除【请填写功能名称】 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtRedPackByIds(Long[] ids); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRedPacketMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRedPacketMapper.java new file mode 100644 index 0000000..75f92bd --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRedPacketMapper.java @@ -0,0 +1,14 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtRedPacket; +import com.ruoyi.domain.other.TtRedPacketBody; +import com.ruoyi.domain.vo.TtRedPacketDataVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface TtRedPacketMapper extends BaseMapper { + List queryList(TtRedPacketBody ttRedPacketBody); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRedPacketRecordMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRedPacketRecordMapper.java new file mode 100644 index 0000000..58cc957 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRedPacketRecordMapper.java @@ -0,0 +1,14 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtRedPacketRecord; +import com.ruoyi.domain.other.TtRedPacketRecordBody; +import com.ruoyi.domain.vo.TtRedPacketRecordDataVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface TtRedPacketRecordMapper extends BaseMapper { + List queryList(TtRedPacketRecordBody ttRedPacketRecordBody); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtReplacementRecordMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtReplacementRecordMapper.java new file mode 100644 index 0000000..37d2f12 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtReplacementRecordMapper.java @@ -0,0 +1,62 @@ +package com.ruoyi.admin.mapper; + +import com.ruoyi.domain.other.TtReplacementRecord; + +import java.util.List; + +/** + * 汰换记录Mapper接口 + * + * @author junhai + * @date 2023-09-10 + */ +public interface TtReplacementRecordMapper +{ + /** + * 查询汰换记录 + * + * @param id 汰换记录主键 + * @return 汰换记录 + */ + public TtReplacementRecord selectTtReplacementRecordById(Long id); + + /** + * 查询汰换记录列表 + * + * @param ttReplacementRecord 汰换记录 + * @return 汰换记录集合 + */ + public List selectTtReplacementRecordList(TtReplacementRecord ttReplacementRecord); + + /** + * 新增汰换记录 + * + * @param ttReplacementRecord 汰换记录 + * @return 结果 + */ + public int insertTtReplacementRecord(TtReplacementRecord ttReplacementRecord); + + /** + * 修改汰换记录 + * + * @param ttReplacementRecord 汰换记录 + * @return 结果 + */ + public int updateTtReplacementRecord(TtReplacementRecord ttReplacementRecord); + + /** + * 删除汰换记录 + * + * @param id 汰换记录主键 + * @return 结果 + */ + public int deleteTtReplacementRecordById(Long id); + + /** + * 批量删除汰换记录 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtReplacementRecordByIds(Long[] ids); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightGroupBoxMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightGroupBoxMapper.java new file mode 100644 index 0000000..0b3ac5f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightGroupBoxMapper.java @@ -0,0 +1,78 @@ +package com.ruoyi.admin.mapper; + +import java.util.List; +import com.ruoyi.domain.other.TtRobotFightGroupBox; +import org.apache.ibatis.annotations.Param; + +/** + * 机器人对战分组宝箱Mapper接口 + * + * @author ruoyi + * @date 2024-06-28 + */ +public interface TtRobotFightGroupBoxMapper +{ + /** + * 查询机器人对战分组宝箱 + * + * @param groupId 机器人对战分组宝箱主键 + * @return 机器人对战分组宝箱 + */ + public TtRobotFightGroupBox selectTtRobotFightGroupBoxByBoxId( + @Param("groupId") Integer groupId, + @Param("boxId") Integer boxId + ); + + /** + * 查询机器人对战分组宝箱列表 + * + * @param ttRobotFightGroupBox 机器人对战分组宝箱 + * @return 机器人对战分组宝箱集合 + */ + public List selectTtRobotFightGroupBoxList( + @Param("groupId") Integer groupId, + @Param("ttRobotFightGroupBox") TtRobotFightGroupBox ttRobotFightGroupBox + ); + + public int hasChildByGroupId(Integer groupId); + + /** + * 新增机器人对战分组宝箱 + * + * @param ttRobotFightGroupBox 机器人对战分组宝箱 + * @return 结果 + */ + public int insertTtRobotFightGroupBox(TtRobotFightGroupBox ttRobotFightGroupBox); + + /** + * 修改机器人对战分组宝箱 + * + * @param ttRobotFightGroupBox 机器人对战分组宝箱 + * @return 结果 + */ + public int updateTtRobotFightGroupBox(TtRobotFightGroupBox ttRobotFightGroupBox); + + /** + * 删除机器人对战分组宝箱 + */ + public int deleteTtRobotFightGroupBoxByGroupId( + @Param("groupId") Integer groupId, + @Param("boxId") Integer boxId + ); + + /** + * 批量删除机器人对战分组宝箱 + */ + public int deleteTtRobotFightGroupBoxByGroupIds( + @Param("groupId") Integer groupId, + @Param("boxIds") Integer[] boxIds + ); + + /** + * 删除指定分组下的所有宝箱 + * + * @param groupId 分组ID + * @return 结果 + */ + public int deleteTtRobotFightGroupBoxByGroupIdAll(Integer groupId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightGroupMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightGroupMapper.java new file mode 100644 index 0000000..11c14c9 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightGroupMapper.java @@ -0,0 +1,69 @@ +package com.ruoyi.admin.mapper; + +import java.util.List; +import com.ruoyi.domain.other.TtRobotFightGroup; + +/** + * 机器人对战分组Mapper接口 + * + * @author ruoyi + * @date 2024-06-28 + */ +public interface TtRobotFightGroupMapper +{ + /** + * 查询机器人对战分组 + * + * @param groupId 机器人对战分组主键 + * @return 机器人对战分组 + */ + public TtRobotFightGroup selectTtRobotFightGroupByGroupId(Integer groupId); + + /** + * 查询机器人对战分组列表 + * + * @param ttRobotFightGroup 机器人对战分组 + * @return 机器人对战分组集合 + */ + public List selectTtRobotFightGroupList(TtRobotFightGroup ttRobotFightGroup); + + /** + * 新增机器人对战分组 + * + * @param ttRobotFightGroup 机器人对战分组 + * @return 结果 + */ + public int insertTtRobotFightGroup(TtRobotFightGroup ttRobotFightGroup); + + /** + * 修改机器人对战分组 + * + * @param ttRobotFightGroup 机器人对战分组 + * @return 结果 + */ + public int updateTtRobotFightGroup(TtRobotFightGroup ttRobotFightGroup); + + /** + * 删除机器人对战分组 + * + * @param groupId 机器人对战分组主键 + * @return 结果 + */ + public int deleteTtRobotFightGroupByGroupId(Integer groupId); + + /** + * 批量删除机器人对战分组 + * + * @param groupIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtRobotFightGroupByGroupIds(Integer[] groupIds); + + /** + * 根据价格档位配置ID统计关联的分组数量 + * + * @param priceConfigId 价格档位配置ID + * @return 分组数量 + */ + public int selectCountByPriceConfigId(Integer priceConfigId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightJoinConfigMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightJoinConfigMapper.java new file mode 100644 index 0000000..a316402 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightJoinConfigMapper.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.mapper; + +import java.util.List; +import com.ruoyi.domain.other.TtRobotFightJoinConfig; + +/** + * 机器人对战进房时间配置Mapper接口 + * + * @author ruoyi + * @date 2026-07-23 + */ +public interface TtRobotFightJoinConfigMapper +{ + /** + * 查询机器人对战进房时间配置 + * + * @param id 主键 + * @return 机器人对战进房时间配置 + */ + public TtRobotFightJoinConfig selectTtRobotFightJoinConfigById(Integer id); + + /** + * 查询机器人对战进房时间配置列表 + * + * @param ttRobotFightJoinConfig 机器人对战进房时间配置 + * @return 机器人对战进房时间配置集合 + */ + public List selectTtRobotFightJoinConfigList(TtRobotFightJoinConfig ttRobotFightJoinConfig); + + /** + * 新增机器人对战进房时间配置 + * + * @param ttRobotFightJoinConfig 机器人对战进房时间配置 + * @return 结果 + */ + public int insertTtRobotFightJoinConfig(TtRobotFightJoinConfig ttRobotFightJoinConfig); + + /** + * 修改机器人对战进房时间配置 + * + * @param ttRobotFightJoinConfig 机器人对战进房时间配置 + * @return 结果 + */ + public int updateTtRobotFightJoinConfig(TtRobotFightJoinConfig ttRobotFightJoinConfig); + + /** + * 删除机器人对战进房时间配置 + * + * @param id 主键 + * @return 结果 + */ + public int deleteTtRobotFightJoinConfigById(Integer id); + + /** + * 批量删除机器人对战进房时间配置 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtRobotFightJoinConfigByIds(Integer[] ids); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightPriceConfigMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightPriceConfigMapper.java new file mode 100644 index 0000000..4261638 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRobotFightPriceConfigMapper.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.mapper; + +import java.util.List; +import com.ruoyi.domain.other.TtRobotFightPriceConfig; + +/** + * 机器人对战价格档位配置Mapper接口 + * + * @author ruoyi + * @date 2026-08-13 + */ +public interface TtRobotFightPriceConfigMapper +{ + /** + * 查询机器人对战价格档位配置 + * + * @param id 主键 + * @return 机器人对战价格档位配置 + */ + public TtRobotFightPriceConfig selectTtRobotFightPriceConfigById(Integer id); + + /** + * 查询机器人对战价格档位配置列表 + * + * @param ttRobotFightPriceConfig 机器人对战价格档位配置 + * @return 机器人对战价格档位配置集合 + */ + public List selectTtRobotFightPriceConfigList(TtRobotFightPriceConfig ttRobotFightPriceConfig); + + /** + * 新增机器人对战价格档位配置 + * + * @param ttRobotFightPriceConfig 机器人对战价格档位配置 + * @return 结果 + */ + public int insertTtRobotFightPriceConfig(TtRobotFightPriceConfig ttRobotFightPriceConfig); + + /** + * 修改机器人对战价格档位配置 + * + * @param ttRobotFightPriceConfig 机器人对战价格档位配置 + * @return 结果 + */ + public int updateTtRobotFightPriceConfig(TtRobotFightPriceConfig ttRobotFightPriceConfig); + + /** + * 删除机器人对战价格档位配置 + * + * @param id 主键 + * @return 结果 + */ + public int deleteTtRobotFightPriceConfigById(Integer id); + + /** + * 批量删除机器人对战价格档位配置 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtRobotFightPriceConfigByIds(Integer[] ids); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollJackpotMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollJackpotMapper.java new file mode 100644 index 0000000..1fc0764 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollJackpotMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtRollJackpot; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtRollJackpotMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollJackpotOrnamentsMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollJackpotOrnamentsMapper.java new file mode 100644 index 0000000..31c5782 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollJackpotOrnamentsMapper.java @@ -0,0 +1,30 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.dto.roll.GetRollPrizePool; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnaments; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnamentsBody; +import com.ruoyi.domain.vo.TtRollJackpotOrnamentsDataVO; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsVO; +import com.ruoyi.domain.vo.roll.SimpleRollOrnamentVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtRollJackpotOrnamentsMapper extends BaseMapper { + List queryList(TtRollJackpotOrnamentsBody rollJackpotOrnamentsBody); + + List rollShow(@Param("jackpotId") Integer jackpotId); + + List listByRollId(GetRollPrizePool param); + + Integer ornamentsNumberOfRoll(@Param("jackpotId") Integer jackpotId); + + List byOrnamentIds(@Param("rollId") Integer rollId, + @Param("noHOrnamentIds") List noHOrnamentIds); + + Integer totalByCondition(GetRollPrizePool param); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollMapper.java new file mode 100644 index 0000000..2e531d6 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollMapper.java @@ -0,0 +1,21 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtRoll; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.TtRollPrizeDataVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface TtRollMapper extends BaseMapper { + + List getRollJackpotOrnamentsList(Integer rollId); + + List getSpecifiedRollJackpotOrnamentsList(Integer rollId); + + List getRollUser(Integer rollId); + + List getRollRobotIds(Integer rollId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollUserMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollUserMapper.java new file mode 100644 index 0000000..3afdae5 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollUserMapper.java @@ -0,0 +1,22 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.roll.GetRollPlayersParam; +import com.ruoyi.domain.entity.roll.TtRollUser; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.domain.vo.roll.RollUserPrizeVO; +import com.ruoyi.domain.vo.roll.RollUserVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtRollUserMapper extends BaseMapper { + R> byRollId(Integer rollId); + + List pageByRollId(GetRollPlayersParam param); + + List rollWinners(@Param("rollId") Integer rollId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollUserPrizeMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollUserPrizeMapper.java new file mode 100644 index 0000000..81c2e8f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtRollUserPrizeMapper.java @@ -0,0 +1,19 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtRollUser; +import com.ruoyi.domain.entity.roll.TtRollUserPrize; +import com.ruoyi.domain.vo.roll.RollUserPrizeVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtRollUserPrizeMapper extends BaseMapper { + List byRollUserIds(@Param("rollUserIds") List rollUserIds); + + Integer ownOrnamentNumber(@Param("rollJackpotOrnamentId") Integer rollJackpotOrnamentId,@Param("rollId") Integer rollId); + + List byRollId(@Param("rollId") Integer rollId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTaskCenterMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTaskCenterMapper.java new file mode 100644 index 0000000..29cda1e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTaskCenterMapper.java @@ -0,0 +1,65 @@ +package com.ruoyi.admin.mapper; + +import java.util.List; +import com.ruoyi.domain.other.TtTaskCenter; +import org.apache.ibatis.annotations.Mapper; + +/** + * 任务中心Mapper接口 + * + * @author ruoyi + * @date 2024-05-25 + */ +@Mapper +public interface TtTaskCenterMapper +{ + /** + * 查询任务中心 + * + * @param taskId 任务中心主键 + * @return 任务中心 + */ + public TtTaskCenter selectTtTaskCenterByTaskId(Integer taskId); + + public TtTaskCenter selectTtTaskCenterByIdentify(String identify); + + /** + * 查询任务中心列表 + * + * @param ttTaskCenter 任务中心 + * @return 任务中心集合 + */ + public List selectTtTaskCenterList(TtTaskCenter ttTaskCenter); + + /** + * 新增任务中心 + * + * @param ttTaskCenter 任务中心 + * @return 结果 + */ + public int insertTtTaskCenter(TtTaskCenter ttTaskCenter); + + /** + * 修改任务中心 + * + * @param ttTaskCenter 任务中心 + * @return 结果 + */ + public int updateTtTaskCenter(TtTaskCenter ttTaskCenter); + + /** + * 删除任务中心 + * + * @param taskId 任务中心主键 + * @return 结果 + */ + public int deleteTtTaskCenterByTaskId(Integer taskId); + + /** + * 批量删除任务中心 + * + * @param taskIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtTaskCenterByTaskIds(Integer[] taskIds); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTimeRollMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTimeRollMapper.java new file mode 100644 index 0000000..2c060f0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTimeRollMapper.java @@ -0,0 +1,14 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtTimeRoll; +import com.ruoyi.domain.vo.TtRollPrizeDataVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface TtTimeRollMapper extends BaseMapper { + List getRollJackpotOrnamentsList(Integer id); + List getSpecifiedTimeRollJackpotOrnamentsList(Integer id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTimeRollUserMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTimeRollUserMapper.java new file mode 100644 index 0000000..d1a0eb4 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTimeRollUserMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.roll.TtTimeRollUser; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtTimeRollUserMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTransferApplyMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTransferApplyMapper.java new file mode 100644 index 0000000..eb562db --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtTransferApplyMapper.java @@ -0,0 +1,41 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.transfer.TtTransferApply; +import com.ruoyi.domain.other.TransferApplyBody; +import com.ruoyi.domain.vo.transfer.TransferApplyBatchVO; +import com.ruoyi.domain.vo.transfer.TransferApplyVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtTransferApplyMapper extends BaseMapper { + + /** + * 获取转赠申请列表(含转赠人昵称、受赠人昵称、饰品信息、转赠次数) + */ + List getTransferApplyList(TransferApplyBody body); + + /** + * 获取转赠申请批次列表(同一批次聚合为一个对象,内部包含饰品明细) + */ + List getTransferApplyBatchList(TransferApplyBody body); + + /** + * 分页查询:获取满足条件的批次号列表(去重) + */ + List getTransferApplyBatchIds(TransferApplyBody body); + + /** + * 根据批次号列表查询完整批次数据(含饰品明细) + */ + List getTransferApplyBatchListByBatchIds(@Param("batchIds") List batchIds, + @Param("body") TransferApplyBody body); + + /** + * 统计某个转赠人的总转赠申请次数 + */ + Integer countByFromUserId(@Param("fromUserId") Integer fromUserId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUpgradeFailOrnamentsMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUpgradeFailOrnamentsMapper.java new file mode 100644 index 0000000..58a2c20 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUpgradeFailOrnamentsMapper.java @@ -0,0 +1,27 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.admin.controller.TtUpgradeFailOrnamentsController; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.other.TtUpgradeFailOrnaments; +import com.ruoyi.domain.vo.TtUpgradeFailOrnamentsDataVO; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface TtUpgradeFailOrnamentsMapper extends BaseMapper { + List queryList(TtUpgradeFailOrnamentsController.listParam param); + + List getFailOrnamentsList(@Param("id") Integer id, @Param("price") BigDecimal price); + + List ornamentInfoByUpgradeId(@Param("upgradeOrnId") Integer upgradeOrnId); + + @Update("TRUNCATE TABLE tt_upgrade_fail_ornaments") + void truncateTable(); + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUpgradeOrnamentsMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUpgradeOrnamentsMapper.java new file mode 100644 index 0000000..e8f65f0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUpgradeOrnamentsMapper.java @@ -0,0 +1,18 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtUpgradeOrnaments; +import com.ruoyi.domain.other.TtUpgradeOrnamentsBody; +import com.ruoyi.domain.vo.TtUpgradeOrnamentsDataVO; +import org.apache.ibatis.annotations.Mapper; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@Mapper +public interface TtUpgradeOrnamentsMapper extends BaseMapper { + List queryList(TtUpgradeOrnamentsBody ttUpgradeOrnamentsBody); + + Map getUpgradeProfitStatistics(Integer id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUpgradeRecordMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUpgradeRecordMapper.java new file mode 100644 index 0000000..30b3ef1 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUpgradeRecordMapper.java @@ -0,0 +1,17 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.dto.upgrade.UpgradeCondition; +import com.ruoyi.domain.other.TtUpgradeRecord; +import com.ruoyi.domain.other.TtUpgradeRecordBody; +import com.ruoyi.domain.vo.upgrade.UpgradeRecordVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface TtUpgradeRecordMapper extends BaseMapper { + List getUpgradeRecord(TtUpgradeRecordBody ttUpgradeRecordBody); + + List adminGetLog(UpgradeCondition param); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserAmountRecordsMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserAmountRecordsMapper.java new file mode 100644 index 0000000..553c155 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserAmountRecordsMapper.java @@ -0,0 +1,52 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.vo.TtUserAccountRecordsRankVO; +import com.ruoyi.domain.entity.recorde.TtUserAmountRecords; +import com.ruoyi.domain.other.TtUserAmountRecordsBody; +import com.ruoyi.domain.vo.UserBERankVO; +import com.ruoyi.domain.vo.task.SimpleAmountRecordVO; +import com.ruoyi.domain.vo.task.SimpleBlendErcashRecordVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface TtUserAmountRecordsMapper extends BaseMapper { + + List queryList(TtUserAmountRecordsBody ttUserAmountRecordsBody); + + List rank(@Param("beginT") String beginT, + @Param("endT") String endT, + @Param("page") Integer page, + @Param("size") Integer size); + + BigDecimal pWHistoryTotal(Integer uid); + + List blendErcashRank(@Param("sourceList") List sourceList, + @Param("beginT") String beginT, + @Param("endT") String endT, + @Param("limit") Integer limit, + @Param("size") Integer size); + + List recordsByTimeAndHasBoss(@Param("beginT") String beginT, + @Param("endT") String endT); + + Integer totalSize(TtUserAmountRecordsBody param); + + List BlendErcashByTimeAndHasBoss(@Param("beginT") String beginT, + @Param("endT") String endT); + + BigDecimal pWelfareByTime(@Param("bossId") Integer bossId, + @Param("beginT") String beginT, + @Param("endT") String endT); + + List bossByHasConsumeEmployee(@Param("beginT") String beginT,@Param("endT") String endT); + + /** + * 获取用户的总消费 + */ + BigDecimal getTotalConsumptionByUserId(Integer userId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserAvatarMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserAvatarMapper.java new file mode 100644 index 0000000..31eabc9 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserAvatarMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtUserAvatar; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtUserAvatarMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserBlendErcashMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserBlendErcashMapper.java new file mode 100644 index 0000000..56016ef --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserBlendErcashMapper.java @@ -0,0 +1,208 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.dto.userRecord.AmountRecordsDetailCondition; +import com.ruoyi.domain.dto.userRecord.BlendErcashCondition; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.vo.ChildDetailByBossVO; +import com.ruoyi.domain.vo.TtUserAmountRecords.PersonBlendErcashVO; +import com.ruoyi.domain.vo.TtUserAmountRecords.TtUserBlendErcashVO; +import com.ruoyi.domain.vo.TtUserAmountRecords.UserAmountDetailVO; +import com.ruoyi.domain.vo.UserBERankVO; +import com.ruoyi.domain.vo.sys.SimpleTtUserVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; + +@Mapper +public interface TtUserBlendErcashMapper extends BaseMapper { + + /** 用户流水统计列表(快速模式:先分页用户,再只聚合当页用户数据,排序仅限当页) */ + List> userTurnoverList( + @Param("userId") Integer userId, + @Param("nickName") String nickName, + @Param("phone") String phone, + @Param("startTime") String startTime, + @Param("endTime") String endTime, + @Param("includeArchive") boolean includeArchive, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** 用户流水统计列表(全局排序模式:先聚合所有匹配用户 → 排序 → 分页) */ + List> userTurnoverListSorted( + @Param("userId") Integer userId, + @Param("nickName") String nickName, + @Param("phone") String phone, + @Param("startTime") String startTime, + @Param("endTime") String endTime, + @Param("includeArchive") boolean includeArchive, + @Param("sortField") String sortField, + @Param("sortOrder") String sortOrder, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** 用户流水统计总数 */ + int userTurnoverCount( + @Param("userId") Integer userId, + @Param("nickName") String nickName, + @Param("phone") String phone); + + List rank(@Param("sourceList") List sourceList, + @Param("beginT") String beginT, + @Param("endT") String endT, + @Param("limit") Integer limit, + @Param("size") Integer size); + + List batchConsumeTotal(@Param("userIdList") List userIdList, + @Param("beginTime") String beginTime, + @Param("endTime") String endTime, + @Param("orderByFie") Integer orderByFie, + @Param("orderByType") Integer orderByType, + @Param("limit") Integer limit, + @Param("size") Integer size); + + /** + * 统计用户在一个时间段内的消费 + */ + PersonBlendErcashVO personTotalConsumeByTime( + @Param("userId") Integer userId, + @Param("beginT") String beginT, + @Param("endT") String endT, + @Param("type") Integer type + ); + + /** + * 批量统计用户在一个时间段内的消费 + */ + List personsTotalConsumeByTime( + @Param("userIds") List userIds, + @Param("beginT") String beginT, + @Param("endT") String endT, + @Param("type") Integer type + + ); + + List userAccountDetail(AmountRecordsDetailCondition param); + + List byCondition( + @Param("userId") Integer userId, + @Param("source") Integer source, + @Param("type") Integer type, + @Param("userName") String userName, + @Param("moneyType") Integer moneyType, + @Param("phoneNumber") String phoneNumber, + @Param("limit") Integer limit, + @Param("size") Integer size); + + Integer count( + @Param("userId") Integer userId, + @Param("source") Integer source, + @Param("type") Integer type, + @Param("userName") String userName, + @Param("moneyType") Integer moneyType, + @Param("phoneNumber") String phoneNumber, + @Param("limit") Integer limit, + @Param("size") Integer size + ); + + /** 只查归档表 tt_user_blend_ercash_archive 的收支明细分页查询(镜像 byCondition) */ + List byConditionArchive( + @Param("userId") Integer userId, + @Param("source") Integer source, + @Param("type") Integer type, + @Param("userName") String userName, + @Param("moneyType") Integer moneyType, + @Param("phoneNumber") String phoneNumber, + @Param("limit") Integer limit, + @Param("size") Integer size); + + /** 只查归档表 tt_user_blend_ercash_archive 的收支明细总数(镜像 count) */ + Integer countArchive( + @Param("userId") Integer userId, + @Param("source") Integer source, + @Param("type") Integer type, + @Param("userName") String userName, + @Param("moneyType") Integer moneyType, + @Param("phoneNumber") String phoneNumber, + @Param("limit") Integer limit, + @Param("size") Integer size + ); + + List collectByUIds(@Param("cIds") List cIds, + @Param("beginTime") String beginTime, + @Param("endTime") String endTime, + @Param("type") Integer type, + @Param("sourceList") List sourceList + ); + + /** + * 批量统计指定用户、指定来源、指定类型的金额(主表 + 归档表 UNION ALL) + */ + BigDecimal sumAmountByUserIdsAndSource( + @Param("userIds") List userIds, + @Param("source") Integer source, + @Param("type") Integer type); + + /** + * 批量统计指定用户、指定来源集合、指定类型的金额(主表 + 归档表 UNION ALL) + */ + BigDecimal sumAmountByUserIdsAndSources( + @Param("userIds") List userIds, + @Param("sourceList") List sourceList, + @Param("type") Integer type); + + /** + * 按来源统计指定时间范围内的金额(主表 + 归档表 UNION ALL)。 + * start/end 为空时表示不限时间,统一排除无效用户。 + */ + BigDecimal sumAmountBySourceAndTimeWithArchive( + @Param("source") Integer source, + @Param("start") Timestamp start, + @Param("end") Timestamp end); + + /** + * 首页折线图:按 source 分组,统计指定时间段内各来源金额之和。 + * 一次查询覆盖多个 source,替代逐 source 的 selectList 全字段求和。 + * 每行返回:source、amt(SUM(amount) 原值)、absAmt(SUM(ABS(amount)) 绝对值)。 + */ + List> sumBySourcesAndTime( + @Param("sources") List sources, + @Param("start") Timestamp start, + @Param("end") Timestamp end); + + /** + * 首页折线图(按天/周):一次查询覆盖整个时间范围,按 source + 日期(DATE(create_time)) 分组聚合。 + * 每行返回:source、bucket(yyyy-MM-dd)、amt(SUM(amount))、absAmt(SUM(ABS(amount)))。 + * 替代按天循环的多次查询,配合 (source, create_time) 复合索引单次范围扫描完成。 + */ + List> sumBySourcesGroupByDate( + @Param("sources") List sources, + @Param("start") Timestamp start, + @Param("end") Timestamp end); + + /** + * 首页折线图(按小时):一次查询覆盖当日,按 source + 小时(HOUR(create_time)) 分组聚合。 + * 每行返回:source、bucket(0-23)、amt、absAmt。替代按小时循环的 24 次查询。 + */ + List> sumBySourcesGroupByHour( + @Param("sources") List sources, + @Param("start") Timestamp start, + @Param("end") Timestamp end); + + /** + * 亏损返水结算:按用户汇总指定时间范围内的游戏投入与游戏产出(主表 + 归档表 UNION ALL)。 + *

+ * 必须带归档表:流水只保留 {@code flowArchiveRetainDays} 天(默认 7 天), + * 半月区间直接查主表会漏掉前半段数据。每行返回:userId、betSum、winSum。 + */ + List> sumGameBetWinByUser( + @Param("betSources") List betSources, + @Param("winSources") List winSources, + @Param("allSources") List allSources, + @Param("start") Timestamp start, + @Param("end") Timestamp end); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserCreditsRecordsMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserCreditsRecordsMapper.java new file mode 100644 index 0000000..fa5b36f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserCreditsRecordsMapper.java @@ -0,0 +1,23 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords; +import com.ruoyi.domain.other.TtUserCreditsRecordsBody; +import com.ruoyi.domain.vo.TtUserCreditsRecordsRankVO; +import com.ruoyi.domain.vo.task.SimpleCreditsRecordVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtUserCreditsRecordsMapper extends BaseMapper { + List queryList(TtUserCreditsRecordsBody ttUserCreditsRecordsBody); + + List rank(@Param("page") Integer page, + @Param("size") Integer size, + @Param("begin") String begin, + @Param("end") String end); + + List recordsByTimeAndHasBoss(@Param("beginT") String beginT,@Param("endT") String endT); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserMapper.java new file mode 100644 index 0000000..26ea41c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserMapper.java @@ -0,0 +1,153 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtUserPackSackBody; +import com.ruoyi.domain.param.promotion.AnchorDailyTurnoverParam; +import com.ruoyi.domain.vo.TtUserPackSackDataVO; +import com.ruoyi.domain.vo.promotion.AnchorDailyTurnoverVo; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@Mapper +public interface TtUserMapper extends BaseMapper { + List getPackSack(TtUserPackSackBody ttUserPackSackBody); + + //@MapKey("userId") + Map getUserProfitStatistics(Integer userId); + + @Delete("DELETE FROM tt_user WHERE user_id = #{userId}") + void removeByUserId(@Param("userId") Integer userId); + + /** + * 真删除用户时,清理副号绑定关系(双向:无论是主号还是副号) + */ + @Delete("DELETE FROM tt_user_sub_account WHERE main_uid = #{userId} OR sub_uid = #{userId}") + void removeUserSubAccountByUserId(@Param("userId") Integer userId); + + /** + * 将指定用户的所有直属下级的 parent_id 置为 null(级联解绑第一步) + */ + @Update("UPDATE tt_user SET parent_id = NULL WHERE parent_id = #{parentId}") + int clearChildrenParentId(@Param("parentId") Integer parentId); + + public TtUser selectTtUserById(Long id); + + /** + * 事务内加行锁读取用户(SELECT ... FOR UPDATE)。 + * 用途:扣款前在同一事务里锁定该行后再计算"被连带压掉的积分",避免与充值/积分兑金币并发时 + * 记错 credits_cut(退款会按记录值返还,记错就等于凭空多退/少退积分)。 + */ + @org.apache.ibatis.annotations.Select("select * from tt_user where user_id = #{userId} for update") + TtUser selectByUserIdForUpdate(@Param("userId") Long userId); + + int updateTtUserCoin(@Param("userId") Long userId, @Param("money") BigDecimal money); + + @Update("update tt_user set account_amount = account_amount + #{money}, account_credits = account_credits + #{money} where user_id = #{userId}") + int addAccountAmount(@Param("userId") Long userId, @Param("money") BigDecimal money); + + @Update("update tt_user set account_amount = account_amount + #{money} where user_id = #{userId}") + int addOnlyAccountAmount(@Param("userId") Long userId, @Param("money") BigDecimal money); + + /** + * 扣减金币,并将积分收敛到不超过扣减后的金币余额。 + * 注意:MySQL 的 SET 子句从左到右求值,后面的表达式引用的是前面已更新的新值。 + * 因此积分赋值必须放在金币赋值之前,使其引用原始 account_amount, + * 否则会变成 LEAST(积分, 原金币 - 2×扣款额),导致积分被扣成负数。 + * 外层再用 GREATEST(..., 0) 兜底,保证积分永不为负。 + */ + @Update("update tt_user " + + "set account_credits = GREATEST(LEAST(account_credits, account_amount - #{money}), 0), " + + "account_amount = account_amount - #{money} " + + "where user_id = #{userId} " + + " and account_amount >= #{money}") + int subtractAccountAmount(@Param("userId") Long userId, @Param("money") BigDecimal money); + + /** + * 仅扣减金币余额,不动积分(避开 LEAST 副作用,专供仅扣金币场景使用)。 + */ + @Update("update tt_user " + + "set account_amount = account_amount - #{money} " + + "where user_id = #{userId} " + + " and account_amount >= #{money}") + int subtractOnlyAccountAmount(@Param("userId") Long userId, @Param("money") BigDecimal money); + + @Update("update tt_user " + + "set account_credits = account_credits - #{money}, " + + "account_amount = account_amount - #{money} " + + "where user_id = #{userId} " + + " and account_credits >= #{money} " + + " and account_amount >= #{money}") + int subtractCreditsAmount(@Param("userId") Long userId, @Param("money") BigDecimal money); + + /** + * 仅扣减积分,不动金币。 + * 商城等“纯积分消费”场景使用,避免同时扣减 account_amount。 + */ + @Update("update tt_user " + + "set account_credits = account_credits - #{money} " + + "where user_id = #{userId} " + + " and account_credits >= #{money}") + int subtractOnlyAccountCredits(@Param("userId") Long userId, @Param("money") BigDecimal money); + + @Update("update tt_user set account_credits = account_credits + #{money} where user_id = #{userId}") + int addOnlyAccountCredits(@Param("userId") Long userId, @Param("money") BigDecimal money); + + /** + * 积分转金币:扣减积分,等额增加金币(原子操作,带积分守卫) + */ + @Update("update tt_user set account_credits = account_credits - #{credits}, account_amount = account_amount + #{credits} where user_id = #{userId} and account_credits >= #{credits}") + int creditsToAmount(@Param("userId") Long userId, @Param("credits") BigDecimal credits); + + /** + * 卡商余额充值(增加 merchant_balance) + */ + @Update("update tt_user set merchant_balance = merchant_balance + #{money} where user_id = #{userId}") + int addMerchantBalance(@Param("userId") Long userId, @Param("money") BigDecimal money); + + /** + * 卡商余额扣减(生成卡密时扣减 merchant_balance) + */ + @Update("update tt_user " + + "set merchant_balance = merchant_balance - #{money} " + + "where user_id = #{userId} " + + " and merchant_balance >= #{money}") + int subtractMerchantBalance(@Param("userId") Long userId, @Param("money") BigDecimal money); + + List propRankUsers(@Param("sources") Integer[] sources, + @Param("begin") String begin, + @Param("end") String end); + + List maxPricePropByUserIds(@Param("sources") Integer[] sources, + @Param("beginT") String beginT, + @Param("endT") String endT, + @Param("userId") List userIds, + @Param("size") Integer size); + + List allEmployeesByParents(@Param("bossIds") List bossIds); + + List teamUIds(Integer bossId); + + /** + * 查询名下主播每日流水 + */ + List getAnchorDailyTurnover(AnchorDailyTurnoverParam param); + + /** + * 原子递增累计充值金额(充值时调用,保证缓存字段实时更新) + */ + @Update("UPDATE tt_user SET total_recharge_amount = COALESCE(total_recharge_amount, 0) + #{amount} WHERE user_id = #{userId}") + int incrementTotalRecharge(@Param("userId") Integer userId, @Param("amount") BigDecimal amount); + + /** + * 直接设置累计充值金额(updateUserVIPLevel重算时使用) + */ + @Update("UPDATE tt_user SET total_recharge_amount = #{amount} WHERE user_id = #{userId}") + int setTotalRechargeAmount(@Param("userId") Integer userId, @Param("amount") BigDecimal amount); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserSourceReportMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserSourceReportMapper.java new file mode 100644 index 0000000..6a12910 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserSourceReportMapper.java @@ -0,0 +1,45 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtUserSourceReport; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface TtUserSourceReportMapper extends BaseMapper { + + /** + * 通过日报表差值统计用户在 [startDate, endDate] 的各 source 累计消费(快速模式:先分页用户再聚合)。 + * 公式:report(endDate).total - report(startDate-1).total + */ + List> userTurnoverByReport( + @Param("userId") Integer userId, + @Param("nickName") String nickName, + @Param("phone") String phone, + @Param("endDate") String endDate, + @Param("startDateMinus1") String startDateMinus1, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** + * 通过日报表差值统计用户在 [startDate, endDate] 的各 source 累计消费(全局排序模式:先聚合再排序分页)。 + */ + List> userTurnoverByReportSorted( + @Param("userId") Integer userId, + @Param("nickName") String nickName, + @Param("phone") String phone, + @Param("endDate") String endDate, + @Param("startDateMinus1") String startDateMinus1, + @Param("sortField") String sortField, + @Param("sortOrder") String sortOrder, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** + * 查询最新可用的报表日期(MAX(report_date)) + */ + String selectMaxReportDate(); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserTransferDailyMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserTransferDailyMapper.java new file mode 100644 index 0000000..d136b99 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtUserTransferDailyMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.transfer.TtUserTransferDaily; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtUserTransferDailyMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtVipLevelMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtVipLevelMapper.java new file mode 100644 index 0000000..8881ca7 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtVipLevelMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtVipLevel; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtVipLevelMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtWelfareMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtWelfareMapper.java new file mode 100644 index 0000000..518e030 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtWelfareMapper.java @@ -0,0 +1,68 @@ +package com.ruoyi.admin.mapper; + +import java.util.List; +import com.ruoyi.domain.other.TtWelfare; +import org.apache.ibatis.annotations.Mapper; + +/** + * 福利列表Mapper接口 + * + * @author ruoyi + * @date 2024-05-11 + */ +@Mapper +public interface TtWelfareMapper +{ + /** + * 查询福利列表 + * + * @param welfareId 福利列表主键 + * @return 福利列表 + */ + public TtWelfare selectTtWelfareByWelfareId(Integer welfareId); + + /** + * 查询福利列表列表 + * + * @param ttWelfare 福利列表 + * @return 福利列表集合 + */ + public List selectTtWelfareList(TtWelfare ttWelfare); + + /** + * 新增福利列表 + * + * @param ttWelfare 福利列表 + * @return 结果 + */ + public int insertTtWelfare(TtWelfare ttWelfare); + + /** + * 修改福利列表 + * + * @param ttWelfare 福利列表 + * @return 结果 + */ + public int updateTtWelfare(TtWelfare ttWelfare); + + /** + * 删除福利列表 + * + * @param welfareId 福利列表主键 + * @return 结果 + */ + public int deleteTtWelfareByWelfareId(Integer welfareId); + + /** + * 批量删除福利列表 + * + * @param welfareIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtWelfareByWelfareIds(Integer[] welfareIds); + + /** + * 根据VIP等级查询福利配置 + */ + public TtWelfare selectTtWelfareByVipLevel(Integer vipLevel); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtWelfareMonthlyRechargesMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtWelfareMonthlyRechargesMapper.java new file mode 100644 index 0000000..c54516c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtWelfareMonthlyRechargesMapper.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.mapper; + +import java.util.List; +import com.ruoyi.domain.other.TtWelfareMonthlyRecharges; + +/** + * 每月充值福利Mapper接口 + * + * @author ruoyi + * @date 2024-06-24 + */ +public interface TtWelfareMonthlyRechargesMapper +{ + /** + * 查询每月充值福利 + * + * @param id 每月充值福利主键 + * @return 每月充值福利 + */ + public TtWelfareMonthlyRecharges selectTtWelfareMonthlyRechargesById(Integer id); + + /** + * 查询每月充值福利列表 + * + * @param ttWelfareMonthlyRecharges 每月充值福利 + * @return 每月充值福利集合 + */ + public List selectTtWelfareMonthlyRechargesList(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges); + + /** + * 新增每月充值福利 + * + * @param ttWelfareMonthlyRecharges 每月充值福利 + * @return 结果 + */ + public int insertTtWelfareMonthlyRecharges(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges); + + /** + * 修改每月充值福利 + * + * @param ttWelfareMonthlyRecharges 每月充值福利 + * @return 结果 + */ + public int updateTtWelfareMonthlyRecharges(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges); + + /** + * 删除每月充值福利 + * + * @param id 每月充值福利主键 + * @return 结果 + */ + public int deleteTtWelfareMonthlyRechargesById(Integer id); + + /** + * 批量删除每月充值福利 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtWelfareMonthlyRechargesByIds(Integer[] ids); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtYYOrnamentsMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtYYOrnamentsMapper.java new file mode 100644 index 0000000..e291016 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TtYYOrnamentsMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtYYOrnaments; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TtYYOrnamentsMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryErcashMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryErcashMapper.java new file mode 100644 index 0000000..daa9124 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryErcashMapper.java @@ -0,0 +1,71 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Mapper +public interface TurnoverLotteryErcashMapper extends BaseMapper { + + /** + * 查询用户当日符合条件的流水总额 (绝对值之和) + */ + @Select("") + BigDecimal sumTodayTurnover( + @Param("userId") Integer userId, + @Param("sourceList") List sourceList, + @Param("type") Integer type, + @Param("todayStart") Date todayStart, + @Param("todayEnd") Date todayEnd); + + /** + * 查询用户当日已使用的流水总额 (抽奖消耗) + */ + @Select("") + BigDecimal sumTodayUsedTurnover( + @Param("userId") Integer userId, + @Param("source") Integer source, + @Param("type") Integer type, + @Param("todayStart") Date todayStart, + @Param("todayEnd") Date todayEnd); + + /** + * 查询用户当日疯狂转盘注单退款总额(source=64 WHEEL_BET_REFUND,type=INPUT,amount 为正数)。 + * 用于从流水抽奖的流水统计中扣除已退款的注单,避免"下注→兜底退款"后仍计入流水。 + */ + @Select("SELECT IFNULL(SUM(amount), 0) " + + "FROM tt_user_blend_ercash " + + "WHERE user_id = #{userId} " + + " AND source = #{source} " + + " AND type = #{type} " + + " AND create_time BETWEEN #{todayStart} AND #{todayEnd}") + BigDecimal sumTodayWheelRefund( + @Param("userId") Integer userId, + @Param("source") Integer source, + @Param("type") Integer type, + @Param("todayStart") Date todayStart, + @Param("todayEnd") Date todayEnd); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryMapper.java new file mode 100644 index 0000000..aad3fdf --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.lottery.TtTurnoverLottery; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TurnoverLotteryMapper extends BaseMapper { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryPrizeMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryPrizeMapper.java new file mode 100644 index 0000000..893edcc --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryPrizeMapper.java @@ -0,0 +1,23 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.lottery.TtTurnoverLotteryPrize; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +@Mapper +public interface TurnoverLotteryPrizeMapper extends BaseMapper { + + /** + * 原子扣减奖品库存:real_count - 1,total_count + 1。 + * 仅当 real_count > 0 时生效,返回受影响行数(0 表示库存已耗尽或并发竞争失败)。 + * 作为数据库层兜底,防止 Redis 锁失效/被绕过时奖品超发。 + */ + @Update("UPDATE tt_turnover_lottery_prize " + + "SET real_count = real_count - 1, " + + " total_count = IFNULL(total_count, 0) + 1, " + + " update_time = NOW() " + + "WHERE id = #{id} AND real_count > 0") + int decrementStock(@Param("id") Long id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.java new file mode 100644 index 0000000..dd97010 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.java @@ -0,0 +1,49 @@ +package com.ruoyi.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.lottery.TtTurnoverLotteryRecord; +import com.ruoyi.domain.vo.lottery.TurnoverLotteryWinVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Mapper +public interface TurnoverLotteryRecordMapper extends BaseMapper { + + /** + * 全服中奖记录分页查询(按中奖时间倒序)。 + * SQL 见 resources/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.xml + * 已排除白号、机器人、封禁及已删除用户,避免污染前端全服公示列表。 + * + * @param lotteryId 抽奖活动 ID,为空查全部 + * @param minPrice 最低奖品价值门槛,为空不限 + * @param offset 分页偏移量 + * @param pageSize 每页条数 + */ + List selectGlobalWinPage(@Param("lotteryId") Long lotteryId, + @Param("minPrice") BigDecimal minPrice, + @Param("offset") Integer offset, + @Param("pageSize") Integer pageSize); + + /** + * 全服中奖记录总数(过滤条件与 selectGlobalWinPage 保持一致)。 + * SQL 见 resources/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.xml + */ + Long countGlobalWin(@Param("lotteryId") Long lotteryId, + @Param("minPrice") BigDecimal minPrice); + + /** + * 查询用户当日已消耗的流水总额(从抽奖记录表统计 cost_turnover)。 + */ + @Select("SELECT IFNULL(SUM(cost_turnover), 0) " + + "FROM tt_turnover_lottery_record " + + "WHERE user_id = #{userId} " + + " AND create_time BETWEEN #{todayStart} AND #{todayEnd}") + BigDecimal sumTodayUsedTurnover(@Param("userId") Integer userId, + @Param("todayStart") Date todayStart, + @Param("todayEnd") Date todayEnd); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/WebsiteSetupMapper.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/WebsiteSetupMapper.java new file mode 100644 index 0000000..0ccfd5e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/WebsiteSetupMapper.java @@ -0,0 +1,35 @@ +package com.ruoyi.admin.mapper; + +import com.ruoyi.domain.other.ConfigData; +import com.ruoyi.domain.other.OperationalStatistics; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface WebsiteSetupMapper { + + List getOperationalStatistics(); + + List selectParameterSettingList(); + + void truncateSysJobLog(); + + void truncateSysLogininfor(); + + void truncateSysOperLog(); + + void truncateTtBoxRecords(); + + void truncateTtFight(); + + void truncateTtFightResult(); + + void truncateTtFightUser(); + + void truncateTtVipLevel(); + + void truncateTtPromotionLevel(); + + void truncateTtOrnamentsLevel(); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/model/BoxMockCache.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/model/BoxMockCache.java new file mode 100644 index 0000000..368463f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/model/BoxMockCache.java @@ -0,0 +1,21 @@ +package com.ruoyi.admin.model; + +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +@Data +public class BoxMockCache implements Serializable { + + private List items; + + @Data + public static class MockItem implements Serializable { + //最低价格(包含) + private BigDecimal lowPrice; + //最高价格(包含) + private BigDecimal highPrice; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/model/UpdateUserAccountBo.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/model/UpdateUserAccountBo.java new file mode 100644 index 0000000..d23dc01 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/model/UpdateUserAccountBo.java @@ -0,0 +1,14 @@ +package com.ruoyi.admin.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@AllArgsConstructor +public class UpdateUserAccountBo { + private Long userId; + private BigDecimal accountAmount; + private BigDecimal accountCredits; +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/scheduled/OfficialRollTask.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/scheduled/OfficialRollTask.java new file mode 100644 index 0000000..91b8e30 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/scheduled/OfficialRollTask.java @@ -0,0 +1,161 @@ +package com.ruoyi.admin.scheduled; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.service.TtOfficialRollConfigService; +import com.ruoyi.admin.service.TtRollService; +import com.ruoyi.admin.service.impl.TtOfficialRollConfigServiceImpl; +import com.ruoyi.domain.entity.roll.TtOfficialRollConfig; +import com.ruoyi.domain.entity.roll.TtRoll; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +/** + * 官方周期Roll房定时创建任务。 + *

+ * 注册到 RuoYi 系统监控-定时任务(不加 @Scheduled,由 sys_job 统一调度): + * - 任务名称:官方日Roll生成 调用目标:officialRollTask.generateDailyRolls() cron:0 1 0 * * ?(每天0:01) + * - 任务名称:官方周Roll生成 调用目标:officialRollTask.generateWeeklyRolls() cron:0 10 0 ? * MON(每周一0:10) + *

+ * 复用官方房(roll_type=0)无扣款语义,直接创建 tt_roll;加入校验与到期开奖沿用现有逻辑。 + */ +@Slf4j +@Component("officialRollTask") +public class OfficialRollTask { + + /** 官方房类型 */ + private static final String ROLL_TYPE_OFFICIAL = "0"; + /** 未开奖状态 */ + private static final String ROLL_STATUS_OPEN = "0"; + /** 官方房创建者ID(0=官方) */ + private static final Integer OFFICIAL_USER_ID = 0; + + @Autowired + private TtOfficialRollConfigService configService; + + @Autowired + private TtRollService rollService; + + /** + * 每日0:01为所有启用的 DAILY 配置创建当日官方roll房。 + * 充值统计起点=当天0点(今日充值),开奖=次日0点。 + */ + public void generateDailyRolls() { + Date start = todayStart(); + Date end = daysLater(start, 1); + generate(TtOfficialRollConfigServiceImpl.PERIOD_DAILY, start, end); + } + + /** + * 每周一0:10为所有启用的 WEEKLY 配置创建本周官方roll房。 + * 充值统计起点=本周一0点(本周充值),开奖=下周一0点。 + */ + public void generateWeeklyRolls() { + Date start = thisMondayStart(); + Date end = daysLater(start, 7); + generate(TtOfficialRollConfigServiceImpl.PERIOD_WEEKLY, start, end); + } + + private void generate(String periodType, Date rechargeStartTime, Date endTime) { + log.info("[官方周期Roll] 开始生成 period={}, start={}, end={}", periodType, rechargeStartTime, endTime); + + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(TtOfficialRollConfig::getStatus, "0") + .eq(TtOfficialRollConfig::getPeriodType, periodType); + List configs = configService.list(query); + + if (configs == null || configs.isEmpty()) { + log.info("[官方周期Roll] 无启用的 {} 配置,跳过", periodType); + return; + } + + int created = 0; + int skipped = 0; + for (TtOfficialRollConfig config : configs) { + try { + // 幂等:同一配置 + 同一充值统计起点 至多生成一个房间 + long exists = rollService.count(Wrappers.lambdaQuery() + .eq(TtRoll::getOfficialConfigId, config.getId()) + .eq(TtRoll::getRechargeStartTime, rechargeStartTime)); + if (exists > 0) { + skipped++; + log.info("[官方周期Roll] 配置{}({})本周期房间已存在,跳过", config.getId(), config.getConfigName()); + continue; + } + + TtRoll roll = TtRoll.builder() + .jackpotId(config.getJackpotId()) + .userId(OFFICIAL_USER_ID) + .rollType(ROLL_TYPE_OFFICIAL) + .rollName(config.getRoomName()) + .description(config.getDescription()) + .endTime(endTime) + .peopleNum(config.getPeopleNum()) + .rollPassword(config.getRollPassword()) + .minRecharge(config.getMinRecharge() != null ? config.getMinRecharge() : BigDecimal.ZERO) + .vipLevel(config.getVipLevel()) + .rechargeStartTime(rechargeStartTime) + .sortBy(config.getSortBy()) + .rollStatus(ROLL_STATUS_OPEN) + .deductCredits(BigDecimal.ZERO) + .officialConfigId(config.getId()) + .createBy("system") + .createTime(new Date()) + .build(); + rollService.save(roll); + + // 回写上次生成时间(幂等辅助/可观测) + TtOfficialRollConfig update = new TtOfficialRollConfig(); + update.setId(config.getId()); + update.setLastGenerateTime(new Date()); + configService.updateById(update); + + created++; + log.info("[官方周期Roll] 配置{}({}) 已生成房间 rollId={},起点={},开奖={}", + config.getId(), config.getConfigName(), roll.getId(), rechargeStartTime, endTime); + } catch (Exception e) { + log.error("[官方周期Roll] 配置{}生成房间失败:{}", config.getId(), e.getMessage(), e); + } + } + + log.info("[官方周期Roll] 生成完成 period={}, 配置数={}, 新建={}, 跳过={}", + periodType, configs.size(), created, skipped); + } + + /** 今天0点 */ + private Date todayStart() { + Calendar c = Calendar.getInstance(); + clearTime(c); + return c.getTime(); + } + + /** 本周一0点 */ + private Date thisMondayStart() { + Calendar c = Calendar.getInstance(); + clearTime(c); + c.setFirstDayOfWeek(Calendar.MONDAY); + c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + return c.getTime(); + } + + /** 指定日期 + n 天 */ + private Date daysLater(Date base, int days) { + Calendar c = Calendar.getInstance(); + c.setTime(base); + c.add(Calendar.DAY_OF_MONTH, days); + return c.getTime(); + } + + private void clearTime(Calendar c) { + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/security/ApiUserDetailsServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/security/ApiUserDetailsServiceImpl.java new file mode 100644 index 0000000..39b5262 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/security/ApiUserDetailsServiceImpl.java @@ -0,0 +1,167 @@ +package com.ruoyi.admin.security; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.PromoAdminMapper; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.sys.PromoAdmin; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.entity.UserData; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.UserStatus; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.framework.web.service.SysPasswordService; +import com.ruoyi.framework.web.service.SysPermissionService; +import com.ruoyi.system.service.ISysUserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; + +/** + * 用户验证处理 + */ +@Service +public class ApiUserDetailsServiceImpl implements UserDetailsService { + + private static final Logger log = LoggerFactory.getLogger(ApiUserDetailsServiceImpl.class); + private final TtUserService ttUserService; + private final ISysUserService userService; + private final SysPasswordService passwordService; + private final SysPermissionService permissionService; + private final PromoAdminMapper promoAdminMapper; + + public ApiUserDetailsServiceImpl(TtUserService ttUserService, + ISysUserService userService, + SysPasswordService passwordService, + SysPermissionService permissionService, + PromoAdminMapper promoAdminMapper) { + this.ttUserService = ttUserService; + this.userService = userService; + this.passwordService = passwordService; + this.permissionService = permissionService; + this.promoAdminMapper = promoAdminMapper; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + + if (username.startsWith("api_")) { + username = username.substring(4); + TtUser user = new LambdaQueryChainWrapper<>(ttUserService.getBaseMapper()).eq(TtUser::getUserName, username).one(); + if (StringUtils.isNull(user)) { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException(MessageUtils.message("user.password.delete")); + } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + // 获取上级用户 + TtUser ParentUser = new LambdaQueryChainWrapper<>(ttUserService.getBaseMapper()).eq(TtUser::getUserId, user.getParentId()).one(); + + UserData userData = UserData.builder().build(); + // 对象拷贝 + BeanUtils.copyBeanProp(userData, user); + + if (ObjectUtils.isEmpty(ParentUser)) { + userData.setParentInvitationCode(""); + } else { + userData.setParentInvitationCode(ParentUser.getInvitationCode()); + } + return createLoginUser(userData); + } else if (username.startsWith("promo_user_")) { + String phoneNumber = username.substring(11); + TtUser user = new LambdaQueryChainWrapper<>(ttUserService.getBaseMapper()) + .eq(TtUser::getPhoneNumber, phoneNumber) + .one(); + boolean hasPromoRole = user != null + && !UserStatus.DELETED.getCode().equals(user.getDelFlag()) + && !UserStatus.DISABLE.getCode().equals(user.getStatus()) + && (UserType.ANCHOR.getCode().equals(user.getUserType()) + || UserType.BUSINESS_USER.getCode().equals(user.getUserType()) + || UserType.CARD_MERCHANT.getCode().equals(user.getUserType())); + if (hasPromoRole) { + UserData userData = UserData.builder().build(); + BeanUtils.copyBeanProp(userData, user); + return createLoginUser(userData); + } + // 超级管理员账号(promo_admin.username / phone_number)也可以是手机号 + PromoAdmin promoAdmin = promoAdminMapper.findByAccount(phoneNumber); + if (promoAdmin != null) { + UserData userData = UserData.builder() + .userId(promoAdmin.getId()) + .userName(promoAdmin.getUsername()) + .nickName(promoAdmin.getNickName()) + .userType(UserType.SUPER_ADMIN.getCode()) + .password(promoAdmin.getPassword()) + .build(); + return createLoginUser(userData); + } + // 以下报错语义与原逻辑保持一致 + if (StringUtils.isNull(user)) { + log.info("登录用户:{} 不存在.", phoneNumber); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { + log.info("登录用户:{} 已被删除.", phoneNumber); + throw new ServiceException(MessageUtils.message("user.password.delete")); + } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { + log.info("登录用户:{} 已被停用.", phoneNumber); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + log.info("登录用户:{} 身份错误.", phoneNumber); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } else if (username.startsWith("promo_")) { + username = username.substring(6); + PromoAdmin promoAdmin = promoAdminMapper.findByUsername(username); + if (promoAdmin == null) { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } else if (UserStatus.DISABLE.getCode().equals(promoAdmin.getStatus())) { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + UserData userData = UserData.builder() + .userId(promoAdmin.getId()) + .userName(promoAdmin.getUsername()) + .nickName(promoAdmin.getNickName()) + .userType(UserType.SUPER_ADMIN.getCode()) + .password(promoAdmin.getPassword()) + .build(); + return createLoginUser(userData); + } else { + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException(MessageUtils.message("user.password.delete")); + } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + + passwordService.validate(user); + + return createLoginUser(user); + } + } + + private UserDetails createLoginUser(UserData userData) { + return new LoginUser(userData.getUserId().longValue(), userData); + } + + private UserDetails createLoginUser(SysUser user) { + return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/BoxMockService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/BoxMockService.java new file mode 100644 index 0000000..e37da67 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/BoxMockService.java @@ -0,0 +1,58 @@ +package com.ruoyi.admin.service; + +import com.ruoyi.admin.cache.BoxMockCacheRepository; +import com.ruoyi.admin.domain.body.BoxMockRequest; +import com.ruoyi.admin.model.BoxMockCache; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +public class BoxMockService { + + @Autowired + private BoxMockCacheRepository boxMockCacheRepository; + + //1.根据用户的id获取redis中临时爆率列表 + public List getMockResultList(Integer uid) { + BoxMockCache cache = boxMockCacheRepository.getMockResultCache(uid); + if (cache == null || CollectionUtils.isEmpty(cache.getItems())) { + return new ArrayList<>(); + } + return cache.getItems(); + } + + public List updateMockResultList(BoxMockRequest request) { + //1.根据用户的id获取redis中临时爆率列表 + BoxMockCache cache = boxMockCacheRepository.getMockResultCache(request.getUid()); + if (cache == null) { + cache = new BoxMockCache(); + } + //2.判断lowPrice / highPrice 不能为空,且lowPrice < highPrice,lowPrice不能为负数,将数据填充到items中 + List items = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(request.getItems())) { + for (BoxMockRequest.MockItem req : request.getItems()) { + if (req.getLowPrice() == null || req.getHighPrice() == null) continue; + if (req.getLowPrice().compareTo(BigDecimal.ZERO) < 0) continue; + if (req.getLowPrice().compareTo(req.getHighPrice()) > 0) continue; + + BoxMockCache.MockItem item = new BoxMockCache.MockItem(); + item.setLowPrice(req.getLowPrice()); + item.setHighPrice(req.getHighPrice()); + items.add(item); + } + } + + cache.setItems(items); + //3.将用户的临时爆率存储在redis中,存储七天 + boxMockCacheRepository.setMockResultCache(request.getUid(), cache); + //4.返回临时爆率列表 + return cache.getItems(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/ConfigService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/ConfigService.java new file mode 100644 index 0000000..1546b89 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/ConfigService.java @@ -0,0 +1,7 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.ConfigData; + +public interface ConfigService extends IService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/ITtWelfareMonthlyRechargesService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/ITtWelfareMonthlyRechargesService.java new file mode 100644 index 0000000..07bcd5a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/ITtWelfareMonthlyRechargesService.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.domain.other.TtWelfareMonthlyRecharges; + +/** + * 每月充值福利Service接口 + * + * @author ruoyi + * @date 2024-06-24 + */ +public interface ITtWelfareMonthlyRechargesService +{ + /** + * 查询每月充值福利 + * + * @param id 每月充值福利主键 + * @return 每月充值福利 + */ + public TtWelfareMonthlyRecharges selectTtWelfareMonthlyRechargesById(Integer id); + + /** + * 查询每月充值福利列表 + * + * @param ttWelfareMonthlyRecharges 每月充值福利 + * @return 每月充值福利集合 + */ + public List selectTtWelfareMonthlyRechargesList(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges); + + /** + * 新增每月充值福利 + * + * @param ttWelfareMonthlyRecharges 每月充值福利 + * @return 结果 + */ + public int insertTtWelfareMonthlyRecharges(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges); + + /** + * 修改每月充值福利 + * + * @param ttWelfareMonthlyRecharges 每月充值福利 + * @return 结果 + */ + public int updateTtWelfareMonthlyRecharges(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges); + + /** + * 批量删除每月充值福利 + * + * @param ids 需要删除的每月充值福利主键集合 + * @return 结果 + */ + public int deleteTtWelfareMonthlyRechargesByIds(Integer[] ids); + + /** + * 删除每月充值福利信息 + * + * @param id 每月充值福利主键 + * @return 结果 + */ + public int deleteTtWelfareMonthlyRechargesById(Integer id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/LossRebateService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/LossRebateService.java new file mode 100644 index 0000000..ff2a092 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/LossRebateService.java @@ -0,0 +1,516 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtLossRebateMapper; +import com.ruoyi.admin.mapper.TtRedPacketMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.entity.TtLossRebate; +import com.ruoyi.domain.other.TtRedPacket; +import com.ruoyi.message.service.MessageService; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.service.ISysConfigService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 亏损返水(玩家可见名:亏损返利)—— 每半个月结算一次。 + *

+ * 命名:后台/报表口径叫「亏损返水」,玩家可见叫「亏损返利」; + * 红包标题(tt_red_packet.title)=「亏损返利CDK」,站内信标题(tt_notice.title)=「亏损返利」。 + *

+ * 口径:本期「游戏投入 - 游戏产出」> 0 即算亏损,返水 = 亏损 × 比例(默认 5%)。 + * 投入/产出取 {@link TtAccountRecordSource#getGameBetCodes()} / {@link TtAccountRecordSource#getGameRewardCodes()}, + * 且主表 + 归档表(tt_user_blend_ercash_archive) UNION 查询 —— 流水只保留 + * {@code flowArchiveRetainDays} 天(默认 7 天),半月区间若只查主表会漏掉前半段。 + *

+ * 为什么不用「余额 + 背包饰品」口径:提货 / 转赠会让玩家资产归零,但那并不是亏损。 + * 按资产算会给「赢了饰品并已经提货拿走实物」的大赢家继续发返水,可被套利; + * 游戏净输口径不可套利,且在「充值 → 玩 → 剩 X」的场景下与资产口径结果一致 + * (充 100、净输 90 → 资产 10 与净输 90,均返 4.5)。 + *

+ * 结算流程(定时任务 {@code lossRebateService.generateHalfMonthLossRebate()},无手动触发入口): + * 每个亏损玩家先落一条 {@link TtLossRebate} 明细((period,user_id) 唯一,天然幂等),然后: + *

    + *
  • 返水 ≤ 单期上限:自动生成 tt_red_packet + 发站内信,明细 status=1(已自动发放)
  • + *
  • 返水 > 单期上限:不自动发,明细 status=0(待人工发放), + * 运营在后台「CDK管理 - 亏损返水」页点「发放CDK」人工发(见 {@link #grantManually})
  • + *
  • 自动发放过程中抛异常:明细 status=3(自动发放失败),同样可在该页点「发放CDK」重试
  • + *
+ * 生成的 CDK 都写进 tt_red_packet,所以会正常出现在后台「CDK管理 - CDK口令」列表里; + * 玩家领取记录在「后台CDK记录」。兑换入账走 {@link TtAccountRecordSource#RECEIVE_RED_PACKET}(福利CDK收入)。 + *

+ * 排除:白号(is_blank_account=1)、机器人(user_type=03)、封禁(status=1)、已删除(del_flag!=0), + * 过滤在 SQL 里完成(见 TtUserBlendErcashMapper.sumGameBetWinByUser)。 + *

+ * ⚠ 主播(01)/招商(04)/卡商(05)/超管 是有意参与结算的,不是漏加条件 —— + * 用户明确确认过结算范围只排白号与机器人,别再"顺手修掉"。 + *

+ * 注册到 RuoYi 系统监控 - 定时任务:
+ * - 任务名称:亏损返水回血红包
+ * - 任务组名:LOSS_REBATE
+ * - 调用目标:lossRebateService.generateHalfMonthLossRebate()
+ * - cron:0 30 1 1,16 * ? (每月 1 日与 16 日 01:30,结算上一个自然半月) + */ +@Slf4j +@Service +public class LossRebateService { + + /** 红包标题:后台「CDK管理 - CDK口令」列表里显示的标题 */ + private static final String RED_PACKET_TITLE = "亏损返利CDK"; + /** 站内信标题:玩家消息中心里显示的标题 */ + private static final String NOTICE_TITLE = "亏损返利"; + /** + * 红包描述前缀:让后台在 CDK口令列表里一眼认出这是亏损返水自动生成的码。 + * 只用于展示/排查,不是幂等键(幂等靠 tt_loss_rebate 的 (period,user_id) 唯一键)。 + * 描述刻意短:金额看「金额区间」列、净亏看「亏损返水」页,这里只留期次 + 用户ID, + * 免得 CDK口令列表那一行被撑成 6 行高。 + */ + private static final String DESC_PREFIX = "亏损返水 "; + /** 自动任务写入的 create_by(手动「生成红包」仍记登录管理员名) */ + private static final String AUTO_CREATE_BY = "lossRebate"; + + private static final String CFG_RATE = "lossRebateRate"; + private static final String CFG_MIN_AMOUNT = "lossRebateMinAmount"; + private static final String CFG_MAX_AMOUNT = "lossRebateMaxAmount"; + private static final String CFG_VALID_DAYS = "lossRebateValidDays"; + + private static final BigDecimal DEFAULT_RATE = new BigDecimal("0.05"); + /** 返水低于该金额不结算:避免几毛钱的 CDK + 站内信刷屏,也避免生成一堆没人兑的码 */ + private static final BigDecimal DEFAULT_MIN_AMOUNT = new BigDecimal("1"); + /** + * 单期返水上限:超过就不自动发,转后台人工发放(见 {@link #grantManually})。≤0 表示不设上限。 + */ + private static final BigDecimal DEFAULT_MAX_AMOUNT = new BigDecimal("500"); + /** CDK 有效期天数 */ + private static final int DEFAULT_VALID_DAYS = 30; + + /** tt_loss_rebate.status:待人工发放(超过单期上限,等运营点「发放CDK」) */ + public static final int STATUS_PENDING = 0; + /** tt_loss_rebate.status:已自动发放 */ + public static final int STATUS_AUTO = 1; + /** tt_loss_rebate.status:已人工发放 */ + public static final int STATUS_MANUAL = 2; + /** tt_loss_rebate.status:自动发放失败,可在后台点「发放CDK」重试 */ + public static final int STATUS_FAILED = 3; + + private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + /** 站内信里的有效期格式:年月日 + 时分 */ + private static final String NOTICE_TIME_PATTERN = "yyyy年M月d日 HH:mm"; + + @Autowired + private TtUserBlendErcashMapper blendErcashMapper; + + @Autowired + private TtRedPacketMapper ttRedPacketMapper; + + @Autowired + private TtLossRebateMapper lossRebateMapper; + + @Autowired + private MessageService messageService; + + @Autowired + private ISysConfigService configService; + + /** 定时任务入口:结算上一个自然半月 */ + public Map generateHalfMonthLossRebate() { + LocalDate today = LocalDate.now(); + LocalDate begin; + LocalDate end; + if (today.getDayOfMonth() <= 15) { + // 1~15 日跑:结算上月 16 日 ~ 上月末 + LocalDate lastMonthEnd = today.withDayOfMonth(1).minusDays(1); + begin = lastMonthEnd.withDayOfMonth(16); + end = lastMonthEnd; + } else { + // 16 日 ~ 月末跑:结算本月 1 日 ~ 15 日 + begin = today.withDayOfMonth(1); + end = today.withDayOfMonth(15); + } + return generate(begin, end); + } + + /** + * 结算指定自然日区间(含首尾)的亏损返水。 + *

+ * 幂等靠 tt_loss_rebate 的 (period, user_id) 唯一键:已结算过的用户直接跳过, + * 所以定时任务重复跑(或运维在定时任务页点「执行一次」)只会补齐没有明细的用户,不会重复发钱。 + *

+ * ⚠ 不要"删掉该期明细再重跑"来整期重算:明细就是幂等键,删掉等于拆掉防重复发放的唯一防线, + * 重跑会把该期每个用户的红包和站内信再发一遍。下面的安全闸会直接拒绝这种情况 —— + * 真要整期重算,必须先把该期已发的返利CDK作废(后台「CDK口令」搜标题「亏损返利CDK」逐个删除/禁用)。 + */ + public Map generate(LocalDate begin, LocalDate end) { + String period = begin.format(DAY_FMT) + "~" + end.format(DAY_FMT); + Map result = new LinkedHashMap<>(); + result.put("period", period); + + // 安全闸:该期已发过返利CDK,却查不到同等数量的结算明细 —— 说明明细被人删过。 + // 明细是幂等键,这种情况下再跑会把所有人都重发一遍,直接拒绝。 + // 匹配方式用「标题 + 描述包含期次」而不是描述前缀:历史红包的描述是旧格式 + // 「亏损返水(期次 2026-09-01~2026-09-15; userId=...)」,用前缀匹配会漏掉它们,闸就形同虚设。 + long sentRedPackets = ttRedPacketMapper.selectCount(new LambdaQueryChainWrapper<>(ttRedPacketMapper) + .eq(TtRedPacket::getTitle, RED_PACKET_TITLE) + .like(TtRedPacket::getDescription, period)); + long settledRecords = lossRebateMapper.selectCount( + new LambdaQueryWrapper().eq(TtLossRebate::getPeriod, period)); + if (sentRedPackets > settledRecords) { + log.error("[亏损返水] 期次 {} 已有 {} 个返利CDK,但结算明细只剩 {} 条(明细被删过)。" + + "继续执行会整期重复发放,已拒绝。", + period, sentRedPackets, settledRecords); + result.put("refused", true); + result.put("reason", "该期已有返利CDK但明细缺失,拒绝重算以防重复发放"); + result.put("grantedCount", 0); + result.put("totalRebate", BigDecimal.ZERO); + return result; + } + + BigDecimal rate = configDecimal(CFG_RATE, DEFAULT_RATE); + BigDecimal minAmount = configDecimal(CFG_MIN_AMOUNT, DEFAULT_MIN_AMOUNT); + BigDecimal maxAmount = configDecimal(CFG_MAX_AMOUNT, DEFAULT_MAX_AMOUNT); + int validDays = configInt(CFG_VALID_DAYS, DEFAULT_VALID_DAYS); + + List betSources = new ArrayList<>(TtAccountRecordSource.getGameBetCodes()); + List winSources = new ArrayList<>(TtAccountRecordSource.getGameRewardCodes()); + List allSources = new ArrayList<>(betSources); + allSources.addAll(winSources); + + List> rows = blendErcashMapper.sumGameBetWinByUser( + betSources, winSources, allSources, + Timestamp.valueOf(begin.atStartOfDay()), + Timestamp.valueOf(end.atTime(LocalTime.MAX))); + if (CollectionUtils.isEmpty(rows)) { + log.info("[亏损返水] 期次 {} 无游戏流水,无需结算", period); + result.put("userCount", 0); + result.put("inserted", 0); + result.put("grantedCount", 0); + result.put("totalRebate", BigDecimal.ZERO); + result.put("pendingCount", 0); + result.put("pendingTotal", BigDecimal.ZERO); + return result; + } + + int inserted = 0; + int granted = 0; + int failed = 0; + BigDecimal total = BigDecimal.ZERO; + int pendingCount = 0; + BigDecimal pendingTotal = BigDecimal.ZERO; + for (Map row : rows) { + Object rawUserId = row.get("userId"); + if (!(rawUserId instanceof Number)) continue; + Integer userId = ((Number) rawUserId).intValue(); + try { + BigDecimal bet = toDecimal(row.get("betSum")); + BigDecimal win = toDecimal(row.get("winSum")); + BigDecimal loss = bet.subtract(win); + if (loss.compareTo(BigDecimal.ZERO) <= 0) continue; + + BigDecimal rebate = loss.multiply(rate).setScale(2, RoundingMode.HALF_UP); + if (rebate.compareTo(minAmount) < 0) continue; + + boolean overCap = maxAmount.compareTo(BigDecimal.ZERO) > 0 + && rebate.compareTo(maxAmount) > 0; + + Date now = DateUtils.getNowDate(); + TtLossRebate record = new TtLossRebate() + .setPeriod(period) + .setUserId(userId) + .setUserName(asString(row.get("userName"))) + .setNickName(asString(row.get("nickName"))) + .setBetAmount(bet) + .setWinAmount(win) + .setLossAmount(loss) + .setRebateAmount(rebate) + .setStatus(overCap ? STATUS_PENDING : STATUS_AUTO) + .setRemark(overCap ? "超过单期上限" + maxAmount.toPlainString() + ",待人工发放" + : "定时任务自动发放") + .setCreateTime(now) + .setUpdateTime(now); + try { + lossRebateMapper.insert(record); + } catch (DuplicateKeyException dke) { + // (period, user_id) 唯一键:本期该用户已结算过,跳过即可(定时任务重复执行安全) + continue; + } + inserted++; + + if (overCap) { + pendingCount++; + pendingTotal = pendingTotal.add(rebate); + log.warn("[亏损返水] 期次 {} userId={} 应发 {} 超过单期上限 {},转后台人工发放(明细id={})", + period, userId, rebate, maxAmount, record.getId()); + continue; + } + + TtRedPacket redPacket = null; + // 站内信是否已发出 = 玩家是否已经拿到口令。只要为 true,这条明细就永远不能再发第二次, + // 因为运营点「发放CDK」是看 status 是否为「失败/待发放」——绝不能把已发出的置成那两种。 + boolean issued = false; + try { + redPacket = createRedPacket(now, validDays, rebate, + period, userId, AUTO_CREATE_BY); + sendRebateNotice(userId, redPacket.getPassword(), redPacket.getValidity()); + issued = true; + } catch (Exception e) { + log.error("[亏损返水] 期次 {} userId={} 自动发放异常(issued={})", period, userId, issued, e); + if (!issued && redPacket != null) { + // 红包建好了但站内信没发出去:作废这个孤立红包。 + // 它的口令在后台「CDK口令」列表里是明文可见的,留着等于留了一个谁都没用、但能被兑走的码。 + try { + ttRedPacketMapper.deleteById(redPacket.getId()); + } catch (Exception ignore) { + log.warn("[亏损返水] 孤立红包作废失败,需人工核对:红包id={}", redPacket.getId()); + } + } + } + + if (issued) { + try { + lossRebateMapper.update(null, new LambdaUpdateWrapper() + .eq(TtLossRebate::getId, record.getId()) + .set(TtLossRebate::getRedPacketId, redPacket.getId()) + .set(TtLossRebate::getCdkey, redPacket.getPassword()) + .set(TtLossRebate::getGrantTime, now) + .set(TtLossRebate::getGrantBy, AUTO_CREATE_BY) + .set(TtLossRebate::getUpdateTime, DateUtils.getNowDate())); + } catch (Exception e) { + // 回填失败也必须保持「已自动发放」:明细插入时就是 STATUS_AUTO, + // 页面不会给「发放CDK」按钮,宁可人工核对也不能冒重发风险。 + log.error("[亏损返水] 明细回填失败,保持已发放状态待人工核对:" + + "明细id={} 红包id={} 口令={}", + record.getId(), redPacket.getId(), redPacket.getPassword(), e); + } + granted++; + total = total.add(rebate); + } else { + try { + lossRebateMapper.update(null, new LambdaUpdateWrapper() + .eq(TtLossRebate::getId, record.getId()) + .set(TtLossRebate::getStatus, STATUS_FAILED) + .set(TtLossRebate::getRemark, "自动发放失败,请点「发放CDK」重试") + .set(TtLossRebate::getUpdateTime, DateUtils.getNowDate())); + } catch (Exception e) { + log.error("[亏损返水] 标记自动发放失败也失败,明细保持已发放状态待人工核对:明细id={}", + record.getId(), e); + } + failed++; + } + } catch (Exception e) { + log.error("[亏损返水] 结算失败,期次={},userId={}", period, userId, e); + } + } + + log.info("[亏损返水] 期次 {} 结算完成:有流水玩家数={},新增明细={},自动发放人数={},返水总额={}," + + "超上限待人工={},待发总额={},自动发放失败={}", + period, rows.size(), inserted, granted, total, pendingCount, pendingTotal, failed); + result.put("userCount", rows.size()); + result.put("inserted", inserted); + result.put("grantedCount", granted); + result.put("totalRebate", total); + result.put("maxAmount", maxAmount); + result.put("pendingCount", pendingCount); + result.put("pendingTotal", pendingTotal); + result.put("failedCount", failed); + return result; + } + + /** + * 后台人工发放:给一条待发放(或自动发放失败)的明细生成 CDK 并给玩家发站内信。 + * 并发安全:先按 status 做 CAS 抢占,抢不到说明已被其他运营发过,直接返回失败。 + * + * @param id tt_loss_rebate.id + * @param amount 实发金额;传 null/≤0 则按明细里的应返水金额发放 + * @param operator 操作人(登录管理员账号) + */ + @Transactional(rollbackFor = Exception.class) + public R grantManually(Long id, BigDecimal amount, String operator) { + TtLossRebate record = lossRebateMapper.selectById(id); + if (record == null) return R.fail("结算明细不存在"); + if (record.getStatus() == null + || (record.getStatus() != STATUS_PENDING && record.getStatus() != STATUS_FAILED)) { + return R.fail("该记录已发放过,请勿重复操作"); + } + + BigDecimal rebate = (amount != null && amount.compareTo(BigDecimal.ZERO) > 0) + ? amount.setScale(2, RoundingMode.HALF_UP) + : record.getRebateAmount(); + if (rebate == null || rebate.compareTo(BigDecimal.ZERO) <= 0) return R.fail("发放金额必须大于0"); + // 只允许下调,不允许发放超过应返水:手滑多打一位数(7225.69 → 72256.9)会直接多发钱。 + // 真要给得比应返水多,走已有的「用户列表 → 更多 → 修改余额」(有独立审计流水)。 + if (record.getRebateAmount() != null && rebate.compareTo(record.getRebateAmount()) > 0) { + return R.fail("发放金额不能超过应返水金额 " + record.getRebateAmount().toPlainString()); + } + + Date now = DateUtils.getNowDate(); + // rebate_amount 保持"系统算出来的应返水"不动(对账基准),实发金额记进 remark —— + // 否则人工下调后原始的应返水金额就被覆盖没了,事后对不出"系统算多少 / 实际发多少"。 + int cas = lossRebateMapper.update(null, new LambdaUpdateWrapper() + .eq(TtLossRebate::getId, id) + .in(TtLossRebate::getStatus, STATUS_PENDING, STATUS_FAILED) + .set(TtLossRebate::getStatus, STATUS_MANUAL) + .set(TtLossRebate::getGrantTime, now) + .set(TtLossRebate::getGrantBy, operator) + .set(TtLossRebate::getRemark, "后台人工发放:应返水 " + + (record.getRebateAmount() == null ? "-" : record.getRebateAmount().toPlainString()) + + ",实发 " + rebate.toPlainString() + ",操作人 " + operator) + .set(TtLossRebate::getUpdateTime, now)); + if (cas <= 0) return R.fail("该记录已被其他运营发放,请刷新后查看"); + + int validDays = configInt(CFG_VALID_DAYS, DEFAULT_VALID_DAYS); + TtRedPacket redPacket = createRedPacket(now, validDays, rebate, + record.getPeriod(), record.getUserId(), operator); + sendRebateNotice(record.getUserId(), redPacket.getPassword(), redPacket.getValidity()); + + lossRebateMapper.update(null, new LambdaUpdateWrapper() + .eq(TtLossRebate::getId, id) + .set(TtLossRebate::getRedPacketId, redPacket.getId()) + .set(TtLossRebate::getCdkey, redPacket.getPassword()) + .set(TtLossRebate::getUpdateTime, DateUtils.getNowDate())); + + log.info("[亏损返水] 人工发放成功:明细id={},userId={},金额={},操作人={}", + id, record.getUserId(), rebate, operator); + return R.ok(redPacket.getPassword(), "发放成功,玩家已收到站内信"); + } + + /** 查询结算明细(后台「亏损返水」页),全部条件可选 */ + public List list(String period, Integer userId, Integer status) { + return new LambdaQueryChainWrapper<>(lossRebateMapper) + .eq(StringUtils.isNotBlank(period), TtLossRebate::getPeriod, period) + .eq(userId != null, TtLossRebate::getUserId, userId) + .eq(status != null, TtLossRebate::getStatus, status) + .orderByDesc(TtLossRebate::getPeriod) + .orderByDesc(TtLossRebate::getLossAmount) + .list(); + } + + /** 读取当前单期返水上限(后台「亏损返水」页顶部展示/编辑) */ + public BigDecimal getMaxAmount() { + return configDecimal(CFG_MAX_AMOUNT, DEFAULT_MAX_AMOUNT); + } + + /** + * 保存单期返水上限到 sys_config(键不存在则新建)。 + * ISysConfigService 的 insert/update 会同步刷新 Redis 缓存,所以下一次结算立即生效。 + * + * @param amount 上限金额;0 表示不设上限 + */ + public void saveMaxAmount(BigDecimal amount) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("单期上限不能为负数,填 0 表示不设上限"); + } + String value = amount.stripTrailingZeros().toPlainString(); + + SysConfig query = new SysConfig(); + query.setConfigKey(CFG_MAX_AMOUNT); + List exists = configService.selectConfigList(query); + if (CollectionUtils.isEmpty(exists)) { + SysConfig config = new SysConfig(); + config.setConfigName("亏损返水单期上限"); + config.setConfigKey(CFG_MAX_AMOUNT); + config.setConfigValue(value); + config.setConfigType("N"); + config.setRemark("返水超过该金额不自动发放,转「CDK管理-亏损返水」页人工发放;填 0 表示不设上限"); + configService.insertConfig(config); + } else { + SysConfig config = exists.get(0); + config.setConfigValue(value); + configService.updateConfig(config); + } + log.info("[亏损返水] 单期上限已更新为 {}(0=不设上限)", value); + } + + /** + * 生成一条亏损返利红包:num=1、金额固定、口令由调用方通过站内信私下发给本人。 + *

+ * 收款人绑定:{@code receiverUserId = userId},兑换时只放行本人 + * (见 ApiBonusServiceImpl.receiveRedPacketCheck),口令被转发/截图也兑不走。 + *

+ * {@code userId} 仍写 0(不限用户),不能写成本人 —— receiveRedPacketCheck 会把 + * 「userId=自己」判成「给粉丝的专属红包,您自己无法领取」,玩家反而兑不了自己的码。 + * 两者语义不同:userId 是"主播ID(粉丝专属)",receiverUserId 才是"指定领取人"。 + * + * @return 已插入的红包(id 与 password 已回填) + */ + private TtRedPacket createRedPacket(Date now, int validDays, BigDecimal rebate, + String period, Integer userId, String createBy) { + TtRedPacket redPacket = new TtRedPacket() + .setTitle(RED_PACKET_TITLE) + .setDescription(DESC_PREFIX + period + " uid=" + userId) + .setNum(1) + .setAmount("[" + rebate.toPlainString() + "," + rebate.toPlainString() + "]") + .setUserId(0) + .setReceiverUserId(userId) + .setPassword(IdUtils.fastSimpleUUID().toUpperCase()) + .setOpeningTime(now) + .setValidity(new Date(now.getTime() + validDays * 24L * 3600L * 1000L)) + .setStatus(0) + .setUseStatus(0) + .setCreateBy(createBy) + .setCreateTime(now); + ttRedPacketMapper.insert(redPacket); + return redPacket; + } + + /** + * 给玩家发「亏损返利」站内信。 + */ + private void sendRebateNotice(Integer userId, String password, Date validity) { + String content = "兑换CDK口令:[" + password + "]" + + ",复制以上口令,前往「CDK兑换口令页」粘贴领取,有效期至 " + + DateUtils.parseDateToStr(NOTICE_TIME_PATTERN, validity) + "。"; + messageService.sendNotice(Long.valueOf(userId), NOTICE_TITLE, content, + MessageCategoryEnum.SYS_LOSS_REBATE.getCode()); + } + + private BigDecimal configDecimal(String key, BigDecimal fallback) { + try { + String value = configService.selectConfigByKey(key); + return StringUtils.isBlank(value) ? fallback : new BigDecimal(value.trim()); + } catch (Exception e) { + return fallback; + } + } + + private int configInt(String key, int fallback) { + try { + String value = configService.selectConfigByKey(key); + return StringUtils.isBlank(value) ? fallback : Integer.parseInt(value.trim()); + } catch (Exception e) { + return fallback; + } + } + + private BigDecimal toDecimal(Object value) { + if (value == null) return BigDecimal.ZERO; + return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString()); + } + + private String asString(Object value) { + return value == null ? null : value.toString(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/PromoCdkeyRedeemService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/PromoCdkeyRedeemService.java new file mode 100644 index 0000000..b467681 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/PromoCdkeyRedeemService.java @@ -0,0 +1,11 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.PromoCdkeyRedeem; + +import java.util.List; + +public interface PromoCdkeyRedeemService extends IService { + + List queryList(PromoCdkeyRedeem query); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/ShoppingService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/ShoppingService.java new file mode 100644 index 0000000..972e534 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/ShoppingService.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.service; + +import com.ruoyi.domain.other.ShoppingBody; +import com.ruoyi.domain.vo.ShoppingDataVO; +import com.ruoyi.common.core.page.PageDataInfo; + +public interface ShoppingService { + PageDataInfo list(ShoppingBody shoppingBody); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtAdvertisementService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtAdvertisementService.java new file mode 100644 index 0000000..15533a8 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtAdvertisementService.java @@ -0,0 +1,8 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtAdvertisement; + +public interface TtAdvertisementService extends IService { + String updatePicById(TtAdvertisement ttAdvertisement); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtAnnouncementService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtAnnouncementService.java new file mode 100644 index 0000000..f63d331 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtAnnouncementService.java @@ -0,0 +1,18 @@ +package com.ruoyi.admin.service; + +import com.ruoyi.domain.other.TtAnnouncement; + +import java.util.List; + +public interface TtAnnouncementService { + + List getAnnouncementList(TtAnnouncement query); + + TtAnnouncement getAnnouncementByAnnouncementId(Long userId, Integer announcementId); + + int addAnnouncement(TtAnnouncement ttAnnouncement); + + int editAnnouncement(TtAnnouncement ttAnnouncement); + + int removeAnnouncementByAnnouncementId(Integer announcementId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBannerService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBannerService.java new file mode 100644 index 0000000..8d4ba34 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBannerService.java @@ -0,0 +1,8 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtBanner; + +public interface TtBannerService extends IService { + String updateBannerById(TtBanner ttBanner); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBonusReceiveRecordService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBonusReceiveRecordService.java new file mode 100644 index 0000000..8a88a18 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBonusReceiveRecordService.java @@ -0,0 +1,7 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtBonusReceiveRecord; + +public interface TtBonusReceiveRecordService extends IService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBonusService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBonusService.java new file mode 100644 index 0000000..727e0fc --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBonusService.java @@ -0,0 +1,20 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtBonus; + +import java.math.BigDecimal; + +public interface TtBonusService extends IService { + + String updateBonusById(TtBonus ttBonus); + + void bonus(Integer userId, Integer rechargeRecordId); + + /** + * 充值返佣(供第三方支付回调复用):按各级上级 commissionRate 逐级向上实时返佣, + * 招商账号跳过实时(走周结),写 source=13(推广福利收入)。 + * 与卡密充值 bonus() 的返佣口径完全一致,避免使用推广等级费率造成口径不符。 + */ + void distributeRechargeCommission(Integer userId, BigDecimal amountActuallyPaid); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxOpenChanceService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxOpenChanceService.java new file mode 100644 index 0000000..7331521 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxOpenChanceService.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.admin.domain.TtBoxOpenChance; + +/** + * 开箱机会Service接口 + * + * @author ruoyi + * @date 2024-07-09 + */ +public interface TtBoxOpenChanceService +{ + /** + * 查询开箱机会 + * + * @param ornamentId 开箱机会主键 + * @return 开箱机会 + */ + public TtBoxOpenChance selectTtBoxOpenChanceByOrnamentId(Integer ornamentId); + + /** + * 查询开箱机会列表 + * + * @param ttBoxOpenChance 开箱机会 + * @return 开箱机会集合 + */ + public List selectTtBoxOpenChanceList(TtBoxOpenChance ttBoxOpenChance); + + /** + * 新增开箱机会 + * + * @param ttBoxOpenChance 开箱机会 + * @return 结果 + */ + public int insertTtBoxOpenChance(TtBoxOpenChance ttBoxOpenChance); + + /** + * 修改开箱机会 + * + * @param ttBoxOpenChance 开箱机会 + * @return 结果 + */ + public int updateTtBoxOpenChance(TtBoxOpenChance ttBoxOpenChance); + + /** + * 批量删除开箱机会 + * + * @param ornamentIds 需要删除的开箱机会主键集合 + * @return 结果 + */ + public int deleteTtBoxOpenChanceByOrnamentIds(Integer[] ornamentIds); + + /** + * 删除开箱机会信息 + * + * @param ornamentId 开箱机会主键 + * @return 结果 + */ + public int deleteTtBoxOpenChanceByOrnamentId(Integer ornamentId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxOrnamentsService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxOrnamentsService.java new file mode 100644 index 0000000..8fc7d02 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxOrnamentsService.java @@ -0,0 +1,39 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.admin.controller.TtBoxOrnamentsController.batchAddParam; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.other.TtBoxOrnaments; +import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; + +import java.util.List; + +public interface TtBoxOrnamentsService extends IService { + List selectTtBoxOrnamentsList(Integer boxId); + int[] sumOdds(Integer boxId); + List selectTtBoxOrnamentsListWithTotals(Integer boxId, + int totalOdds, int totalRealOdds, int totalAnchorOdds, int totalRobotOdds, int totalThirdExplosiveOdds, + int totalRobotOddsLucky, int totalRobotOddsUnlucky, int totalWhiteOdds); + String saveBoxOrnaments(TtBoxOrnaments ttBoxOrnaments); + String updateBoxOrnamentsById(TtBoxOrnamentsDataVO ttBoxOrnamentsDataVO); + String removeBoxOrnamentsByIds(Integer boxId, List list); + AjaxResult getProfitMargin(Integer boxId); + String batchAdd(Integer boxId, List ornamentsIds); + + AjaxResult batchAdd(batchAddParam param); + + List simpleBoxDetail(Integer boxId); + + R globalData(Integer boxId); + + /** + * 检查饰品是否已被其他宝箱使用 (实现饰品独占机制) + * + * @param ornamentId 饰品 ID + * @param boxId 宝箱 ID(用于更新场景排除自己) + * @return 空 = 通过,非空 = 错误提示 + */ + String checkOrnamentUnique(Long ornamentId, Integer boxId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxRecordsService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxRecordsService.java new file mode 100644 index 0000000..5112714 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxRecordsService.java @@ -0,0 +1,18 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.other.TtBoxRecordsBody; +import com.ruoyi.domain.vo.TtBoxRecordsDataVO; + +import java.util.List; + +public interface TtBoxRecordsService extends IService { + + List selectBoxRecordsList(TtBoxRecordsBody ttBoxRecordsBody); + + /** + * 查询福利箱子开箱记录 + */ + List selectWelfareBoxRecordsList(TtBoxRecordsBody ttBoxRecordsBody); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxService.java new file mode 100644 index 0000000..7868d1f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxService.java @@ -0,0 +1,41 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtBoxType; +import com.ruoyi.domain.vo.BoxPoolStatisticsVO; +import com.ruoyi.domain.other.TtBoxBody; +import com.ruoyi.domain.vo.TtBoxDataVO; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.vo.box.TtBoxUserVO; + +import java.util.Date; +import java.util.List; + +public interface TtBoxService extends IService { + + PageDataInfo selectTtBoxList(TtBoxBody ttBoxBody); + + String updateTtBoxById(TtBoxDataVO ttBoxDataVO); + + void isReplenishment(Integer boxId); + + void delCache(Integer boxId); + + /** + * 宝箱统计:按奖池(开箱人类型)拆分的本轮剩余库存 + 今日开箱数据 + */ + List statisticsBoxData(Integer boxId, Date date); + + List getRealList(Integer boxId, int flag); + + String createTtBoxThirdExplosiveUsers(TtBoxUserVO ttBoxUserVO); + + AjaxResult queryTtBoxThirdExplosiveUsers(Integer boxId); + + AjaxResult deleteTtBoxThirdExplosiveUsers(TtBoxUserVO ttBoxUserVO); + + AjaxResult resetBox(Integer boxId); + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxTypeService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxTypeService.java new file mode 100644 index 0000000..fd72cc3 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtBoxTypeService.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtBoxType; + +public interface TtBoxTypeService extends IService { + + String updateBoxTypeById(TtBoxType ttBoxType); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtContentService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtContentService.java new file mode 100644 index 0000000..cc61b7d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtContentService.java @@ -0,0 +1,11 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtContent; + +import java.util.List; + +public interface TtContentService extends IService { + + List queryList(TtContent ttContent); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtContentTypeService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtContentTypeService.java new file mode 100644 index 0000000..829265d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtContentTypeService.java @@ -0,0 +1,10 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtContentType; + +import java.util.List; + +public interface TtContentTypeService extends IService { + List queryList(TtContentType ttContentType); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtDeliveryRecordService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtDeliveryRecordService.java new file mode 100644 index 0000000..c9e9269 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtDeliveryRecordService.java @@ -0,0 +1,32 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.dto.userRecord.DeliveryRecordsConfition; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.domain.other.TtDeliveryApplyBody; +import com.ruoyi.domain.vo.DeliveryApplyVO; +import com.ruoyi.domain.other.TtDeliveryRecordBody; +import com.ruoyi.domain.vo.TtDeliveryRecordDataVO; +import com.ruoyi.domain.vo.delivery.DeliveryRecordVO; + +import java.util.List; + +public interface TtDeliveryRecordService extends IService { + + List getDeliveryApplyList(TtDeliveryApplyBody deliveryApplyBody); + + /** + * 审核通过提货申请(确认消耗次数) + */ + String deliveryApprove(Integer deliveryRecordId); + + String deliveryFail(Integer deliveryRecordId, String message); + + List getDeliveryRecordList(TtDeliveryRecordBody deliveryRecordBody); + + + List getDeliveryRecordByUserList(TtDeliveryRecordBody deliveryRecordBody); + + List byCondition(DeliveryRecordsConfition param); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFightRankingRewardService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFightRankingRewardService.java new file mode 100644 index 0000000..fe968ac --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFightRankingRewardService.java @@ -0,0 +1,65 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.domain.other.TtFightRankingReward; + +/** + * 对战奖励金额Service接口 + * + * @author ruoyi + * @date 2024-07-04 + */ +public interface TtFightRankingRewardService +{ + /** + * 查询对战奖励金额 + * + * @param id 对战奖励金额主键 + * @return 对战奖励金额 + */ + public TtFightRankingReward selectTtFightRankingRewardById(Integer id); + + /** + * 查询对战奖励金额列表 + * + * @param ttFightRankingReward 对战奖励金额 + * @return 对战奖励金额集合 + */ + public List selectTtFightRankingRewardList(TtFightRankingReward ttFightRankingReward); + + // /** + // * 新增对战奖励金额 + // * + // * @param ttFightRankingReward 对战奖励金额 + // * @return 结果 + // */ + // public int insertTtFightRankingReward(TtFightRankingReward ttFightRankingReward); + + public String generateRankingReward(); + + /** + * 修改对战奖励金额 + * + * @param ttFightRankingReward 对战奖励金额 + * @return 结果 + */ + public int updateTtFightRankingReward(TtFightRankingReward ttFightRankingReward); + + /** + * 批量删除对战奖励金额 + * + * @param ids 需要删除的对战奖励金额主键集合 + * @return 结果 + */ + public int deleteTtFightRankingRewardByIds(Integer[] ids); + + // /** + // * 删除对战奖励金额信息 + // * + // * @param id 对战奖励金额主键 + // * @return 结果 + // */ + // public int deleteTtFightRankingRewardById(Integer id); + + public void truncateRankingReward(); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFightResultService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFightResultService.java new file mode 100644 index 0000000..0319bd2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFightResultService.java @@ -0,0 +1,21 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtFightResult; +import com.ruoyi.domain.vo.FightResultDataVO; +import com.ruoyi.domain.vo.fight.FightResultVO; + +import java.util.List; + +public interface TtFightResultService extends IService { + // FightResultDataVO getFightResult(Integer fightId); + FightResultVO getFightResult(Integer fightId); + + /** + * 批量获取多个对战的详细结果(替代循环调用 getFightResult 的 N+1 问题) + * + * @param fightIds 对战ID列表 + * @return 按入参顺序返回的 FightResultVO 列表(跳过不存在的 fightId) + */ + List getFightResultBatch(List fightIds); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFightService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFightService.java new file mode 100644 index 0000000..1d8b2e6 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFightService.java @@ -0,0 +1,17 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.vo.FightBoxDataVO; +import com.ruoyi.domain.other.TtFightBody; + +import java.util.List; + +public interface TtFightService extends IService { + + List selectFightList(TtFightBody ttFightBody); + + List selectFightBoxList(Integer fightId); + + int endFight(String fightId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFirstRechargeService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFirstRechargeService.java new file mode 100644 index 0000000..5fda5a9 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtFirstRechargeService.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.domain.other.TtFirstRecharge; + +/** + * 首充赠送Service接口 + * + * @author ruoyi + * @date 2024-06-21 + */ +public interface TtFirstRechargeService +{ + /** + * 查询首充赠送 + * + * @param id 首充赠送主键 + * @return 首充赠送 + */ + public TtFirstRecharge selectTtFirstRechargeById(Integer id); + + /** + * 查询首充赠送列表 + * + * @param ttFirstRecharge 首充赠送 + * @return 首充赠送集合 + */ + public List selectTtFirstRechargeList(TtFirstRecharge ttFirstRecharge); + + /** + * 新增首充赠送 + * + * @param ttFirstRecharge 首充赠送 + * @return 结果 + */ + public int insertTtFirstRecharge(TtFirstRecharge ttFirstRecharge); + + /** + * 修改首充赠送 + * + * @param ttFirstRecharge 首充赠送 + * @return 结果 + */ + public int updateTtFirstRecharge(TtFirstRecharge ttFirstRecharge); + + /** + * 批量删除首充赠送 + * + * @param ids 需要删除的首充赠送主键集合 + * @return 结果 + */ + public int deleteTtFirstRechargeByIds(Integer[] ids); + + /** + * 删除首充赠送信息 + * + * @param id 首充赠送主键 + * @return 结果 + */ + public int deleteTtFirstRechargeById(Integer id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtMessageSendService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtMessageSendService.java new file mode 100644 index 0000000..b8b7e4e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtMessageSendService.java @@ -0,0 +1,7 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtMessageSend; + +public interface TtMessageSendService extends IService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtMessageService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtMessageService.java new file mode 100644 index 0000000..b53892a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtMessageService.java @@ -0,0 +1,18 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtMessage; + +import java.util.List; + +public interface TtMessageService extends IService { + + List queryList(TtMessage ttMessage); + String delByIds(List ids); + + String singleMessage(Integer messageId, List userIds); + + String massMessaging(Integer messageId); + + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOfficialRollConfigService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOfficialRollConfigService.java new file mode 100644 index 0000000..217fad2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOfficialRollConfigService.java @@ -0,0 +1,18 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.entity.roll.TtOfficialRollConfig; + +public interface TtOfficialRollConfigService extends IService { + + /** + * 新增配置(含基本校验:奖池存在、周期合法、人数>0) + */ + AjaxResult addConfig(TtOfficialRollConfig config); + + /** + * 编辑配置(含基本校验) + */ + AjaxResult editConfig(TtOfficialRollConfig config); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrderService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrderService.java new file mode 100644 index 0000000..2b8cbdc --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrderService.java @@ -0,0 +1,39 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.sys.OrderQueryCondition; +import com.ruoyi.domain.dto.sys.TeamUsersParam; +import com.ruoyi.domain.dto.userRecord.OrderCondition; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.vo.order.TtOrderVO; +import com.ruoyi.domain.vo.sys.SimpleTtUserVO; + +import java.sql.Timestamp; +import java.util.List; + +public interface TtOrderService extends IService { + + List queryList(TtOrder ttOrder); + + List byCondition(OrderCondition param); + + List batchRechargeTotal(List allEmployeesId, + String beginTime, + String endTime, + Integer orderType, + Integer page, + Integer size); + + R clientList(Integer page, Integer size, int uid); + + R adminList(OrderQueryCondition condition); + + SimpleTtUserVO rechargeTotalOfBoss( + Integer employeeId, + Timestamp beginTime, + String endTime, + Integer orderType, + Integer page, + Integer size); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrnamentService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrnamentService.java new file mode 100644 index 0000000..95a3b37 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrnamentService.java @@ -0,0 +1,34 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.queryCondition.OrnamentCondition; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.TtOrnamentsYY; +import com.ruoyi.domain.other.TtOrnamentsBody; +import com.ruoyi.domain.vo.TtOrnamentVO; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; + +import java.math.BigDecimal; +import java.util.List; + +public interface TtOrnamentService extends IService { + + Page listByParam(TtOrnamentsBody param); + + List selectTtOrnamentsList(TtOrnamentsBody param); + + List selectOrnamentsItemIdList(); + + AjaxResult grantOrnaments(Integer userId, Long ornamentsId, Integer ornamentsLevelId, Integer num); + + List byCondition(OrnamentCondition condition); + + List selectOrnamentsIdList(); + + List selectOrnamentsMarketHashNameList(); + + AjaxResult updateOrnamentPrice(Long ornamentId, BigDecimal price); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrnamentYYService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrnamentYYService.java new file mode 100644 index 0000000..67508b2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrnamentYYService.java @@ -0,0 +1,8 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.TtOrnamentsYY; + +public interface TtOrnamentYYService extends IService { + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrnamentsLevelService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrnamentsLevelService.java new file mode 100644 index 0000000..8549215 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtOrnamentsLevelService.java @@ -0,0 +1,13 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtOrnamentsLevel; + +public interface TtOrnamentsLevelService extends IService { + + String generateOrnamentsLevel(Integer num); + + String updateOrnamentsLevelById(TtOrnamentsLevel ttOrnamentsLevel); + + void truncateOrnamentsLevel(); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtPromoTurnoverService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtPromoTurnoverService.java new file mode 100644 index 0000000..9e551e8 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtPromoTurnoverService.java @@ -0,0 +1,23 @@ +package com.ruoyi.admin.service; + + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.param.promotion.AnchorDailyTurnoverParam; +import com.ruoyi.domain.param.promotion.AnchorRechargeParam; +import com.ruoyi.domain.vo.promotion.AnchorDailyTurnoverVo; + +import java.util.List; + +public interface TtPromoTurnoverService { + + /** + * 获取名下主播每日流水 + */ + List getAnchorDailyTurnover(AnchorDailyTurnoverParam param); + /** + * 主播发放工资 + */ + AjaxResult recharge(AnchorRechargeParam param); + + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtPromotionLevelService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtPromotionLevelService.java new file mode 100644 index 0000000..fd161ee --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtPromotionLevelService.java @@ -0,0 +1,13 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.TtPromotionLevel; + +public interface TtPromotionLevelService extends IService { + + String generateVipLevel(Integer num); + + String updatePromotionLevelById(TtPromotionLevel ttPromotionLevel); + + void truncatePromotionLevel(); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtPromotionRecordService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtPromotionRecordService.java new file mode 100644 index 0000000..8ce3b2d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtPromotionRecordService.java @@ -0,0 +1,14 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.TtPromotionRecord; +import com.ruoyi.domain.vo.PromotionDataVO; + +import java.util.List; + +public interface TtPromotionRecordService extends IService { + + List getPromotionRecord(TtPromotionRecord ttPromotionRecord); + + PromotionDataVO statisticsPromotionData(Integer userId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeCardService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeCardService.java new file mode 100644 index 0000000..0452b4b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeCardService.java @@ -0,0 +1,21 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtRechargeCard; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; + +public interface TtRechargeCardService extends IService { + + List queryList(TtRechargeCard ttRechargeCard); + + List generateCard(Integer rechargeListId, Integer num); + + /** + * 生成空白卡密(不计入充值记录) + */ + List generateBlankCard(Integer rechargeListId, Integer num); + + void export(HttpServletResponse response, TtRechargeCard ttRechargeCard); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeConfigService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeConfigService.java new file mode 100644 index 0000000..22178af --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeConfigService.java @@ -0,0 +1,7 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.TtRechargeConfig; + +public interface TtRechargeConfigService extends IService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeProdService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeProdService.java new file mode 100644 index 0000000..579c2cb --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeProdService.java @@ -0,0 +1,7 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.TtRechargeProd; + +public interface TtRechargeProdService extends IService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeRankingRewardService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeRankingRewardService.java new file mode 100644 index 0000000..edcfe83 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeRankingRewardService.java @@ -0,0 +1,50 @@ +package com.ruoyi.admin.service; + +import com.ruoyi.domain.other.TtRechargeRankingReward; + +import java.util.List; + +/** + * 充值奖励金额Service接口 + * + * @author ruoyi + * @date 2024-07-04 + */ +public interface TtRechargeRankingRewardService +{ + /** + * 查询充值奖励金额 + * + * @param id 充值奖励金额主键 + * @return 充值奖励金额 + */ + public TtRechargeRankingReward selectTtRechargeRankingRewardById(Integer id); + + /** + * 查询充值奖励金额列表 + * + * @param ttRechargeRankingReward 充值奖励金额 + * @return 充值奖励金额集合 + */ + public List selectTtRechargeRankingRewardList(TtRechargeRankingReward ttRechargeRankingReward); + + public String generateRankingReward(); + + /** + * 修改充值奖励金额 + * + * @param ttRechargeRankingReward 充值奖励金额 + * @return 结果 + */ + public int updateTtRechargeRankingReward(TtRechargeRankingReward ttRechargeRankingReward); + + /** + * 批量删除充值奖励金额 + * + * @param ids 需要删除的充值奖励金额主键集合 + * @return 结果 + */ + public int deleteTtRechargeRankingRewardByIds(Integer[] ids); + + public void truncateRankingReward(); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeRecordService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeRecordService.java new file mode 100644 index 0000000..3a5f779 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRechargeRecordService.java @@ -0,0 +1,12 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.domain.other.TtRechargeRecordBody; + +import java.util.List; + +public interface TtRechargeRecordService extends IService { + + List queryList(TtRechargeRecordBody rechargeRecordBody); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRedPackService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRedPackService.java new file mode 100644 index 0000000..eb56c87 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRedPackService.java @@ -0,0 +1,62 @@ +package com.ruoyi.admin.service; + +import com.ruoyi.domain.other.TtRedPack; + +import java.util.List; + +/** + * 【请填写功能名称】Service接口 + * + * @author ruoyi + * @date 2023-06-29 + */ +public interface TtRedPackService +{ + /** + * 查询【请填写功能名称】 + * + * @param id 【请填写功能名称】主键 + * @return 【请填写功能名称】 + */ + public TtRedPack selectTtRedPackById(Long id); + + /** + * 查询【请填写功能名称】列表 + * + * @param ttRedPack 【请填写功能名称】 + * @return 【请填写功能名称】集合 + */ + public List selectTtRedPackList(TtRedPack ttRedPack); + + /** + * 新增【请填写功能名称】 + * + * @param ttRedPack 【请填写功能名称】 + * @return 结果 + */ + public int insertTtRedPack(TtRedPack ttRedPack); + + /** + * 修改【请填写功能名称】 + * + * @param ttRedPack 【请填写功能名称】 + * @return 结果 + */ + public int updateTtRedPack(TtRedPack ttRedPack); + + /** + * 批量删除【请填写功能名称】 + * + * @param ids 需要删除的【请填写功能名称】主键集合 + * @return 结果 + */ + public int deleteTtRedPackByIds(Long[] ids); + + /** + * 删除【请填写功能名称】信息 + * + * @param id 【请填写功能名称】主键 + * @return 结果 + */ + public int deleteTtRedPackById(Long id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRedPacketRecordService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRedPacketRecordService.java new file mode 100644 index 0000000..84b8f49 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRedPacketRecordService.java @@ -0,0 +1,13 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtRedPacketRecord; +import com.ruoyi.domain.other.TtRedPacketRecordBody; +import com.ruoyi.domain.vo.TtRedPacketRecordDataVO; + +import java.util.List; + +public interface TtRedPacketRecordService extends IService { + + List queryList(TtRedPacketRecordBody ttRedPacketRecordBody); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRedPacketService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRedPacketService.java new file mode 100644 index 0000000..613a03d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRedPacketService.java @@ -0,0 +1,17 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.other.TtRedPacket; +import com.ruoyi.domain.other.TtRedPacketBody; +import com.ruoyi.domain.vo.TtRedPacketDataVO; + +import java.util.List; + +public interface TtRedPacketService extends IService { + + List queryList(TtRedPacketBody ttRedPacketBody); + + List insertRedPacket(TtRedPacketDataVO ttRedPacketDataVO); + AjaxResult updateRedPacketById(TtRedPacket redPacket); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtReplacementRecordService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtReplacementRecordService.java new file mode 100644 index 0000000..aae93a4 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtReplacementRecordService.java @@ -0,0 +1,62 @@ +package com.ruoyi.admin.service; + +import com.ruoyi.domain.other.TtReplacementRecord; + +import java.util.List; + +/** + * 汰换记录Service接口 + * + * @author junhai + * @date 2023-09-10 + */ +public interface TtReplacementRecordService +{ + /** + * 查询汰换记录 + * + * @param id 汰换记录主键 + * @return 汰换记录 + */ + public TtReplacementRecord selectTtReplacementRecordById(Long id); + + /** + * 查询汰换记录列表 + * + * @param ttReplacementRecord 汰换记录 + * @return 汰换记录集合 + */ + public List selectTtReplacementRecordList(TtReplacementRecord ttReplacementRecord); + + /** + * 新增汰换记录 + * + * @param ttReplacementRecord 汰换记录 + * @return 结果 + */ + public int insertTtReplacementRecord(TtReplacementRecord ttReplacementRecord); + + /** + * 修改汰换记录 + * + * @param ttReplacementRecord 汰换记录 + * @return 结果 + */ + public int updateTtReplacementRecord(TtReplacementRecord ttReplacementRecord); + + /** + * 批量删除汰换记录 + * + * @param ids 需要删除的汰换记录主键集合 + * @return 结果 + */ + public int deleteTtReplacementRecordByIds(Long[] ids); + + /** + * 删除汰换记录信息 + * + * @param id 汰换记录主键 + * @return 结果 + */ + public int deleteTtReplacementRecordById(Long id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightGroupBoxService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightGroupBoxService.java new file mode 100644 index 0000000..44c7580 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightGroupBoxService.java @@ -0,0 +1,64 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.domain.other.TtRobotFightGroupBox; + +/** + * 机器人对战分组宝箱Service接口 + * + * @author ruoyi + * @date 2024-06-28 + */ +public interface TtRobotFightGroupBoxService +{ + /** + * 查询机器人对战分组宝箱 + * + * @param groupId 机器人对战分组宝箱主键 + * @return 机器人对战分组宝箱 + */ + public TtRobotFightGroupBox selectTtRobotFightGroupBoxByBoxId(Integer groupId, Integer boxId); + + /** + * 查询机器人对战分组宝箱列表 + * + * @param ttRobotFightGroupBox 机器人对战分组宝箱 + * @return 机器人对战分组宝箱集合 + */ + public List selectTtRobotFightGroupBoxList( + Integer groupId, + TtRobotFightGroupBox ttRobotFightGroupBox + ); + + /** + * 新增机器人对战分组宝箱 + * + * @param ttRobotFightGroupBox 机器人对战分组宝箱 + * @return 结果 + */ + public int insertTtRobotFightGroupBox(TtRobotFightGroupBox ttRobotFightGroupBox); + + /** + * 修改机器人对战分组宝箱 + * + * @param ttRobotFightGroupBox 机器人对战分组宝箱 + * @return 结果 + */ + public int updateTtRobotFightGroupBox(TtRobotFightGroupBox ttRobotFightGroupBox); + + /** + * 批量删除机器人对战分组宝箱 + * + * @param groupIds 需要删除的机器人对战分组宝箱主键集合 + * @return 结果 + */ + public int deleteTtRobotFightGroupBoxByGroupIds(Integer groupId, Integer[] boxIds); + + /** + * 删除机器人对战分组宝箱信息 + * + * @param groupId 机器人对战分组宝箱主键 + * @return 结果 + */ + public int deleteTtRobotFightGroupBoxByGroupId(Integer groupId, Integer boxId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightGroupService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightGroupService.java new file mode 100644 index 0000000..bdbcf6a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightGroupService.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.domain.other.TtRobotFightGroup; + +/** + * 机器人对战分组Service接口 + * + * @author ruoyi + * @date 2024-06-28 + */ +public interface TtRobotFightGroupService +{ + /** + * 查询机器人对战分组 + * + * @param groupId 机器人对战分组主键 + * @return 机器人对战分组 + */ + public TtRobotFightGroup selectTtRobotFightGroupByGroupId(Integer groupId); + + /** + * 查询机器人对战分组列表 + * + * @param ttRobotFightGroup 机器人对战分组 + * @return 机器人对战分组集合 + */ + public List selectTtRobotFightGroupList(TtRobotFightGroup ttRobotFightGroup); + + /** + * 新增机器人对战分组 + * + * @param ttRobotFightGroup 机器人对战分组 + * @return 结果 + */ + public int insertTtRobotFightGroup(TtRobotFightGroup ttRobotFightGroup); + + /** + * 修改机器人对战分组 + * + * @param ttRobotFightGroup 机器人对战分组 + * @return 结果 + */ + public int updateTtRobotFightGroup(TtRobotFightGroup ttRobotFightGroup); + + /** + * 批量删除机器人对战分组 + * + * @param groupIds 需要删除的机器人对战分组主键集合 + * @return 结果 + */ + public int deleteTtRobotFightGroupByGroupIds(Integer[] groupIds); + + /** + * 删除机器人对战分组信息 + * + * @param groupId 机器人对战分组主键 + * @return 结果 + */ + public int deleteTtRobotFightGroupByGroupId(Integer groupId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightJoinConfigService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightJoinConfigService.java new file mode 100644 index 0000000..647e908 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightJoinConfigService.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.domain.other.TtRobotFightJoinConfig; + +/** + * 机器人对战进房时间配置Service接口 + * + * @author ruoyi + * @date 2026-07-23 + */ +public interface TtRobotFightJoinConfigService +{ + /** + * 查询机器人对战进房时间配置 + * + * @param id 主键 + * @return 机器人对战进房时间配置 + */ + public TtRobotFightJoinConfig selectTtRobotFightJoinConfigById(Integer id); + + /** + * 查询机器人对战进房时间配置列表 + * + * @param ttRobotFightJoinConfig 机器人对战进房时间配置 + * @return 机器人对战进房时间配置集合 + */ + public List selectTtRobotFightJoinConfigList(TtRobotFightJoinConfig ttRobotFightJoinConfig); + + /** + * 新增机器人对战进房时间配置 + * + * @param ttRobotFightJoinConfig 机器人对战进房时间配置 + * @return 结果 + */ + public int insertTtRobotFightJoinConfig(TtRobotFightJoinConfig ttRobotFightJoinConfig); + + /** + * 修改机器人对战进房时间配置 + * + * @param ttRobotFightJoinConfig 机器人对战进房时间配置 + * @return 结果 + */ + public int updateTtRobotFightJoinConfig(TtRobotFightJoinConfig ttRobotFightJoinConfig); + + /** + * 批量删除机器人对战进房时间配置 + * + * @param ids 需要删除的机器人对战进房时间配置主键集合 + * @return 结果 + */ + public int deleteTtRobotFightJoinConfigByIds(Integer[] ids); + + /** + * 删除机器人对战进房时间配置信息 + * + * @param id 主键 + * @return 结果 + */ + public int deleteTtRobotFightJoinConfigById(Integer id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightPriceConfigService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightPriceConfigService.java new file mode 100644 index 0000000..9a0c5d2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRobotFightPriceConfigService.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.domain.other.TtRobotFightPriceConfig; + +/** + * 机器人对战价格档位配置Service接口 + * + * @author ruoyi + * @date 2026-08-13 + */ +public interface TtRobotFightPriceConfigService +{ + /** + * 查询机器人对战价格档位配置 + * + * @param id 主键 + * @return 机器人对战价格档位配置 + */ + public TtRobotFightPriceConfig selectTtRobotFightPriceConfigById(Integer id); + + /** + * 查询机器人对战价格档位配置列表 + * + * @param ttRobotFightPriceConfig 机器人对战价格档位配置 + * @return 机器人对战价格档位配置集合 + */ + public List selectTtRobotFightPriceConfigList(TtRobotFightPriceConfig ttRobotFightPriceConfig); + + /** + * 新增机器人对战价格档位配置 + * + * @param ttRobotFightPriceConfig 机器人对战价格档位配置 + * @return 结果 + */ + public int insertTtRobotFightPriceConfig(TtRobotFightPriceConfig ttRobotFightPriceConfig); + + /** + * 修改机器人对战价格档位配置 + * + * @param ttRobotFightPriceConfig 机器人对战价格档位配置 + * @return 结果 + */ + public int updateTtRobotFightPriceConfig(TtRobotFightPriceConfig ttRobotFightPriceConfig); + + /** + * 批量删除机器人对战价格档位配置 + * + * @param ids 需要删除的机器人对战价格档位配置主键集合 + * @return 结果 + */ + public int deleteTtRobotFightPriceConfigByIds(Integer[] ids); + + /** + * 删除机器人对战价格档位配置信息 + * + * @param id 主键 + * @return 结果 + */ + public int deleteTtRobotFightPriceConfigById(Integer id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollJackpotOrnamentsService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollJackpotOrnamentsService.java new file mode 100644 index 0000000..5ef0dcd --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollJackpotOrnamentsService.java @@ -0,0 +1,22 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.dto.rollJackpotOrnament.RollJOEdit; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnaments; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnamentsBody; +import com.ruoyi.domain.vo.TtRollJackpotOrnamentsDataVO; + +import java.util.List; + +public interface TtRollJackpotOrnamentsService extends IService { + + List queryList(TtRollJackpotOrnamentsBody rollJackpotOrnamentsBody); + + String insertRollJackpotOrnaments(TtRollJackpotOrnaments ttRollJackpotOrnaments); + + String updateRollJackpotOrnamentsById(TtRollJackpotOrnaments rollJOEdit); + + String batchAdd(Integer rollJackpotId, List OrnamentsIds); + + void recalcJackpotTotalPrice(Integer jackpotId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollJackpotService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollJackpotService.java new file mode 100644 index 0000000..9fb82af --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollJackpotService.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.roll.TtRollJackpot; + +public interface TtRollJackpotService extends IService { + + String removeRollJackpotById(Long jackpotId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollService.java new file mode 100644 index 0000000..a379c59 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollService.java @@ -0,0 +1,53 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.admin.controller.TtRollController; +import com.ruoyi.admin.domain.body.AddRobotBody; +import com.ruoyi.admin.domain.body.AddUserBody; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.roll.GetRollPrizePool; +import com.ruoyi.domain.dto.roll.InviteRollUser; +import com.ruoyi.domain.entity.roll.TtRoll; +import com.ruoyi.domain.entity.roll.TtRollBody; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.TtRollPrizeDataVO; +import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsByPageVO; +import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsVO; + +import java.util.List; +import java.util.Map; + +public interface TtRollService extends IService { + + List queryList(TtRollBody ttRollBody); + + AjaxResult createRoll(TtRoll ttRoll); + + AjaxResult removeRollById(Long rollId, String updateBy); + + AjaxResult updateRollById(TtRoll ttRoll); + + List getRollPrizeList(Integer rollId); + + AjaxResult namedWinner(TtRollPrizeDataVO param); + + AjaxResult getRollUsers(Integer rollId); + + R cancelNamedWinner(List rollUserPrizeIds); + + R getRollPrizePool(GetRollPrizePool param); + + R inviteRollUser(InviteRollUser inviteRollUser); + + List getRollUser(Integer rollId); + + /** + * Roll房实际中奖记录(开奖后参与用户中了什么饰品) + */ + R>> winningRecords(Integer rollId); + + AjaxResult addRobot(AddRobotBody addRobotBody); + + AjaxResult addUser(AddUserBody addUserBody); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollUserPrizeService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollUserPrizeService.java new file mode 100644 index 0000000..4a58e2b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollUserPrizeService.java @@ -0,0 +1,8 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.roll.TtRollUser; +import com.ruoyi.domain.entity.roll.TtRollUserPrize; + +public interface TtRollUserPrizeService extends IService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollUserService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollUserService.java new file mode 100644 index 0000000..f5efaf6 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtRollUserService.java @@ -0,0 +1,10 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.roll.TtRollUser; + +public interface TtRollUserService extends IService { + R rollWinners(Integer rollId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTaskCenterService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTaskCenterService.java new file mode 100644 index 0000000..c792a6a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTaskCenterService.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.domain.other.TtTaskCenter; + +/** + * 任务中心Service接口 + * + * @author ruoyi + * @date 2024-05-25 + */ +public interface TtTaskCenterService +{ + /** + * 查询任务中心 + * + * @param taskId 任务中心主键 + * @return 任务中心 + */ + public TtTaskCenter selectTtTaskCenterByTaskId(Integer taskId); + + /** + * 查询任务中心列表 + * + * @param ttTaskCenter 任务中心 + * @return 任务中心集合 + */ + public List selectTtTaskCenterList(TtTaskCenter ttTaskCenter); + + /** + * 新增任务中心 + * + * @param ttTaskCenter 任务中心 + * @return 结果 + */ + public int insertTtTaskCenter(TtTaskCenter ttTaskCenter); + + /** + * 修改任务中心 + * + * @param ttTaskCenter 任务中心 + * @return 结果 + */ + public int updateTtTaskCenter(TtTaskCenter ttTaskCenter); + + /** + * 批量删除任务中心 + * + * @param taskIds 需要删除的任务中心主键集合 + * @return 结果 + */ + public int deleteTtTaskCenterByTaskIds(Integer[] taskIds); + + /** + * 删除任务中心信息 + * + * @param taskId 任务中心主键 + * @return 结果 + */ + public int deleteTtTaskCenterByTaskId(Integer taskId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTimeRollService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTimeRollService.java new file mode 100644 index 0000000..893e539 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTimeRollService.java @@ -0,0 +1,23 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.roll.TtTimeRoll; +import com.ruoyi.domain.vo.TtRollPrizeDataVO; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.exception.job.TaskException; +import org.quartz.SchedulerException; + +import java.util.List; + +public interface TtTimeRollService extends IService { + + String insertTimeRoll(TtTimeRoll ttTimeRoll) throws SchedulerException, TaskException; + + AjaxResult removeTimeRollById(Integer id) throws SchedulerException; + + String changeStatus(TtTimeRoll ttTimeRoll) throws SchedulerException; + + List getTimeRollPrizeList(Integer id); + + String namedWinner(TtRollPrizeDataVO rollPrizeData); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTimeRollUserService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTimeRollUserService.java new file mode 100644 index 0000000..2f3b346 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTimeRollUserService.java @@ -0,0 +1,7 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.roll.TtTimeRollUser; + +public interface TtTimeRollUserService extends IService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTransferApplyService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTransferApplyService.java new file mode 100644 index 0000000..8d96a8f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtTransferApplyService.java @@ -0,0 +1,43 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.transfer.TtTransferApply; +import com.ruoyi.domain.other.TransferApplyBody; +import com.ruoyi.domain.vo.transfer.TransferApplyBatchVO; +import com.ruoyi.domain.vo.transfer.TransferApplyVO; + +import java.util.List; + +public interface TtTransferApplyService extends IService { + + /** + * 获取转赠申请列表(按饰品逐条展示) + */ + List getTransferApplyList(TransferApplyBody body); + + /** + * 获取转赠申请批次列表(同一批次聚合展示,含饰品明细) + */ + List getTransferApplyBatchList(TransferApplyBody body); + + /** + * 审核通过 + */ + String approve(Integer id); + + /** + * 审核驳回 + */ + String reject(Integer id, String rejectReason); + + /** + * 批量审核通过(按批次号,整个批次一起通过) + */ + String approveBatch(String batchId); + + /** + * 批量审核驳回(按批次号,整个批次一起驳回) + */ + String rejectBatch(String batchId, String rejectReason); +} + diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUpgradeFailOrnamentsService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUpgradeFailOrnamentsService.java new file mode 100644 index 0000000..7162bc5 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUpgradeFailOrnamentsService.java @@ -0,0 +1,19 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.admin.controller.TtUpgradeFailOrnamentsController; +import com.ruoyi.admin.controller.TtUpgradeFailOrnamentsController.BatchAddParam; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.other.TtUpgradeFailOrnaments; +import com.ruoyi.domain.vo.TtUpgradeFailOrnamentsDataVO; + +import java.util.List; + +public interface TtUpgradeFailOrnamentsService extends IService { + + List queryList(TtUpgradeFailOrnamentsController.listParam param); + + AjaxResult batchAdd(BatchAddParam param); + + String updateUpgradeFailOrnamentsById(TtUpgradeFailOrnamentsDataVO ttUpgradeFailOrnamentsDataVO); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUpgradeOrnamentsService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUpgradeOrnamentsService.java new file mode 100644 index 0000000..9fb937d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUpgradeOrnamentsService.java @@ -0,0 +1,21 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtUpgradeOrnaments; +import com.ruoyi.domain.other.TtUpgradeOrnamentsBody; +import com.ruoyi.domain.vo.TtUpgradeOrnamentsDataVO; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +public interface TtUpgradeOrnamentsService extends IService { + + List queryList(TtUpgradeOrnamentsBody ttUpgradeOrnamentsBody); + + String batchAdd(List ornamentsIds); + + String updateUpgradeOrnamentsById(TtUpgradeOrnamentsDataVO ttUpgradeOrnamentsDataVO); + + Map getUpgradeProfitStatistics(Integer id); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUpgradeRecordService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUpgradeRecordService.java new file mode 100644 index 0000000..5bda257 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUpgradeRecordService.java @@ -0,0 +1,21 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.dto.upgrade.UpgradeCondition; +import com.ruoyi.domain.other.TtUpgradeRecord; +import com.ruoyi.domain.other.TtUpgradeRecordBody; +import com.ruoyi.domain.vo.upgrade.UpgradeRecordVO; + +import java.util.List; + +public interface TtUpgradeRecordService extends IService { + + List getUpgradeRecord(TtUpgradeRecordBody ttUpgradeRecordBody); + + R> historyDetail(UpgradeCondition param); + + R> adminGetLog(UpgradeCondition param); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserAmountRecordsService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserAmountRecordsService.java new file mode 100644 index 0000000..8bc7a31 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserAmountRecordsService.java @@ -0,0 +1,26 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.userRecord.AmountRecordsDetailCondition; +import com.ruoyi.domain.vo.TtUserAccountRecordsRankVO; +import com.ruoyi.domain.entity.recorde.TtUserAmountRecords; +import com.ruoyi.domain.other.TtUserAmountRecordsBody; +import com.ruoyi.domain.vo.TtUserAmountRecords.UserAmountDetailVO; +import com.ruoyi.domain.vo.UserBERankVO; +import org.apache.ibatis.annotations.Param; + +import java.sql.Timestamp; +import java.util.List; + +public interface TtUserAmountRecordsService extends IService { + + R queryList(TtUserAmountRecordsBody ttUserAmountRecordsBody); + + Page rank(Timestamp begin, Timestamp end, Integer page, Integer size); + + R pWelfareRecords(Integer userId, Integer page, Integer size); + + List userAccountDetail(AmountRecordsDetailCondition param); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserAvatarService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserAvatarService.java new file mode 100644 index 0000000..170bef7 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserAvatarService.java @@ -0,0 +1,9 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtUserAvatar; + +public interface TtUserAvatarService extends IService { + + String updateUserAvatarById(TtUserAvatar ttUserAvatar); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserBlendErcashService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserBlendErcashService.java new file mode 100644 index 0000000..0481b4a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserBlendErcashService.java @@ -0,0 +1,22 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.ChildDetailByBoss; +import com.ruoyi.domain.dto.userRecord.BlendErcashCondition; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.vo.ChildDetailByBossVO; + +public interface TtUserBlendErcashService extends IService { + + R byCondition(BlendErcashCondition condition); + + /** 只查归档表的收支明细分页查询 */ + R byConditionArchive(BlendErcashCondition condition); + + /** 只查归档表的收支明细总数 */ + Integer countArchive(BlendErcashCondition condition); + + R> childDetailByBoss(ChildDetailByBoss param); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserCreditsRecordsService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserCreditsRecordsService.java new file mode 100644 index 0000000..55b4460 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserCreditsRecordsService.java @@ -0,0 +1,19 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords; +import com.ruoyi.domain.other.TtUserCreditsRecordsBody; +import com.ruoyi.domain.vo.TtUserCreditsRecordsRankVO; + +import java.sql.Timestamp; +import java.util.List; + +public interface TtUserCreditsRecordsService extends IService { + + List queryList(TtUserCreditsRecordsBody ttUserCreditsRecordsBody); + + Page rank(Timestamp begin,Timestamp end, Integer page, Integer size); + + Page pWelfareRecords(Integer uid, Integer type, Integer page, Integer size); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserService.java new file mode 100644 index 0000000..789c2e1 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserService.java @@ -0,0 +1,243 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.admin.model.UpdateUserAccountBo; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.sys.TeamUsersParam; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtUserBody; +import com.ruoyi.domain.other.TtUserPackSackBody; +import com.ruoyi.domain.vo.TtUserPackSackDataVO; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.domain.other.ApiUserOnline; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.vo.TtUserVipLevelVo; +import com.ruoyi.domain.vo.TtUserVipProgressVo; +import com.ruoyi.domain.vo.sys.SimpleTtUserVO; +import com.ruoyi.domain.vo.user.TtUseChildInfoVo; +import com.ruoyi.domain.vo.user.TtUserInfoVo; +import com.ruoyi.domain.vo.user.TtUserVo; +import org.apache.commons.lang3.tuple.Pair; + +import jakarta.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; + +public interface TtUserService extends IService { + + Pair> queryList(TtUserBody ttUserBody); + + AjaxResult updateUserById(TtUser ttUser); + + AjaxResult updateUserInvitationCodeById(TtUser ttUser); + + void generateAccount(HttpServletResponse response, List accountList); + + List getAccountList(Integer num); + + // 生成招商账号:支持按位置传入自定义手机号(为空的位置随机生成) + List getAccountList(Integer num, List customPhoneNumbers); + + TtUser getAccountList(String phoneNumber, String password); + + List getRobotList(Integer num); + + List getBlankAccountList(Integer num); + + // 生成白号:支持按位置传入自定义手机号(为空的位置随机生成) + List getBlankAccountList(Integer num, List customPhoneNumbers); + + List getCardMerchantList(Integer num); + + // 生成卡商账号:支持按位置传入自定义手机号(为空的位置随机生成) + List getCardMerchantList(Integer num, List customPhoneNumbers); + + boolean checkUserNameUnique(TtUser user); + + boolean checkPhoneUnique(TtUser user); + + boolean checkIdNumUnique(TtUser user); + + boolean checkNickNameUnique(TtUser user); + + String getInvitationCode(); + + public TtUser selectTtUserById(Long id); + + ApiUserOnline loginUserToUserOnline(LoginUser user); + + ApiUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user); + + ApiUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user); + + ApiUserOnline selectOnlineByUserName(String userName, LoginUser user); + + @Deprecated + void insertUserAmountRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal amount, BigDecimal finalAmount); + @Deprecated + void insertUserAmountRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal amount, BigDecimal finalAmount, + Integer pwChildId, String pwChildName, BigDecimal childAccount); + + void insertUserAmountRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal amount, BigDecimal finalAmount, + Integer taskId); + + void insertUserCreditsRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal credits, BigDecimal finalCredits); + + void insertUserCreditsRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal credits, BigDecimal finalCredits, + Integer PWChildId, String PWChildName, BigDecimal childAccount); + + void insertUserCreditsRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal credits, BigDecimal finalCredits, + Integer taskId); + + List getPackSack(TtUserPackSackBody ttUserPackSackBody); + + Map getUserProfitStatistics(Integer userId); + + + List propRankOfDay(Timestamp begin,Timestamp end,Integer number); + + R> teamUsers(TeamUsersParam param); + + List teamUIds(Integer bossId); + + void updateUserVIPLevel(Integer userId); + + /** + * 当前vip进度 + */ + TtUserVipLevelVo userVipLeverRate(Integer userId); + + /** + * VIP等级进度(经验制):当前经验、下一级阈值与差值 + */ + TtUserVipProgressVo userVipProgress(Integer userId); + /** + * 获取用户信息包含下级信息 + */ + TtUserInfoVo userInfo(Integer userId); + /** + * 获取用户信息包含下级用户信息 + */ + List userChildInfos(Integer userId); + + /** + * 当前用户领取对应VIP等级奖励的钱 + */ + AjaxResult userVipLevelRechargeThreshold(Integer userId, java.util.List vipLevels); + /** + * 当前用户领取对应VIP等级奖励的钱 + */ + AjaxResult userHasVipLevelRechargeThreshold(Integer userId); + + /** + * 检查当前用户是否有可领取的VIP等级红包 + */ + AjaxResult checkVipLevelClaimable(Integer userId); + /** + * 当前用户领取充值达到的奖金 + */ + AjaxResult userRechargeBonus(Integer userId, Integer recharge, Double bonus); + + /** + * 当前用户领取充值已达到的奖金 + */ + AjaxResult userHasRechargeBonus(Integer userId); + /** + * 当前用户积分兑换为金币 + */ + AjaxResult userCreditsToAmount(Integer userId, BigDecimal credits); + + /** + * 玩游戏日常金币变更,会变更积分,不是提货变更。 + * 更新用户余额状态。amount>0是增加,amount<0是扣除。 + * 扣除逻辑会先检查余额是否充足,不充足的话,则返回失败。 + * 会异步记录流水表 + * @param userId + * @param amount + * @return + */ + R updateUserAccount(Integer userId, BigDecimal amount, TtAccountRecordSource source); + + /** + * 充值、兑换金额变更。增加时,只增加余额,不动积分。 + * 更新用户余额状态。amount>0是增加,amount<0是扣除。 + * 扣除逻辑会先检查余额是否充足,不充足的话,则返回失败。 + * 会异步记录流水表 + * @param userId + * @param amount + * @return + */ + R updateOnlyUserAccount(Integer userId, BigDecimal amount, TtAccountRecordSource source); + + /** + * 仅扣减金币,不动积分(任何场景都不会因 LEAST 副作用压低积分)。 + * 仅支持扣款(amount<0),余额不足返回失败。异步记录金币流水。 + */ + R updateUserAccountKeepCredits(Integer userId, BigDecimal amount, TtAccountRecordSource source); + + /** + * 扣减金币 + 积分,并写入一条同时包含 amount 和 credits 的 blend_ercash 流水记录。 + * 仅支持扣款(amount<0),余额不足返回失败;creditsCut 为 null 或 0 表示不扣积分。 + */ + R updateUserAccountKeepCredits(Integer userId, BigDecimal amount, TtAccountRecordSource source, BigDecimal creditsCut); + + /** + * 退款专用:加金币 + 加积分,写入一条 INPUT 流水。 + * 与 updateUserAccountKeepCredits(扣款专用)对称,用于注单退款/兜底结算。 + * goldAmount 必须 > 0;creditsRefund 为 null 或 0 表示不退积分。 + */ + R refundKeepCredits(Integer userId, BigDecimal goldAmount, TtAccountRecordSource source, BigDecimal creditsRefund); + + /** + * 分发游戏消费佣金给上级(供 MQ 消费者调用)。 + * 递归向上逐级发放佣金,每级按上级的经典/新游戏佣金比例计算,最多 5 级。 + * 消费失败时抛异常,由 MQ 消费者 nack 重投。 + */ + void distributeGameCommission(Integer userId, BigDecimal consumeAmount, TtAccountRecordSource source); + + /** + * 积分变更。比如商城积分换物品。 + * 更新用户余额状态。amount>0是增加,amount<0是扣除。 + * 扣除逻辑会先检查余额是否充足,不充足的话,则返回失败。 + * 会异步记录流水表 + * @param userId + * @param amount + * @return + */ + R updateUserCredits(Integer userId, BigDecimal amount, TtAccountRecordSource source); + + /** + * 仅扣减积分,不动金币。 + * 商城购买等“纯积分消费”场景使用。 + * 仅支持扣款(amount<0),积分不足返回失败。异步记录流水。 + */ + R updateUserOnlyCredits(Integer userId, BigDecimal amount, TtAccountRecordSource source); + + /** + * 管理员通过「修改用户」调整余额/积分时,写入余额变动/积分变动流水(source=65/66)。 + * 只写流水、不动账户字段(账户字段由 updateUserById 统一更新)。 + * newUser 携带修改后的绝对余额/积分(为 null 表示该字段未修改)。 + * 操作人与登录 IP 写入流水 remark 以便审计。 + */ + void recordAdminBalanceCreditChange(TtUser newUser, String operatorName, String operatorIp); + + /** + * 用户删除时的级联解绑:递归将该用户所有下级(含下级的下级)的 parent_id 置为 null。 + * 招商删除→主播解绑→主播下普通用户也解绑; + * 主播删除→普通用户解绑; + * 普通用户删除→无下级,无操作。 + */ + void cascadeUnbindChildren(Integer userId); + + /** + * 清理指定客户端用户(api 端账号)的 Redis 登录态,并踢出 WebSocket 连接。 + * 停用、逻辑删除、真删除后必须调用:否则旧 Token 仍然有效, + * 已登录客户端会继续调接口(如 /api/getAccountAmount 空指针)且永远无法登出。 + */ + void kickOutLoginUsers(java.util.Collection userIds, String reason); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserTransferDailyService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserTransferDailyService.java new file mode 100644 index 0000000..3af74c4 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtUserTransferDailyService.java @@ -0,0 +1,109 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.transfer.TtUserTransferDaily; + +import java.time.LocalDate; + +public interface TtUserTransferDailyService extends IService { + + /** + * 获取或创建用户指定 VIP 等级今日的转赠次数记录 + * + * @param userId 用户ID + * @param vipLevel VIP等级 + * @param totalCount 该VIP等级今日转赠次数上限 + * @return 今日记录 + */ + TtUserTransferDaily getOrCreateToday(Integer userId, Integer vipLevel, Integer totalCount); + + /** + * 获取或创建用户指定 VIP 等级今日的记录,并初始化提货次数上限 + * + * @param userId 用户ID + * @param vipLevel VIP等级 + * @param deliveryTotalCount 该VIP等级今日提货次数上限 + * @return 今日记录 + */ + TtUserTransferDaily getOrCreateTodayWithDelivery(Integer userId, Integer vipLevel, Integer deliveryTotalCount); + + /** + * 提交转赠申请时预扣次数 + * + * @param userId 用户ID + * @param vipLevel 提交时的VIP等级 + * @param count 预扣数量 + * @return 是否预扣成功 + */ + boolean preDeduct(Integer userId, Integer vipLevel, Integer count); + + /** + * 提交提货申请时预扣次数 + * + * @param userId 用户ID + * @param vipLevel 提交时的VIP等级 + * @param count 预扣数量 + * @return 是否预扣成功 + */ + boolean preDeductDelivery(Integer userId, Integer vipLevel, Integer count); + + /** + * 审核通过:将预扣转为已用 + * + * @param userId 用户ID + * @param vipLevel 提交时的VIP等级 + * @param statDate 申请日期 + * @return 是否更新成功 + */ + boolean confirmUsed(Integer userId, Integer vipLevel, LocalDate statDate); + + /** + * 提货审核通过:将预扣转为已用 + * + * @param userId 用户ID + * @param vipLevel 提交时的VIP等级 + * @param statDate 申请日期 + * @return 是否更新成功 + */ + boolean confirmUsedDelivery(Integer userId, Integer vipLevel, LocalDate statDate); + + /** + * 审核驳回:返还预扣次数 + * + * @param userId 用户ID + * @param vipLevel 提交时的VIP等级 + * @param statDate 申请日期 + * @return 是否更新成功 + */ + boolean releasePending(Integer userId, Integer vipLevel, LocalDate statDate); + + /** + * 提货审核驳回:返还预扣次数 + * + * @param userId 用户ID + * @param vipLevel 提交时的VIP等级 + * @param statDate 申请日期 + * @return 是否更新成功 + */ + boolean releasePendingDelivery(Integer userId, Integer vipLevel, LocalDate statDate); + + /** + * 直接消耗次数(用于白号假转赠等不经过后台审核的场景) + * + * @param userId 用户ID + * @param vipLevel 当前VIP等级 + * @param count 消耗数量 + * @return 是否消耗成功 + */ + boolean consumeDirectly(Integer userId, Integer vipLevel, Integer count); + + /** + * 直接消耗提货次数(用于不经过后台审核的自动发货场景) + * + * @param userId 用户ID + * @param vipLevel 当前VIP等级 + * @param count 消耗数量 + * @return 是否消耗成功 + */ + boolean consumeDirectlyDelivery(Integer userId, Integer vipLevel, Integer count); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtVipLevelService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtVipLevelService.java new file mode 100644 index 0000000..a95a21c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtVipLevelService.java @@ -0,0 +1,12 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtVipLevel; + +public interface TtVipLevelService extends IService { + + String generateVipLevel(Integer num); + + String updateVipLevelById(TtVipLevel ttVipLevel); + void truncateVipLevel(); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtWelfareService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtWelfareService.java new file mode 100644 index 0000000..6ec7962 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtWelfareService.java @@ -0,0 +1,61 @@ +package com.ruoyi.admin.service; + +import java.util.List; +import com.ruoyi.domain.other.TtWelfare; + +/** + * 福利列表Service接口 + * + * @author ruoyi + * @date 2024-05-11 + */ +public interface TtWelfareService +{ + /** + * 查询福利列表 + * + * @param welfareId 福利列表主键 + * @return 福利列表 + */ + public TtWelfare selectTtWelfareByWelfareId(Integer welfareId); + + /** + * 查询福利列表列表 + * + * @param ttWelfare 福利列表 + * @return 福利列表集合 + */ + public List selectTtWelfareList(TtWelfare ttWelfare); + + /** + * 新增福利列表 + * + * @param ttWelfare 福利列表 + * @return 结果 + */ + public int insertTtWelfare(TtWelfare ttWelfare); + + /** + * 修改福利列表 + * + * @param ttWelfare 福利列表 + * @return 结果 + */ + public int updateTtWelfare(TtWelfare ttWelfare); + + /** + * 批量删除福利列表 + * + * @param welfareIds 需要删除的福利列表主键集合 + * @return 结果 + */ + public int deleteTtWelfareByWelfareIds(Integer[] welfareIds); + + /** + * 删除福利列表信息 + * + * @param welfareId 福利列表主键 + * @return 结果 + */ + public int deleteTtWelfareByWelfareId(Integer welfareId); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtYyOrnamentsService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtYyOrnamentsService.java new file mode 100644 index 0000000..66f5393 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TtYyOrnamentsService.java @@ -0,0 +1,7 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtYYOrnaments; + +public interface TtYyOrnamentsService extends IService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TurnoverLotteryService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TurnoverLotteryService.java new file mode 100644 index 0000000..3ee00f8 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/TurnoverLotteryService.java @@ -0,0 +1,48 @@ +package com.ruoyi.admin.service; + +import com.ruoyi.common.core.domain.R; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 流水抽奖服务接口 + */ +public interface TurnoverLotteryService { + + /** + * 保存抽奖奖品配置 + */ + R savePrizes(Long lotteryId, List> prizeList); + + /** + * 更新抽奖活动状态 + */ + R toggleStatus(Long lotteryId, Integer status); + + /** + * 获取用户今日流水统计和抽奖资格 + */ + R> getUserStats(Integer userId); + + /** + * 仅获取用户今日可用流水(供抽奖时轻量校验) + */ + BigDecimal getAvailableTurnover(Integer userId); + + /** + * 执行抽奖 + */ + R> draw(Integer userId, Long lotteryId); + + /** + * 查询用户自己的抽奖记录(分页) + */ + R> getUserRecords(Integer userId, Long lotteryId, Integer pageNum, Integer pageSize); + + /** + * 查询全服中奖记录(分页,已排除白号/机器人) + */ + R> getGlobalRecords(Long lotteryId, BigDecimal minPrice, Integer pageNum, Integer pageSize); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/UserIdGenerator.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/UserIdGenerator.java new file mode 100644 index 0000000..3e60f9a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/UserIdGenerator.java @@ -0,0 +1,54 @@ +package com.ruoyi.admin.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.mapper.ApiUserSubAccountMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.domain.entity.TtUserSubAccount; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; +import java.util.Objects; + +@Service +public class UserIdGenerator { + @Autowired + private TtUserMapper ttUserMapper; + @Autowired + private ApiUserSubAccountMapper subAccountMapper; + + + // 生成随机的八位数,不能与数据库中已有账号或副号绑定记录重复 + public Integer generateUserId() { + SecureRandom random = new SecureRandom(); + int maxRetries = 100; // 防止死循环的最大重试次数 + int retryCount = 0; + + while (retryCount < maxRetries) { + // 生成 10000000 到 99999999 之间的随机数 + int candidateId = 10000000 + random.nextInt(90000000); + + // 检查 tt_user 中是否存在该 ID + var userExists = ttUserMapper.selectTtUserById((long) candidateId); + if (Objects.nonNull(userExists)) { + retryCount++; + continue; + } + + // 检查 tt_user_sub_account 中是否已占用该 ID(真删除用户的副号记录可能残留) + Long subAccountCount = subAccountMapper.selectCount( + new LambdaQueryWrapper() + .eq(TtUserSubAccount::getSubUid, candidateId) + .or() + .eq(TtUserSubAccount::getMainUid, candidateId)); + if (subAccountCount != null && subAccountCount > 0) { + retryCount++; + continue; + } + + return candidateId; + } + + throw new RuntimeException("生成用户 ID 失败:达到最大重试次数,可能八位数 ID 已耗尽"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/WebsitePropertyService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/WebsitePropertyService.java new file mode 100644 index 0000000..86ec8ae --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/WebsitePropertyService.java @@ -0,0 +1,18 @@ +package com.ruoyi.admin.service; + +import com.ruoyi.domain.vo.WebsitePropertyDataVO; + +import java.util.List; + +public interface WebsitePropertyService { + + List list(); + + WebsitePropertyDataVO getById(Integer id); + + String save(WebsitePropertyDataVO websitePropertyDataVO); + + String updateWebsitePropertyById(WebsitePropertyDataVO websitePropertyDataVO); + + int deleteWebsitePropertyByIds(Integer[] ids); +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/WebsiteSetupService.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/WebsiteSetupService.java new file mode 100644 index 0000000..b79ff04 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/WebsiteSetupService.java @@ -0,0 +1,15 @@ +package com.ruoyi.admin.service; + +import com.ruoyi.domain.other.OperationalStatistics; +import com.ruoyi.domain.other.ParameterSettingBody; + +import java.util.List; + +public interface WebsiteSetupService { + + List getOperationalStatistics(); + ParameterSettingBody getParameterSetting(); + + String updateParameterSetting(ParameterSettingBody parameterSettingBody); + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/ConfigServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/ConfigServiceImpl.java new file mode 100644 index 0000000..7bc0b49 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/ConfigServiceImpl.java @@ -0,0 +1,11 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.ConfigData; +import com.ruoyi.admin.mapper.ConfigMapper; +import com.ruoyi.admin.service.ConfigService; +import org.springframework.stereotype.Service; + +@Service +public class ConfigServiceImpl extends ServiceImpl implements ConfigService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/PromoCdkeyRedeemServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/PromoCdkeyRedeemServiceImpl.java new file mode 100644 index 0000000..c002545 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/PromoCdkeyRedeemServiceImpl.java @@ -0,0 +1,22 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.entity.PromoCdkeyRedeem; +import com.ruoyi.admin.mapper.AdminCdkeyRedeemMapper; +import com.ruoyi.admin.service.PromoCdkeyRedeemService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class PromoCdkeyRedeemServiceImpl extends ServiceImpl implements PromoCdkeyRedeemService { + + @Override + public List queryList(PromoCdkeyRedeem query) { + return baseMapper.selectRedeemListWithCreator( + query.getCdkeyCode(), + query.getRedeemerId(), + query.getRedeemerName(), + query.getCreatorId()); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/ShoppingServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/ShoppingServiceImpl.java new file mode 100644 index 0000000..5b55af4 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/ShoppingServiceImpl.java @@ -0,0 +1,84 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.github.pagehelper.PageInfo; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.admin.service.ShoppingService; +import com.ruoyi.domain.other.ShoppingBody; +import com.ruoyi.domain.vo.ShoppingDataVO; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.system.service.ISysConfigService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class ShoppingServiceImpl implements ShoppingService { + + private final ISysConfigService sysConfigService; + private final TtOrnamentMapper ornamentsMapper; + + public ShoppingServiceImpl(ISysConfigService sysConfigService, + TtOrnamentMapper ornamentsMapper) { + this.sysConfigService = sysConfigService; + this.ornamentsMapper = ornamentsMapper; + } + + @Override + public PageDataInfo list(ShoppingBody shoppingBody) { + + LambdaQueryChainWrapper wrapper = new LambdaQueryChainWrapper<>(ornamentsMapper); + if (StringUtils.isNotNull(shoppingBody.getItemName())) + wrapper.and(w -> w.like(TtOrnament::getName, shoppingBody.getItemName()) + .or().like(TtOrnament::getShortName, shoppingBody.getItemName())); + if (ObjectUtils.isNotEmpty(shoppingBody.getId())) wrapper.eq(TtOrnament::getId, shoppingBody.getId()); + if (StringUtils.isNotNull(shoppingBody.getExterior())) + wrapper.eq(TtOrnament::getExterior, shoppingBody.getExterior()); + if (StringUtils.isNotNull(shoppingBody.getType())) wrapper.eq(TtOrnament::getType, shoppingBody.getType()); + if (StringUtils.isNotNull(shoppingBody.getMinPrice())) + wrapper.ge(TtOrnament::getStorePrice, shoppingBody.getMinPrice()); + if (StringUtils.isNotNull(shoppingBody.getMaxPrice())) + wrapper.le(TtOrnament::getStorePrice, shoppingBody.getMaxPrice()); + if (StringUtils.isNotNull(shoppingBody.getIsPutaway())) + wrapper.eq(TtOrnament::getIsPutaway, shoppingBody.getIsPutaway()); + // wrapper.eq(TtOrnament::getIsProprietaryProperty, "1"); + // market_hash_name 为空的饰品无法第三方提货、不能上架售卖,商城列表直接不展示(此类饰品可在“饰品列表”页查看/删除) + wrapper.isNotNull(TtOrnament::getMarketHashName) + .ne(TtOrnament::getMarketHashName, ""); + // 商城价格(store_price)为0或NULL的饰品不展示 + wrapper.isNotNull(TtOrnament::getStorePrice) + .gt(TtOrnament::getStorePrice, BigDecimal.ZERO); + // 图片(image_url)为空的饰品不展示 + wrapper.isNotNull(TtOrnament::getImageUrl) + .ne(TtOrnament::getImageUrl, ""); + wrapper.orderByAsc(TtOrnament::getIsPutaway); + + List list = wrapper.list(); + + String exchangePriceRatio = sysConfigService.selectConfigByKey("exchangePriceRatio"); + List resultList = list.stream().map(ttOrnaments -> { + ShoppingDataVO shoppingDataVO = ShoppingDataVO.builder().build(); + BeanUtils.copyBeanProp(shoppingDataVO, ttOrnaments); + // 将数据库的 name (长名称) 映射到 VO 的 name 字段 + shoppingDataVO.setName(ttOrnaments.getName()); + shoppingDataVO.setUseCredits(ObjectUtil.isEmpty(ttOrnaments.getUsePrice()) ? BigDecimal.ZERO : ttOrnaments.getUsePrice().multiply(new BigDecimal(exchangePriceRatio))); + return shoppingDataVO; + }).collect(Collectors.toList()); + PageDataInfo pageDataInfo = new PageDataInfo<>(); + pageDataInfo.setCode(HttpStatus.SUCCESS); + pageDataInfo.setMsg("查询成功"); + pageDataInfo.setRows(resultList); + pageDataInfo.setTotal(new PageInfo<>(list).getTotal()); + return pageDataInfo; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtAdvertisementServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtAdvertisementServiceImpl.java new file mode 100644 index 0000000..bbbde1b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtAdvertisementServiceImpl.java @@ -0,0 +1,21 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtAdvertisementMapper; +import com.ruoyi.admin.service.TtAdvertisementService; +import com.ruoyi.domain.other.TtAdvertisement; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class TtAdvertisementServiceImpl extends ServiceImpl implements TtAdvertisementService { + + @Override + public String updatePicById(TtAdvertisement ttAdvertisement) { + // String picture = ttBanner.getPicture(); + // ttBanner.setPicture(RuoYiConfig.getDomainName() + picture); + if (this.updateById(ttAdvertisement)) return ""; + return "修改banner数据异常!"; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtAnnouncementServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtAnnouncementServiceImpl.java new file mode 100644 index 0000000..67930cb --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtAnnouncementServiceImpl.java @@ -0,0 +1,62 @@ +package com.ruoyi.admin.service.impl; + +import com.ruoyi.admin.mapper.TtAnnouncementMapper; +import com.ruoyi.admin.service.TtAnnouncementService; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.other.TtAnnouncement; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; + +@Service +public class TtAnnouncementServiceImpl implements TtAnnouncementService { + + @Autowired + private TtAnnouncementMapper ttAnnouncementMapper; + + @Override + public List getAnnouncementList(TtAnnouncement query) { + return ttAnnouncementMapper.getAnnouncementList(query); + } + + @Override + public TtAnnouncement getAnnouncementByAnnouncementId(Long userId, Integer announcementId) { + return ttAnnouncementMapper.getAnnouncementByAnnouncementId(announcementId); + } + + @Override + public int addAnnouncement(TtAnnouncement ttAnnouncement) { + // 系统公告全局只允许存在一条 + if (ttAnnouncement.getType() != null && ttAnnouncement.getType() == 1 + && ttAnnouncementMapper.countSystemAnnouncement() > 0) { + throw new ServiceException("系统公告已存在!!!"); + } + // 设置消息分类:系统公告 or 个人公告 + String category = (ttAnnouncement.getType() != null && ttAnnouncement.getType() == 1) + ? MessageCategoryEnum.SYS_ANNOUNCEMENT.getCode() + : MessageCategoryEnum.SYS_PERSONAL.getCode(); + ttAnnouncement.setCategory(category); + ttAnnouncement.setCreateTime(new Date()); + return ttAnnouncementMapper.addAnnouncement(ttAnnouncement); + } + + @Override + public int editAnnouncement(TtAnnouncement ttAnnouncement) { + // 编辑时同步修正分类 + if (ttAnnouncement.getType() != null) { + String category = (ttAnnouncement.getType() == 1) + ? MessageCategoryEnum.SYS_ANNOUNCEMENT.getCode() + : MessageCategoryEnum.SYS_PERSONAL.getCode(); + ttAnnouncement.setCategory(category); + } + return ttAnnouncementMapper.editAnnouncement(ttAnnouncement); + } + + @Override + public int removeAnnouncementByAnnouncementId(Integer announcementId) { + return ttAnnouncementMapper.removeAnnouncementByAnnouncementId(announcementId); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBannerServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBannerServiceImpl.java new file mode 100644 index 0000000..d0ee876 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBannerServiceImpl.java @@ -0,0 +1,23 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtBanner; +import com.ruoyi.admin.mapper.TtBannerMapper; +import com.ruoyi.admin.service.TtBannerService; +import com.ruoyi.common.config.RuoYiConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; + +@Service +@Slf4j +public class TtBannerServiceImpl extends ServiceImpl implements TtBannerService { + + @Override + public String updateBannerById(TtBanner ttBanner) { + // String picture = ttBanner.getPicture(); + // ttBanner.setPicture(RuoYiConfig.getDomainName() + picture); + if (this.updateById(ttBanner)) return ""; + return "修改banner数据异常!"; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBonusReceiveRecordServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBonusReceiveRecordServiceImpl.java new file mode 100644 index 0000000..b373cd0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBonusReceiveRecordServiceImpl.java @@ -0,0 +1,11 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtBonusReceiveRecord; +import com.ruoyi.admin.mapper.TtBonusReceiveRecordMapper; +import com.ruoyi.admin.service.TtBonusReceiveRecordService; +import org.springframework.stereotype.Service; + +@Service +public class TtBonusReceiveRecordServiceImpl extends ServiceImpl implements TtBonusReceiveRecordService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBonusServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBonusServiceImpl.java new file mode 100644 index 0000000..d059955 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBonusServiceImpl.java @@ -0,0 +1,370 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtBonusMapper; +import com.ruoyi.admin.mapper.TtNoticeMapper; +import com.ruoyi.admin.mapper.TtPromotionRecordMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.*; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtPromotionLevel; +import com.ruoyi.domain.entity.TtPromotionRecord; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class TtBonusServiceImpl extends ServiceImpl implements TtBonusService { + + private final TtUserService userService; + private final TtVipLevelService vipLevelService; + private final TtPromotionLevelService promotionLevelService; + private final TtRechargeRecordMapper rechargeRecordMapper; + private final TtPromotionRecordMapper promotionRecordMapper; + private final TtUserBlendErcashMapper ttUserBlendErcashMapper; + private final TtBonusReceiveRecordService bonusReceiveRecordService; + private final TtNoticeMapper ttNoticeMapper; + + public TtBonusServiceImpl(TtUserService userService, + TtVipLevelService vipLevelService, + TtPromotionLevelService promotionLevelService, + TtRechargeRecordMapper rechargeRecordMapper, + TtPromotionRecordMapper promotionRecordMapper, + TtUserBlendErcashMapper ttUserBlendErcashMapper, + TtBonusReceiveRecordService bonusReceiveRecordService, + TtNoticeMapper ttNoticeMapper) { + this.userService = userService; + this.vipLevelService = vipLevelService; + this.promotionLevelService = promotionLevelService; + this.rechargeRecordMapper = rechargeRecordMapper; + this.promotionRecordMapper = promotionRecordMapper; + this.ttUserBlendErcashMapper = ttUserBlendErcashMapper; + this.bonusReceiveRecordService = bonusReceiveRecordService; + this.ttNoticeMapper = ttNoticeMapper; + } + + /** + * 充值返佣(供第三方支付回调复用):按各级上级 commissionRate 逐级向上实时返佣, + * 招商账号跳过实时(走周结)。复用 distributeRechargeCommissionRecursive, + * 从充值用户本身起递归(depth=0 即先处理直属上级),写 source=13。 + */ + @Override + public void distributeRechargeCommission(Integer userId, BigDecimal amountActuallyPaid) { + if (userId == null || amountActuallyPaid == null + || amountActuallyPaid.compareTo(BigDecimal.ZERO) <= 0) { + return; + } + TtUser ttUser = userService.getById(userId); + if (ttUser == null || Objects.isNull(ttUser.getParentId())) { + return; + } + // 白号充值不触发上级返佣 + if (Integer.valueOf(1).equals(ttUser.getIsBlankAccount())) { + return; + } + distributeRechargeCommissionRecursive(ttUser.getUserId(), amountActuallyPaid, 0); + } + + @Override + public String updateBonusById(TtBonus ttBonus) { + String coverPicture = ttBonus.getCoverPicture(); + ttBonus.setCoverPicture(RuoYiConfig.getDomainName() + coverPicture); + this.updateById(ttBonus); + return ""; + } + + @Override + public void bonus(Integer userId, Integer rechargeRecordId) { + + List rechargeRecords = new LambdaQueryChainWrapper<>(rechargeRecordMapper) + .eq(TtRechargeRecord::getUserId, userId) + .eq(TtRechargeRecord::getStatus, "0") + .list(); + BigDecimal priceTotal = rechargeRecords.stream() + .map(TtRechargeRecord::getAmountActuallyPaid) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + TtUser ttUser = userService.getById(userId); + TtRechargeRecord ttRechargeRecord = rechargeRecordMapper.selectById(rechargeRecordId); + BigDecimal amountActuallyPaid = ttRechargeRecord.getAmountActuallyPaid(); + + // 白号充值不触发上级佣金和返佣 + boolean isBlankAccount = Integer.valueOf(1).equals(ttUser.getIsBlankAccount()); + if (!isBlankAccount && !Objects.isNull(ttUser.getParentId())) { + TtUser parentUser = userService.getById(ttUser.getParentId()); + if (insertParentUserPromotionRecord(ttUser.getUserId(), parentUser, amountActuallyPaid, rechargeRecordId)) { + // 项目暂无推广等级,不触发等级升级逻辑 + // updateParentUserPromotionLevel(parentUser); + } + // 递归向上分发充值佣金给更高层级祖先(从第2层开始,直属上级已由上面处理) + distributeRechargeCommissionRecursive(parentUser.getUserId(), amountActuallyPaid, 1); + } + if (ttUser.getVipLevel() > 0) { + TtVipLevel ttVipLevel = vipLevelService.getById(ttUser.getVipLevel()); + BigDecimal rebate = ttVipLevel.getCommissions().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP) + .multiply(amountActuallyPaid).setScale(2, RoundingMode.HALF_UP); + + userService.updateUserAccount(userId, rebate, TtAccountRecordSource.RECHARGE_AWARD); + } + + List bonusList = new LambdaQueryChainWrapper<>(getBaseMapper()).eq(TtBonus::getStatus, "0").list(); + List bonusReceiveRecordList = new LambdaQueryChainWrapper<>(bonusReceiveRecordService.getBaseMapper()) + .eq(TtBonusReceiveRecord::getUserId, userId) + .eq(TtBonusReceiveRecord::getType, "2") + .list(); + List bonusIds = bonusReceiveRecordList.stream().map(TtBonusReceiveRecord::getBonusId).collect(Collectors.toList()); + List insertList = new ArrayList<>(); + for (TtBonus bonus : bonusList) { + if ("2".equals(bonus.getType())) { + if ("0".equals(bonus.getConditionType())) { + + } else if ("1".equals(bonus.getConditionType())) { + + } else if ("2".equals(bonus.getConditionType())) { + + } else if ("3".equals(bonus.getConditionType())) { + BigDecimal rechargeThreshold = bonus.getRechargeThreshold(); + if (!bonusIds.contains(bonus.getId()) && rechargeThreshold.compareTo(priceTotal) < 0) { + TtBonusReceiveRecord bonusReceiveRecord = TtBonusReceiveRecord.builder().build(); + bonusReceiveRecord.setType("2"); + bonusReceiveRecord.setBonusId(bonus.getId()); + bonusReceiveRecord.setUserId(userId); + bonusReceiveRecord.setAwardType("0"); + bonusReceiveRecord.setAwardPrice(RandomUtils.getRandomPrice(bonus.getAwardSection())); + bonusReceiveRecord.setCreateTime(DateUtils.getNowDate()); + insertList.add(bonusReceiveRecord); + } + } + } else if ("3".equals(bonus.getType())) { + if ("0".equals(bonus.getConditionType())) { + + } else if ("1".equals(bonus.getConditionType())) { + + } else if ("2".equals(bonus.getConditionType())) { + + } else if ("3".equals(bonus.getConditionType())) { + + } + } + } + bonusReceiveRecordService.saveBatch(insertList, 1); + } + + /** + * 主页分享 - 邀请新人充值返佣: + * 当下级用户充值时,按上级用户的 commissionRate(后台“邀请新人充值比例”字段, + * 取值区间 0.01~0.1,即 1%~10%)实时计算返利并直接打入上级账户。 + */ + private boolean insertParentUserPromotionRecord(Integer userId, TtUser parentUser, BigDecimal amountActuallyPaid, Integer rechargeRecordId) { + TtPromotionRecord ttPromotionRecord = TtPromotionRecord.builder().build(); + ttPromotionRecord.setUserId(parentUser.getUserId()); + ttPromotionRecord.setSubordinateUserId(userId); + ttPromotionRecord.setRechargePrice(amountActuallyPaid); + ttPromotionRecord.setRechargeRecordId(rechargeRecordId); + ttPromotionRecord.setCreateTime(DateUtils.getNowDate()); + + BigDecimal commissionRate = parentUser.getCommissionRate(); + BigDecimal rebate = BigDecimal.ZERO; + if (commissionRate != null + && commissionRate.compareTo(BigDecimal.ZERO) > 0 + && amountActuallyPaid != null + && amountActuallyPaid.compareTo(BigDecimal.ZERO) > 0) { + // commissionRate 在后台填写的是百分比数值(如 2 代表 2%,0.5 代表 0.5%),统一除以 100 + BigDecimal rate = commissionRate.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP); + rebate = amountActuallyPaid.multiply(rate).setScale(2, RoundingMode.HALF_UP); + } + ttPromotionRecord.setRebate(rebate); + + boolean inserted = promotionRecordMapper.insert(ttPromotionRecord) > 0; + // 招商账号改为周结,不再实时到账;其他账号实时把返利打入上级账户(推广福利收入) + if (inserted && rebate.compareTo(BigDecimal.ZERO) > 0 + && !UserType.BUSINESS_USER.getCode().equals(parentUser.getUserType())) { + userService.updateUserAccount(parentUser.getUserId(), rebate, TtAccountRecordSource.PROMOTION_CHARGE_COMMISSION); + // 发送返利到账通知 + sendRebateNotice(parentUser.getUserId().longValue(), rebate, amountActuallyPaid); + } + return inserted; + } + + /** + * 递归向上分发充值佣金。 + * 从指定用户的父级开始,按各祖先的 commissionRate 计算佣金并存入金币余额。 + * 最多向上5层(加上直属上级共5层)。 + */ + private void distributeRechargeCommissionRecursive(Integer userId, BigDecimal amountActuallyPaid, int depth) { + if (depth >= 5) return; + TtUser user = userService.getById(userId); + if (user == null || user.getParentId() == null) { + log.info("[充值佣金递归] depth={} userId={} 终止: user={}", depth, userId, user == null ? "null" : "parentId=null"); + return; + } + + TtUser ancestor = userService.getById(user.getParentId()); + if (ancestor == null) { + log.info("[充值佣金递归] depth={} userId={} parentId={} ancestor不存在", depth, userId, user.getParentId()); + return; + } + + BigDecimal commissionRate = ancestor.getCommissionRate(); + log.info("[充值佣金递归] depth={} userId={} ancestorId={} ancestorType={} commissionRate={} amount={}", + depth, userId, ancestor.getUserId(), ancestor.getUserType(), commissionRate, amountActuallyPaid); + + // 招商账号改为周结,跳过该层实时到账,继续向上递归 + if (UserType.BUSINESS_USER.getCode().equals(ancestor.getUserType())) { + distributeRechargeCommissionRecursive(ancestor.getUserId(), amountActuallyPaid, depth + 1); + return; + } + + if (commissionRate != null && commissionRate.compareTo(BigDecimal.ZERO) > 0 + && amountActuallyPaid != null && amountActuallyPaid.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal rate = commissionRate.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP); + BigDecimal rebate = amountActuallyPaid.multiply(rate).setScale(2, RoundingMode.HALF_UP); + if (rebate.compareTo(BigDecimal.ZERO) > 0) { + try { + Object result = userService.updateUserAccount(ancestor.getUserId(), rebate, TtAccountRecordSource.PROMOTION_CHARGE_COMMISSION); + log.info("[充值佣金递归] ancestorId={} rebate={} 入账结果: {}", + ancestor.getUserId(), rebate, result); + sendRebateNotice(ancestor.getUserId().longValue(), rebate, amountActuallyPaid); + } catch (Exception e) { + log.error("[充值佣金递归分发异常] ancestorId={}, rebate={}, depth={}", ancestor.getUserId(), rebate, depth, e); + } + } + } + + // 继续向上递归 + distributeRechargeCommissionRecursive(ancestor.getUserId(), amountActuallyPaid, depth + 1); + } + + /** + * 发送返利到账通知到 tt_notice 表 + */ + private void sendRebateNotice(Long userId, BigDecimal rebate, BigDecimal rechargeAmount) { + TtNotice notice = new TtNotice(); + notice.setUserId(userId); + notice.setTitle("返利到账通知"); + notice.setContent("您的下级用户充值" + rechargeAmount.toPlainString() + + ",您获得返利" + rebate.toPlainString() + "已到账,请注意查收。"); + notice.setRead("0"); + notice.setCreateTime(DateUtils.getNowDate()); + ttNoticeMapper.insert(notice); + } + + /* + private List getLevelIdList(int level, List levelIds) { + List levelList = new ArrayList<>(); + for (int i = 1; i <= level; i++) { + levelList.add(i); + } + levelList.removeAll(levelIds); + return levelList; + } + */ + + /* + private void updateParentUserPromotionLevel(TtUser parentUser) { + List promotionRecordList = new LambdaQueryChainWrapper<>(promotionRecordMapper) + .eq(TtPromotionRecord::getUserId, parentUser.getUserId()) + .list(); + List promotionLevelList = promotionLevelService.list(); + BigDecimal rechargePriceTotal = promotionRecordList.stream().map(TtPromotionRecord::getRechargePrice).reduce(BigDecimal.ZERO, BigDecimal::add); + List promotionLevelPriceList = promotionLevelList.stream().map(TtPromotionLevel::getRechargeThreshold).collect(Collectors.toList()); + int promotionLevel = this.getLevel(rechargePriceTotal, promotionLevelPriceList); + // 如果用户的推广等级发生变化,更新用户的推广等级 + if (parentUser.getPromotionLevel() != promotionLevel) { + parentUser.setPromotionLevel(promotionLevel); + userService.updateById(parentUser); + List bonusReceiveRecordList = new LambdaQueryChainWrapper<>(bonusReceiveRecordService.getBaseMapper()) + .eq(TtBonusReceiveRecord::getUserId, parentUser.getUserId()) + .eq(TtBonusReceiveRecord::getType, "1") + .list(); + List promotionLevelIds = bonusReceiveRecordList.stream().map(TtBonusReceiveRecord::getPromotionLevelId).collect(Collectors.toList()); + List promotionLevelIdList = getLevelIdList(promotionLevel, promotionLevelIds); + if (!promotionLevelIdList.isEmpty()) { + List bonusReceiveRecords = new ArrayList<>(); + for (TtPromotionLevel ttPromotionLevel : promotionLevelService.getBaseMapper().selectBatchIds(promotionLevelIdList)) { + if (BigDecimal.ZERO.compareTo(ttPromotionLevel.getAddedBonus()) < 0) { + TtBonusReceiveRecord bonusReceiveRecord = TtBonusReceiveRecord.builder().build(); + bonusReceiveRecord.setType("1"); + bonusReceiveRecord.setPromotionLevelId(ttPromotionLevel.getId()); + bonusReceiveRecord.setUserId(parentUser.getUserId()); + bonusReceiveRecord.setAwardType("0"); + bonusReceiveRecord.setAwardPrice(ttPromotionLevel.getAddedBonus()); + bonusReceiveRecord.setCreateTime(DateUtils.getNowDate()); + bonusReceiveRecords.add(bonusReceiveRecord); + } + } + bonusReceiveRecordService.saveBatch(bonusReceiveRecords, 1); + } + // TODO 发放等级达标奖励红包 + distributePromotionLevelBonus(parentUser.getPromotionLevel(), parentUser.getUserId()); + } + } + */ + + /* + private int getLevel(BigDecimal price, List levelPriceList) { + levelPriceList.sort(Comparator.naturalOrder()); + for (int i = 0; i < levelPriceList.size(); i++) { + if (price.compareTo(levelPriceList.get(i)) < 0) { + return i; + } + } + return levelPriceList.size(); + } + */ + + /** + * 发放等级达标奖励红包 + */ + /* + private void distributePromotionLevelBonus(Integer promotionLevel, Integer userId) { + // 根据新等级查询红包金额 + TtPromotionLevel ttPromotionLevel = promotionLevelService.getById(promotionLevel); + BigDecimal addedBonus = ttPromotionLevel.getAddedBonus(); + + // 加钱 + TtUser user = userService.getById(userId); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_credits = account_credits + " + addedBonus); + userService.update(userUpdate); + + // 综合消费日志 + TtUser ttUser = userService.getById(user.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + .credits(addedBonus.compareTo(BigDecimal.ZERO) > 0 ? addedBonus : null) + .finalCredits(addedBonus.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountCredits().add(addedBonus) : null) + .total(addedBonus.add(addedBonus)) // 收支合计 + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.PROMOTION_LEVEL_REACH.getCode()) + .remark(TtAccountRecordSource.PROMOTION_LEVEL_REACH.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + ttUserBlendErcashMapper.insert(blendErcash); + } + */ +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxOpenChanceServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxOpenChanceServiceImpl.java new file mode 100644 index 0000000..06748f6 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxOpenChanceServiceImpl.java @@ -0,0 +1,93 @@ +package com.ruoyi.admin.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.admin.mapper.TtBoxOpenChanceMapper; +import com.ruoyi.admin.domain.TtBoxOpenChance; +import com.ruoyi.admin.service.TtBoxOpenChanceService; + +/** + * 开箱机会Service业务层处理 + * + * @author ruoyi + * @date 2024-07-09 + */ +@Service +public class TtBoxOpenChanceServiceImpl implements TtBoxOpenChanceService +{ + @Autowired + private TtBoxOpenChanceMapper ttBoxOpenChanceMapper; + + /** + * 查询开箱机会 + * + * @param ornamentId 开箱机会主键 + * @return 开箱机会 + */ + @Override + public TtBoxOpenChance selectTtBoxOpenChanceByOrnamentId(Integer ornamentId) + { + return ttBoxOpenChanceMapper.selectTtBoxOpenChanceByOrnamentId(ornamentId); + } + + /** + * 查询开箱机会列表 + * + * @param ttBoxOpenChance 开箱机会 + * @return 开箱机会 + */ + @Override + public List selectTtBoxOpenChanceList(TtBoxOpenChance ttBoxOpenChance) + { + return ttBoxOpenChanceMapper.selectTtBoxOpenChanceList(ttBoxOpenChance); + } + + /** + * 新增开箱机会 + * + * @param ttBoxOpenChance 开箱机会 + * @return 结果 + */ + @Override + public int insertTtBoxOpenChance(TtBoxOpenChance ttBoxOpenChance) + { + return ttBoxOpenChanceMapper.insertTtBoxOpenChance(ttBoxOpenChance); + } + + /** + * 修改开箱机会 + * + * @param ttBoxOpenChance 开箱机会 + * @return 结果 + */ + @Override + public int updateTtBoxOpenChance(TtBoxOpenChance ttBoxOpenChance) + { + return ttBoxOpenChanceMapper.updateTtBoxOpenChance(ttBoxOpenChance); + } + + /** + * 批量删除开箱机会 + * + * @param ornamentIds 需要删除的开箱机会主键 + * @return 结果 + */ + @Override + public int deleteTtBoxOpenChanceByOrnamentIds(Integer[] ornamentIds) + { + return ttBoxOpenChanceMapper.deleteTtBoxOpenChanceByOrnamentIds(ornamentIds); + } + + /** + * 删除开箱机会信息 + * + * @param ornamentId 开箱机会主键 + * @return 结果 + */ + @Override + public int deleteTtBoxOpenChanceByOrnamentId(Integer ornamentId) + { + return ttBoxOpenChanceMapper.deleteTtBoxOpenChanceByOrnamentId(ornamentId); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxOrnamentsServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxOrnamentsServiceImpl.java new file mode 100644 index 0000000..0d95662 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxOrnamentsServiceImpl.java @@ -0,0 +1,624 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.controller.TtBoxOrnamentsController.batchAddParam; +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.domain.dto.boxRecords.TtBoxRecordsNum; +import com.ruoyi.domain.dto.boxRecords.TtBoxRecordsRemark; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtBoxOrnaments; +import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper; +import com.ruoyi.admin.service.TtBoxOrnamentsService; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.domain.vo.box.BoxGlobalData; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +public class TtBoxOrnamentsServiceImpl extends ServiceImpl implements TtBoxOrnamentsService { + + @Autowired + private TtBoxService boxService; + + @Autowired + private TtBoxMapper boxMapper; + + @Autowired + private TtBoxRecordsMapper ttBoxRecordsMapper; + + @Autowired + private RedisCache redisCache; + + @Autowired + private LotteryMachine lotteryMachine; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + // 设置日期转换为字符串格式 + private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + static { + sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); + } + + /** + * 检查饰品是否已被其他宝箱使用 (实现饰品独占机制) + * + * @param ornamentId 饰品 ID + * @param boxId 宝箱 ID(用于更新场景排除自己) + * @return 空 = 通过,非空 = 错误提示 + */ + public String checkOrnamentUnique(Long ornamentId, Integer boxId) { + if (ornamentId == null) { + return "饰品 ID 不能为空"; + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtBoxOrnaments::getOrnamentId, ornamentId); + + if (boxId != null) { + wrapper.ne(TtBoxOrnaments::getBoxId, boxId); // 排除当前宝箱 + } + + List existing = list(wrapper); + // 仅当存在"有效宝箱"(未删除)持有该饰品时才拦截; + // 指向已删除/不存在的宝箱关联数据跳过,避免 NPE + for (TtBoxOrnaments item : existing) { + Integer existBoxId = item.getBoxId(); + // 直接用 queryWrapper 只取未删除的宝箱记录 + TtBox existBox = boxService.getOne( + new LambdaQueryWrapper() + .eq(TtBox::getBoxId, existBoxId) + .eq(TtBox::getDelFlag, "0") + ); + if (existBox != null) { + String boxName = existBox.getBoxName() == null ? "未知" : existBox.getBoxName(); + return String.format("饰品 (id=%s) 已被宝箱【%s】(%d) 使用,无法添加到其他宝箱", + ornamentId.toString(), + boxName, + existBoxId); + } + } + + return ""; + } + + @Override + public List selectTtBoxOrnamentsList(Integer boxId) { + // 宝箱内物品明细 + List boxOrnaments = baseMapper.selectTtBoxOrnamentsList(boxId); + fillRemainingOdds(boxId, boxOrnaments); + return boxOrnaments; + } + + /** + * 根据开箱记录计算各类奖池的剩余库存 + */ + private void fillRemainingOdds(Integer boxId, List boxOrnaments) { + if (CollectionUtils.isEmpty(boxOrnaments)) { + return; + } + // 解析每个饰品的统计起始时间(real_odds 变更时间),并按起始时间分组, + // 避免逐个饰品查询大表 tt_box_records 造成 N+1 慢查询。 + // beginTime 仅在真实爆率被修改过时才有值,绝大多数饰品为 null, + // 分组后通常只需 1 次批量查询即可覆盖全部饰品。 + Map> beginTimeToOrnamentIds = new HashMap<>(); + for (TtBoxOrnamentsDataVO boxOrnament : boxOrnaments) { + if (boxOrnament == null || boxOrnament.getOrnamentId() == null) { + continue; + } + String beginTime = null; + if (StringUtils.isNotEmpty(boxOrnament.getRemark())) { + TtBoxRecordsRemark remark = JSONUtil.toBean(boxOrnament.getRemark(), TtBoxRecordsRemark.class); + if (remark != null) { + beginTime = remark.getRealOddsChange(); + } + } + // 以空串作为“无起始时间”的分组Key,规避 HashMap null key 的歧义 + String groupKey = beginTime == null ? "" : beginTime; + beginTimeToOrnamentIds.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(boxOrnament.getOrnamentId()); + } + Map boxRecordsMap = new HashMap<>(); + for (Map.Entry> entry : beginTimeToOrnamentIds.entrySet()) { + List ornamentIds = entry.getValue(); + if (CollectionUtils.isEmpty(ornamentIds)) { + continue; + } + String beginTime = entry.getKey().isEmpty() ? null : entry.getKey(); + // 同一起始时间的所有饰品一次性批量统计,语义与逐个查询完全一致 + List boxRecordsNums = ttBoxRecordsMapper.selectBoxRecordsByOrnamentIds(boxId, ornamentIds, beginTime); + if (CollectionUtils.isNotEmpty(boxRecordsNums)) { + for (TtBoxRecordsNum num : boxRecordsNums) { + if (num == null || num.getOrnamentId() == null) { + continue; + } + boxRecordsMap.put(num.getOrnamentId(), num); + } + } + } + for (TtBoxOrnamentsDataVO boxOrnament : boxOrnaments) { + if (boxOrnament == null || boxOrnament.getOrnamentId() == null) { + continue; + } + // 各列剩余按开箱者类型分开统计,对齐 LotteryMachine 的分池消耗: + // 主播爆率列=仅主播(01),真实爆率列=其余非主播非机器人(普通/招商/卡商/超管等), + // 机器人三列按对战模式拆分:欧皇(model=0)/非酋(model=1)/其余走普通机器人列 + TtBoxRecordsNum recordsNum = boxRecordsMap.get(boxOrnament.getOrnamentId()); + int realNum = recordsNum == null || recordsNum.getRealNum() == null ? 0 : recordsNum.getRealNum(); + int anchorNum = recordsNum == null || recordsNum.getAnchorNum() == null ? 0 : recordsNum.getAnchorNum(); + int robotNum = recordsNum == null || recordsNum.getRobotNum() == null ? 0 : recordsNum.getRobotNum(); + int robotLuckyNum = recordsNum == null || recordsNum.getRobotLuckyNum() == null ? 0 : recordsNum.getRobotLuckyNum(); + int robotUnluckyNum = recordsNum == null || recordsNum.getRobotUnluckyNum() == null ? 0 : recordsNum.getRobotUnluckyNum(); + int whiteNum = recordsNum == null || recordsNum.getWhiteNum() == null ? 0 : recordsNum.getWhiteNum(); + if (boxOrnament.getRealOdds() != null && boxOrnament.getRealOdds() > 0) { + boxOrnament.setRealRemainingOdds(boxOrnament.getRealOdds() - (realNum % boxOrnament.getRealOdds())); + } else { + boxOrnament.setRealRemainingOdds(0); + } + if (boxOrnament.getAnchorOdds() != null && boxOrnament.getAnchorOdds() > 0) { + boxOrnament.setAnchorRemainingOdds(boxOrnament.getAnchorOdds() - (anchorNum % boxOrnament.getAnchorOdds())); + } else { + boxOrnament.setAnchorRemainingOdds(0); + } + if (boxOrnament.getRobotOdds() != null && boxOrnament.getRobotOdds() > 0) { + boxOrnament.setRobotRemainingOdds(boxOrnament.getRobotOdds() - (robotNum % boxOrnament.getRobotOdds())); + } else { + boxOrnament.setRobotRemainingOdds(0); + } + if (boxOrnament.getRobotOddsLucky() != null && boxOrnament.getRobotOddsLucky() > 0) { + boxOrnament.setRobotLuckyRemainingOdds(boxOrnament.getRobotOddsLucky() - (robotLuckyNum % boxOrnament.getRobotOddsLucky())); + } else { + boxOrnament.setRobotLuckyRemainingOdds(0); + } + if (boxOrnament.getRobotOddsUnlucky() != null && boxOrnament.getRobotOddsUnlucky() > 0) { + boxOrnament.setRobotUnluckyRemainingOdds(boxOrnament.getRobotOddsUnlucky() - (robotUnluckyNum % boxOrnament.getRobotOddsUnlucky())); + } else { + boxOrnament.setRobotUnluckyRemainingOdds(0); + } + if (boxOrnament.getWhiteOdds() != null && boxOrnament.getWhiteOdds() > 0) { + boxOrnament.setWhiteRemainingOdds(boxOrnament.getWhiteOdds() - (whiteNum % boxOrnament.getWhiteOdds())); + } else { + boxOrnament.setWhiteRemainingOdds(0); + } + } + } + + /** 返回各类odds总数 */ + public int[] sumOdds(Integer boxId) { + java.util.List> sumList = baseMapper.selectMaps( + new QueryWrapper() + .select("COALESCE(SUM(odds), 0) AS odds", + "COALESCE(SUM(real_odds), 0) AS realOdds", + "COALESCE(SUM(anchor_odds), 0) AS anchorOdds", + "COALESCE(SUM(robot_odds), 0) AS robotOdds", + "COALESCE(SUM(robot_odds_lucky), 0) AS robotOddsLucky", + "COALESCE(SUM(robot_odds_unlucky), 0) AS robotOddsUnlucky", + "COALESCE(SUM(white_odds), 0) AS whiteOdds", + "COALESCE(SUM(third_explosive_odds), 0) AS thirdExplosiveOdds") + .eq("box_id", boxId) + ); + java.util.Map sums = sumList.isEmpty() ? java.util.Collections.emptyMap() : sumList.get(0); + return new int[]{ + Math.max(toInt(sums.get("odds")), 1), + Math.max(toInt(sums.get("realOdds")), 1), + Math.max(toInt(sums.get("anchorOdds")), 1), + Math.max(toInt(sums.get("robotOdds")), 1), + Math.max(toInt(sums.get("thirdExplosiveOdds")), 1), + Math.max(toInt(sums.get("robotOddsLucky")), 1), + Math.max(toInt(sums.get("robotOddsUnlucky")), 1), + Math.max(toInt(sums.get("whiteOdds")), 1) + }; + } + + /** 精确版:传入总数,不受分页影响 */ + public List selectTtBoxOrnamentsListWithTotals(Integer boxId, + int totalOdds, int totalRealOdds, int totalAnchorOdds, int totalRobotOdds, int totalThirdExplosiveOdds, + int totalRobotOddsLucky, int totalRobotOddsUnlucky, int totalWhiteOdds) { + List list = baseMapper.selectTtBoxOrnamentsListWithTotals(boxId, + Math.max(totalOdds, 1), Math.max(totalRealOdds, 1), Math.max(totalAnchorOdds, 1), + Math.max(totalRobotOdds, 1), Math.max(totalRobotOddsLucky, 1), + Math.max(totalRobotOddsUnlucky, 1), Math.max(totalWhiteOdds, 1), + Math.max(totalThirdExplosiveOdds, 1)); + fillRemainingOdds(boxId, list); + return list; + } + + private int toInt(Object val) { + if (val == null) return 0; + return val instanceof Number ? ((Number) val).intValue() : Integer.parseInt(val.toString()); + } + + @Override + public String saveBoxOrnaments(TtBoxOrnaments ttBoxOrnaments) { + // 检查饰品是否已被其他宝箱使用 + String uniqueCheck = checkOrnamentUnique(ttBoxOrnaments.getOrnamentId(), null); + if (StringUtils.isNotEmpty(uniqueCheck)) { + return uniqueCheck; // 返回错误信息给调用方 + } + + this.save(ttBoxOrnaments); + boxService.delCache(ttBoxOrnaments.getBoxId()); + boxService.isReplenishment(ttBoxOrnaments.getBoxId()); + return ""; + } + + @Override + public String updateBoxOrnamentsById(TtBoxOrnamentsDataVO ttBoxOrnamentsDataVO) { + + Integer boxId = ttBoxOrnamentsDataVO.getBoxId(), + boxOrnamentsId = ttBoxOrnamentsDataVO.getId(); + + TtBoxOrnaments dbBoxOrnaments = this.getById(boxOrnamentsId); + boolean realOddsChange = false; + if (dbBoxOrnaments != null && !Objects.equals(dbBoxOrnaments.getRealOdds(), ttBoxOrnamentsDataVO.getRealOdds())) { + realOddsChange = true; + } + boolean oddsChange = false; + if (dbBoxOrnaments != null && !Objects.equals(dbBoxOrnaments.getOdds(), ttBoxOrnamentsDataVO.getOdds())) { + oddsChange = true; + } + Long ornamentsId = ttBoxOrnamentsDataVO.getOrnamentId(); + TtBoxOrnaments ttBoxOrnaments = TtBoxOrnaments.builder() + .id(boxOrnamentsId) + .boxId(boxId) + .ornamentId(ornamentsId) + .build(); + BeanUtils.copyBeanProp(ttBoxOrnaments, ttBoxOrnamentsDataVO); + ttBoxOrnaments.setLevel(ttBoxOrnamentsDataVO.getOrnamentsLevelId()); + ttBoxOrnaments.setOdds(ttBoxOrnamentsDataVO.getOdds()); + // 显示爆率百分比由前端独立控制,数量变化不再清空,仅统一格式化为2位小数 + String percentStr = ttBoxOrnamentsDataVO.getOddsPercentum(); + if (StringUtils.isNotBlank(percentStr) && percentStr.contains("%")) { + try { + BigDecimal percent = new BigDecimal(percentStr.replace("%", "")); + ttBoxOrnaments.setOddsPercentum(percent.setScale(2, RoundingMode.HALF_UP).toPlainString() + "%"); + } catch (NumberFormatException e) { + ttBoxOrnaments.setOddsPercentum(percentStr); + } + } else { + ttBoxOrnaments.setOddsPercentum(percentStr); + } + ttBoxOrnaments.setRealOdds(ttBoxOrnamentsDataVO.getRealOdds()); + ttBoxOrnaments.setAnchorOdds(ttBoxOrnamentsDataVO.getAnchorOdds()); + ttBoxOrnaments.setRobotOdds(ttBoxOrnamentsDataVO.getRobotOdds()); + ttBoxOrnaments.setRobotOddsLucky(ttBoxOrnamentsDataVO.getRobotOddsLucky()); + ttBoxOrnaments.setRobotOddsUnlucky(ttBoxOrnamentsDataVO.getRobotOddsUnlucky()); + ttBoxOrnaments.setCompOdds(ttBoxOrnamentsDataVO.getCompOdds()); + ttBoxOrnaments.setRealCompOdds(ttBoxOrnamentsDataVO.getRealCompOdds()); + ttBoxOrnaments.setThirdExplosiveOdds(ttBoxOrnamentsDataVO.getThirdExplosiveOdds()); + ttBoxOrnaments.setCoverImg(ttBoxOrnamentsDataVO.getCoverImg()); + ttBoxOrnaments.setUpdateBy(SecurityUtils.getUsername()); + ttBoxOrnaments.setUpdateTime(DateUtils.getNowDate()); + if (realOddsChange) { + TtBoxRecordsRemark remark = new TtBoxRecordsRemark(); + remark.setRealOddsChange(sdf.format(new Date())); + ttBoxOrnaments.setRemark(JSONUtil.toJsonStr(remark)); + } + this.updateById(ttBoxOrnaments); + + // 更新饰品金额(use_price) + if (ttBoxOrnamentsDataVO.getUsePrice() != null && ornamentsId != null) { + new LambdaUpdateChainWrapper<>(ttOrnamentMapper) + .eq(TtOrnament::getId, ornamentsId) + .set(TtOrnament::getUsePrice, ttBoxOrnamentsDataVO.getUsePrice()) + .update(); + } + + // todo 旧的开箱机制,全部更新以后可以删除 + // boxService.delCache(boxId); + // boxService.isReplenishment(boxId); + + // 更新redis奖池 + LambdaQueryWrapper ttBoxOrnamentsQuery = new LambdaQueryWrapper<>(); + ttBoxOrnamentsQuery.eq(TtBoxOrnaments::getBoxId, boxId); + List list = this.list(ttBoxOrnamentsQuery); + + String anchorOddsKey = "prize_pool:" + boxId + ":01"; + String realOddsKey = "prize_pool:" + boxId + ":02"; + String compensationOddsKey = "prize_pool:" + boxId + ":03"; // 新增补偿奖池Key + String thirdExplosiveOddsKey = "prize_pool:" + boxId + ":04"; // 新增三级爆率Key + HashMap realOddsSpace = new HashMap<>(); + HashMap anchorOddsSpace = new HashMap<>(); + HashMap compensationOddsSpace = new HashMap<>(); + HashMap thirdExplosiveOddsSpace = new HashMap<>(); + for (TtBoxOrnaments item : list) { + realOddsSpace.put(String.valueOf(item.getOrnamentId()), item.getRealOdds()); + anchorOddsSpace.put(String.valueOf(item.getOrnamentId()), item.getAnchorOdds()); + compensationOddsSpace.put(String.valueOf(item.getOrnamentId()), item.getRealCompOdds()); + thirdExplosiveOddsSpace.put(String.valueOf(item.getOrnamentId()), item.getThirdExplosiveOdds()); + } + redisCache.setCacheMap(realOddsKey, realOddsSpace, 600, TimeUnit.SECONDS); + redisCache.setCacheMap(anchorOddsKey, anchorOddsSpace, 600, TimeUnit.SECONDS); + redisCache.setCacheMap(compensationOddsKey, compensationOddsSpace, 600, TimeUnit.SECONDS); + redisCache.setCacheMap(thirdExplosiveOddsKey, thirdExplosiveOddsSpace, 600, TimeUnit.SECONDS); + + // 清空内存奖池 + lotteryMachine.clearBoxPrizePool(boxId); + + return ""; + } + + @Override + public String removeBoxOrnamentsByIds(Integer boxId, List list) { + this.removeByIds(list); + boxService.delCache(boxId); + boxService.isReplenishment(boxId); + return ""; + } + + @Override + public AjaxResult getProfitMargin(Integer boxId) { + TtBox ttBox = boxService.getById(boxId); + if (Objects.isNull(ttBox)) { + return AjaxResult.error("宝箱不存在"); + } + List list = this.selectTtBoxOrnamentsList(boxId); + list = list.stream().filter(v -> v.getOrnamentId() != null && v.getUsePrice() != null).collect(Collectors.toList()); + int oddsTotalNum = list.stream().map(TtBoxOrnamentsDataVO::getOdds).mapToInt(i -> i != null ? i : 0).sum(); + BigDecimal oddsAmountConsumed = ttBox.getPrice().multiply(BigDecimal.valueOf(oddsTotalNum)); + BigDecimal oddsAggregateAmount = list.stream().map(v -> v.getUsePrice() + .multiply(BigDecimal.valueOf(v.getOdds() != null ? v.getOdds() : 0))).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal oddsTotalProfit = oddsAmountConsumed.subtract(oddsAggregateAmount); + BigDecimal oddsTotalProfitMargin = getTotalProfitMargin(oddsTotalProfit, oddsAmountConsumed); + int realOddsTotalNum = list.stream().map(TtBoxOrnamentsDataVO::getRealOdds).mapToInt(i -> i != null ? i : 0).sum(); + BigDecimal realOddsAmountConsumed = ttBox.getPrice().multiply(BigDecimal.valueOf(realOddsTotalNum)); + BigDecimal realOddsAggregateAmount = list.stream().map(v -> v.getUsePrice() + .multiply(BigDecimal.valueOf(v.getRealOdds() != null ? v.getRealOdds() : 0))).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal realOddsTotalProfit = realOddsAmountConsumed.subtract(realOddsAggregateAmount); + BigDecimal realOddsTotalProfitMargin = getTotalProfitMargin(realOddsTotalProfit, realOddsAmountConsumed); + int anchorOddsTotalNum = list.stream().map(TtBoxOrnamentsDataVO::getAnchorOdds).mapToInt(i -> i != null ? i : 0).sum(); + BigDecimal anchorOddsAmountConsumed = ttBox.getPrice().multiply(BigDecimal.valueOf(anchorOddsTotalNum)); + BigDecimal anchorOddsAggregateAmount = list.stream().map(v -> v.getUsePrice() + .multiply(BigDecimal.valueOf(v.getAnchorOdds() != null ? v.getAnchorOdds() : 0))).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal anchorOddsTotalProfit = anchorOddsAmountConsumed.subtract(anchorOddsAggregateAmount); + BigDecimal anchorOddsTotalProfitMargin = getTotalProfitMargin(anchorOddsTotalProfit, anchorOddsAmountConsumed); + + int robotOddsTotalNum = list.stream().map(TtBoxOrnamentsDataVO::getRobotOdds).mapToInt(i -> i != null ? i : 0).sum(); + BigDecimal robotOddsAmountConsumed = ttBox.getPrice().multiply(BigDecimal.valueOf(robotOddsTotalNum)); + BigDecimal robotOddsAggregateAmount = list.stream().map(v -> v.getUsePrice() + .multiply(BigDecimal.valueOf(v.getRobotOdds() != null ? v.getRobotOdds() : 0))).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal robotOddsTotalProfit = robotOddsAmountConsumed.subtract(robotOddsAggregateAmount); + BigDecimal robotOddsTotalProfitMargin = getTotalProfitMargin(robotOddsTotalProfit, robotOddsAmountConsumed); + + int robotOddsLuckyTotalNum = list.stream().map(TtBoxOrnamentsDataVO::getRobotOddsLucky).mapToInt(i -> i != null ? i : 0).sum(); + BigDecimal robotOddsLuckyAmountConsumed = ttBox.getPrice().multiply(BigDecimal.valueOf(robotOddsLuckyTotalNum)); + BigDecimal robotOddsLuckyAggregateAmount = list.stream().map(v -> v.getUsePrice() + .multiply(BigDecimal.valueOf(v.getRobotOddsLucky() != null ? v.getRobotOddsLucky() : 0))).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal robotOddsLuckyTotalProfit = robotOddsLuckyAmountConsumed.subtract(robotOddsLuckyAggregateAmount); + BigDecimal robotOddsLuckyTotalProfitMargin = getTotalProfitMargin(robotOddsLuckyTotalProfit, robotOddsLuckyAmountConsumed); + + int robotOddsUnluckyTotalNum = list.stream().map(TtBoxOrnamentsDataVO::getRobotOddsUnlucky).mapToInt(i -> i != null ? i : 0).sum(); + BigDecimal robotOddsUnluckyAmountConsumed = ttBox.getPrice().multiply(BigDecimal.valueOf(robotOddsUnluckyTotalNum)); + BigDecimal robotOddsUnluckyAggregateAmount = list.stream().map(v -> v.getUsePrice() + .multiply(BigDecimal.valueOf(v.getRobotOddsUnlucky() != null ? v.getRobotOddsUnlucky() : 0))).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal robotOddsUnluckyTotalProfit = robotOddsUnluckyAmountConsumed.subtract(robotOddsUnluckyAggregateAmount); + BigDecimal robotOddsUnluckyTotalProfitMargin = getTotalProfitMargin(robotOddsUnluckyTotalProfit, robotOddsUnluckyAmountConsumed); + + int whiteOddsTotalNum = list.stream().map(TtBoxOrnamentsDataVO::getWhiteOdds).mapToInt(i -> i != null ? i : 0).sum(); + BigDecimal whiteOddsAmountConsumed = ttBox.getPrice().multiply(BigDecimal.valueOf(whiteOddsTotalNum)); + BigDecimal whiteOddsAggregateAmount = list.stream().map(v -> v.getUsePrice() + .multiply(BigDecimal.valueOf(v.getWhiteOdds() != null ? v.getWhiteOdds() : 0))).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal whiteOddsTotalProfit = whiteOddsAmountConsumed.subtract(whiteOddsAggregateAmount); + BigDecimal whiteOddsTotalProfitMargin = getTotalProfitMargin(whiteOddsTotalProfit, whiteOddsAmountConsumed); + + + AjaxResult success = AjaxResult.success(); + success.put("oddsTotalNum", oddsTotalNum); + success.put("oddsTotalProfit", oddsTotalProfit); + success.put("oddsTotalProfitMargin", oddsTotalProfitMargin + "%"); + success.put("realOddsTotalNum", realOddsTotalNum); + success.put("realOddsTotalProfit", realOddsTotalProfit); + success.put("realOddsTotalProfitMargin", realOddsTotalProfitMargin + "%"); + success.put("anchorOddsTotalNum", anchorOddsTotalNum); + success.put("anchorOddsTotalProfit", anchorOddsTotalProfit); + success.put("anchorOddsTotalProfitMargin", anchorOddsTotalProfitMargin + "%"); + success.put("robotOddsTotalNum", robotOddsTotalNum); + success.put("robotOddsTotalProfit", robotOddsTotalProfit); + success.put("robotOddsTotalProfitMargin", robotOddsTotalProfitMargin + "%"); + success.put("robotOddsLuckyTotalNum", robotOddsLuckyTotalNum); + success.put("robotOddsLuckyTotalProfit", robotOddsLuckyTotalProfit); + success.put("robotOddsLuckyTotalProfitMargin", robotOddsLuckyTotalProfitMargin + "%"); + success.put("robotOddsUnluckyTotalNum", robotOddsUnluckyTotalNum); + success.put("robotOddsUnluckyTotalProfit", robotOddsUnluckyTotalProfit); + success.put("robotOddsUnluckyTotalProfitMargin", robotOddsUnluckyTotalProfitMargin + "%"); + success.put("whiteOddsTotalNum", whiteOddsTotalNum); + success.put("whiteOddsTotalProfit", whiteOddsTotalProfit); + success.put("whiteOddsTotalProfitMargin", whiteOddsTotalProfitMargin + "%"); + + return success; + } + + @Override + public String batchAdd(Integer boxId, List ornamentsIds) { + List tempList = new ArrayList<>(ornamentsIds); + List ttBoxOrnamentsList = new ArrayList<>(); + for (Long ornamentsId : tempList) { + TtBoxOrnaments ttBoxOrnaments = TtBoxOrnaments.builder().build(); + ttBoxOrnaments.setBoxId(boxId); + ttBoxOrnaments.setOrnamentId(ornamentsId); + ttBoxOrnaments.setCreateBy(SecurityUtils.getUsername()); + ttBoxOrnaments.setCreateTime(DateUtils.getNowDate()); + ttBoxOrnamentsList.add(ttBoxOrnaments); + } + this.saveBatch(ttBoxOrnamentsList, 1); + boxService.delCache(boxId); + boxService.isReplenishment(boxId); + return ""; + } + + @Override + public AjaxResult batchAdd(batchAddParam param) { + + Integer boxId = param.getBoxId(); + if (boxId == null) { + return AjaxResult.error("宝箱ID不能为空"); + } + TtBox curBox = boxService.getById(boxId); + if (curBox == null) { + return AjaxResult.error("宝箱不存在,请刷新后重试"); + } + String curBoxName = curBox.getBoxName() == null ? "未知" : curBox.getBoxName(); + + if (CollectionUtils.isEmpty(param.getOrnamentIds())) { + return AjaxResult.error("请先选择要添加的饰品"); + } + + // 去重,避免同一批次传入重复饰品 + List tempList = new ArrayList<>(new LinkedHashSet<>(param.getOrnamentIds())); + + // 一次性查出饰品信息(名称用于提示) + Map ornamentMap = new HashMap<>(); + if (CollectionUtils.isNotEmpty(tempList)) { + List ornamentList = new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .in(TtOrnament::getId, tempList) + .list(); + for (TtOrnament orn : ornamentList) { + ornamentMap.put(orn.getId(), orn); + } + } + + // 当前宝箱已有饰品集合 + Set curBoxOrnIds = new HashSet<>(baseMapper.allOrnId(boxId)); + + // 逐个校验:当前宝箱重复 / 其他宝箱占用 + List errors = new ArrayList<>(); + for (Long ornamentId : tempList) { + if (ornamentId == null) { + continue; + } + String ornName = ornamentMap.get(ornamentId) != null + && StringUtils.isNotBlank(ornamentMap.get(ornamentId).getName()) + ? ornamentMap.get(ornamentId).getName() : String.valueOf(ornamentId); + + // 1. 当前宝箱是否已添加过该饰品 + if (curBoxOrnIds.contains(ornamentId)) { + errors.add(String.format("饰品【%s】(id=%d) 已在当前宝箱【%s】中添加过", + ornName, ornamentId, curBoxName)); + continue; + } + // 2. 该饰品是否已被其他宝箱占用(独占机制) + String uniqueMsg = checkOrnamentUnique(ornamentId, boxId); + if (StringUtils.isNotEmpty(uniqueMsg)) { + errors.add(uniqueMsg); + } + } + + if (CollectionUtils.isNotEmpty(errors)) { + return AjaxResult.error(String.join(";", errors)); + } + + List ttBoxOrnamentsList = new ArrayList<>(); + for (Long ornamentId : tempList) { + // 取饰品自身的 quality,没有则默认 1 + int level = 1; + TtOrnament orn = ornamentMap.get(ornamentId); + if (orn != null && orn.getQuality() != null) { + String q = orn.getQuality().trim(); + if (q.matches("[1-5]")) { + level = Integer.parseInt(q); + } + } + + TtBoxOrnaments boxOrn = TtBoxOrnaments.builder() + .boxId(boxId) + .ornamentId(ornamentId) + .level(level) + .realOdds(1) + .odds(1) + .createBy(SecurityUtils.getUsername()) + .createTime(DateUtils.getNowDate()) + .build(); + + ttBoxOrnamentsList.add(boxOrn); + } + + this.saveBatch(ttBoxOrnamentsList); + boxService.delCache(boxId); + boxService.isReplenishment(boxId); + + return AjaxResult.success("批量填货成功,请手动修改饰品数量!"); + } + + @Override + public List simpleBoxDetail(Integer boxId) { + return baseMapper.simpleBoxDetail(boxId); + } + + @Override + public R globalData(Integer boxId) { + + //检查,空箱子直接返回 + List list = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtBoxOrnaments::getBoxId, boxId) + .list(); + if (ObjectUtil.isEmpty(list) || list.isEmpty()) return R.ok(new BoxGlobalData()); + + // 宝箱统计 + BoxGlobalData globalData = boxMapper.globalData(boxId); + + // 计算利润率 + // 通用 + BigDecimal commonAmountConsumed = globalData.getCommonAmountConsumed(); + if (commonAmountConsumed.compareTo(BigDecimal.ZERO)==0){ + globalData.setCommonProfit(null); + globalData.setCommonProfitMargin(null); + }else { + BigDecimal commonAggregateAmount = globalData.getCommonAggregateAmount(); + BigDecimal commonProfit = commonAmountConsumed.subtract(commonAggregateAmount); + BigDecimal commonProfitMargin = commonProfit.divide(commonAmountConsumed, 4, RoundingMode.HALF_UP); + + globalData.setCommonProfit(commonProfit); + globalData.setCommonProfitMargin(commonProfitMargin); + } + + // 主播 + BigDecimal anchorAmountConsumed = globalData.getAnchorAmountConsumed(); + if (anchorAmountConsumed.compareTo(BigDecimal.ZERO)==0){ + globalData.setAnchorProfit(null); + globalData.setAnchorProfitMargin(null); + }else { + BigDecimal anchorAggregateAmount = globalData.getAnchorAggregateAmount(); + BigDecimal anchorProfit = anchorAmountConsumed.subtract(anchorAggregateAmount); + BigDecimal anchorProfitMargin = anchorProfit.divide(anchorAmountConsumed, 4, RoundingMode.HALF_UP); + + globalData.setAnchorProfit(anchorProfit); + globalData.setAnchorProfitMargin(anchorProfitMargin); + } + + return R.ok(globalData); + } + + private BigDecimal getTotalProfitMargin(BigDecimal totalProfit, BigDecimal amountConsumed) { + if (BigDecimal.ZERO.compareTo(amountConsumed) == 0) return BigDecimal.ZERO; + return totalProfit.divide(amountConsumed, 4, RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxRecordsServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxRecordsServiceImpl.java new file mode 100644 index 0000000..29add17 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxRecordsServiceImpl.java @@ -0,0 +1,25 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.domain.other.TtBoxRecordsBody; +import com.ruoyi.domain.vo.TtBoxRecordsDataVO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TtBoxRecordsServiceImpl extends ServiceImpl implements TtBoxRecordsService { + + @Override + public List selectBoxRecordsList(TtBoxRecordsBody ttBoxRecordsBody) { + return baseMapper.selectBoxRecordsList(ttBoxRecordsBody); + } + + @Override + public List selectWelfareBoxRecordsList(TtBoxRecordsBody ttBoxRecordsBody) { + return baseMapper.selectWelfareBoxRecordsList(ttBoxRecordsBody); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxServiceImpl.java new file mode 100644 index 0000000..bb31d96 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxServiceImpl.java @@ -0,0 +1,418 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import com.ruoyi.admin.enums.BoxPoolType; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.dto.boxRecords.TtBoxRecordsNum; +import com.ruoyi.domain.dto.boxRecords.TtBoxRecordsPoolStat; +import com.ruoyi.domain.dto.boxRecords.TtBoxRecordsRemark; +import com.ruoyi.domain.entity.box.TtBoxThirdExplosiveUser; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtBoxBody; +import com.ruoyi.domain.other.TtBoxOrnaments; +import com.ruoyi.domain.vo.BoxPoolStatisticsVO; +import com.ruoyi.domain.vo.TtBoxDataVO; +import com.ruoyi.domain.vo.box.TtBoxUserVO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class TtBoxServiceImpl extends ServiceImpl implements TtBoxService { + + private final RedisCache redisCache; + private final TtBoxRecordsMapper boxRecordsMapper; + private final TtBoxOrnamentsMapper boxOrnamentsMapper; + + @Autowired + private TtBoxTypeMapper ttBoxTypeMapper; + + @Autowired + private TtBoxThirdExplosiveUserMapper ttBoxThirdExplosiveUserMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private LotteryMachine lotteryMachine; + + public TtBoxServiceImpl(RedisCache redisCache, + TtBoxRecordsMapper boxRecordsMapper, + TtBoxOrnamentsMapper boxOrnamentsMapper) { + this.redisCache = redisCache; + this.boxRecordsMapper = boxRecordsMapper; + this.boxOrnamentsMapper = boxOrnamentsMapper; + } + + @Override + public PageDataInfo selectTtBoxList(TtBoxBody ttBoxBody) { + + List resultList = baseMapper.selectTtBoxList(ttBoxBody); + List list = resultList.stream().peek(ttBoxDataVO -> { + BigDecimal amountConsumed = ttBoxDataVO.getAmountConsumed(); + BigDecimal aggregateAmount = ttBoxDataVO.getAggregateAmount(); + if (StringUtils.isNotNull(amountConsumed) && StringUtils.isNotNull(aggregateAmount) + && amountConsumed.compareTo(BigDecimal.ZERO) != 0) { + + BigDecimal totalProfit = amountConsumed.subtract(aggregateAmount); + BigDecimal totalProfitMargin = totalProfit + .divide(amountConsumed, 4, RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(100)) + .setScale(2, RoundingMode.HALF_UP); + ttBoxDataVO.setProfit(totalProfit); + ttBoxDataVO.setProfitMargin(totalProfitMargin + "%"); + + } + }).collect(Collectors.toList()); + PageDataInfo pageDataInfo = new PageDataInfo<>(); + pageDataInfo.setCode(HttpStatus.SUCCESS); + pageDataInfo.setMsg("查询成功"); + pageDataInfo.setRows(list); + pageDataInfo.setTotal(new PageInfo<>(resultList).getTotal()); + return pageDataInfo; + } + + @Override + public String updateTtBoxById(TtBoxDataVO ttBoxDataVO) { + + String boxImg01 = ttBoxDataVO.getBoxImg01(), boxImg02 = ttBoxDataVO.getBoxImg02(); + TtBox ttBox = TtBox.builder().build(); + BeanUtils.copyBeanProp(ttBox, ttBoxDataVO); + ttBox.setUpdateBy(SecurityUtils.getUsername()); + ttBox.setUpdateTime(DateUtils.getNowDate()); + // 图片字段为空时设为null,避免updateById覆盖已有图片 + ttBox.setBoxImg01(StringUtils.isNotEmpty(boxImg01) ? boxImg01 : ""); + ttBox.setBoxImg02(StringUtils.isNotEmpty(boxImg02) ? boxImg02 : ""); + this.updateById(ttBox); + + //删除该宝箱的奖池 + //BASE_POOL_KEY + item.getBoxId() + ":" + "01" + + return ""; + } + + @Override + public void isReplenishment(Integer boxId) { + TtBox ttBox = this.getById(boxId); + if (ttBox == null) { + return; + } + ttBox.setOpenNum(0L); + this.updateById(ttBox); + + lotteryMachine.resetBoxPrizePool(boxId); + + // 对齐“修改数量”的重置机制:将该宝箱下所有饰品的统计起点 realOddsChange 更新为当前时间。 + // “当前剩余库存”是基于 tt_box_records 历史开箱记录数与 realOddsChange 起点实时计算的, + // 仅清 openNum 与 Redis 奖池无法重置剩余库存;把起点推到当前,剩余库存即恢复为满库存。 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); + TtBoxRecordsRemark remark = new TtBoxRecordsRemark(); + remark.setRealOddsChange(sdf.format(new Date())); + + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(TtBoxOrnaments::getBoxId, boxId) + .set(TtBoxOrnaments::getRemark, JSONUtil.toJsonStr(remark)) + .set(TtBoxOrnaments::getUpdateBy, SecurityUtils.getUsername()) + .set(TtBoxOrnaments::getUpdateTime, DateUtils.getNowDate()); + boxOrnamentsMapper.update(null, updateWrapper); + } + + @Override + public void delCache(Integer boxId) { + } + + @Override + public List statisticsBoxData(Integer boxId, Date date) { + TtBox ttBox = this.getById(boxId); + if (ttBox == null) { + return Collections.emptyList(); + } + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + String formattedDate = StringUtils.isNull(date) ? dateFormat.format(DateUtils.getNowDate()) : dateFormat.format(date); + + // 对战宝箱(is_fight='0')与经典宝箱只统计各自的开箱渠道:对战=急速对战(source=2),经典=常规开箱(source=0/1), + // 指数盲盒/roll房/幸运升级/福利开箱/荒野争锋/玫瑰女郎等其他电子游戏的开箱不计入本统计。 + // 对战开箱记录先按常规开箱(source=1)落库,一轮对战结束后才改成急速对战(source=2),两个来源都算才不会漏掉中途中断的对局。 + boolean fightBox = "0".equals(ttBox.getIsFight()); + List sources = fightBox + ? Arrays.asList(TtboxRecordSource.BLIND_BOX.getCode(), TtboxRecordSource.FIGHT.getCode()) + : Arrays.asList(0, TtboxRecordSource.BLIND_BOX.getCode()); + + // 1 各奖池本轮剩余库存:与「宝箱内物品明细」同一套算法(按开箱人类型分组统计 tt_box_records, + // 统计起点为该饰品的 realOddsChange,即重置宝箱/修改爆率的时间点) + List ornaments = new LambdaQueryChainWrapper<>(boxOrnamentsMapper) + .eq(TtBoxOrnaments::getBoxId, boxId) + .list(); + Map recordsNumMap = loadPoolRecordsNum(boxId, ornaments); + + // 2 今日各奖池的开箱次数与爆出价值(机器人按对战模式拆欧皇/非酋) + TtBoxRecordsPoolStat todayStat = boxRecordsMapper.selectBoxRecordsPoolStat(boxId, formattedDate, sources); + + // 3 组装需要展示的奖池(顺序与宝箱奖项页的列顺序一致:真实 → 主播 → 机器人 → 白号): + // 经典宝箱 = 真实池/主播池/机器人池/白号池 + // 对战宝箱 = 真实池/主播池/机器人欧皇池/机器人非酋池/白号池(对战箱没有普通机器人池) + List pools = new ArrayList<>(); + pools.add(BoxPoolType.NORMAL_PLAYER); + pools.add(BoxPoolType.ANCHOR_PLAYER); + if (fightBox) { + pools.add(BoxPoolType.ROBOT_PLAYER_LUCKY); + pools.add(BoxPoolType.ROBOT_PLAYER_UNLUCKY); + } else { + pools.add(BoxPoolType.ROBOT_PLAYER); + } + pools.add(BoxPoolType.WHITE_PLAYER); + + List result = new ArrayList<>(); + for (BoxPoolType poolType : pools) { + int total = 0; + int remaining = 0; + for (TtBoxOrnaments ornament : ornaments) { + Integer odds = poolOdds(ornament, poolType); + total += odds == null ? 0 : odds; + remaining += poolRemaining(odds, poolOpenedNum(recordsNumMap.get(ornament.getOrnamentId()), poolType)); + } + int todayOpenNum = poolTodayOpenNum(todayStat, poolType); + BigDecimal todayPrice = poolTodayPrice(todayStat, poolType); + BigDecimal todayProfit = ttBox.getPrice() == null + ? BigDecimal.ZERO + : ttBox.getPrice().multiply(BigDecimal.valueOf(todayOpenNum)).subtract(todayPrice); + result.add(BoxPoolStatisticsVO.builder() + .poolType(poolType.getId()) + .poolName(poolDisplayName(poolType)) + .remaining(remaining) + .total(total) + .remainingNum(remaining + "/" + total) + .todayOpenNum(todayOpenNum) + .todayArisePriceTotal(todayPrice) + .todayProfit(todayProfit) + .build()); + } + return result; + } + + /** + * 按用户类型统计各饰品本轮开箱次数,起点为该饰品的 realOddsChange。 + * 与 TtBoxOrnamentsServiceImpl.fillRemainingOdds 同源,保证统计弹窗与「宝箱内物品明细」剩余列对得上。 + */ + private Map loadPoolRecordsNum(Integer boxId, List ornaments) { + Map> beginTimeToOrnamentIds = new HashMap<>(); + for (TtBoxOrnaments ornament : ornaments) { + if (ornament.getOrnamentId() == null) { + continue; + } + String beginTime = null; + if (StringUtils.isNotEmpty(ornament.getRemark())) { + TtBoxRecordsRemark remark = JSONUtil.toBean(ornament.getRemark(), TtBoxRecordsRemark.class); + if (remark != null) { + beginTime = remark.getRealOddsChange(); + } + } + beginTimeToOrnamentIds.computeIfAbsent(beginTime == null ? "" : beginTime, k -> new ArrayList<>()) + .add(ornament.getOrnamentId()); + } + Map recordsNumMap = new HashMap<>(); + for (Map.Entry> entry : beginTimeToOrnamentIds.entrySet()) { + if (CollectionUtils.isEmpty(entry.getValue())) { + continue; + } + List boxRecordsNums = boxRecordsMapper.selectBoxRecordsByOrnamentIds( + boxId, entry.getValue(), entry.getKey().isEmpty() ? null : entry.getKey()); + if (CollectionUtils.isEmpty(boxRecordsNums)) { + continue; + } + for (TtBoxRecordsNum num : boxRecordsNums) { + if (num != null && num.getOrnamentId() != null) { + recordsNumMap.put(num.getOrnamentId(), num); + } + } + } + return recordsNumMap; + } + + /** + * 该奖池的爆率(库存) + */ + private static Integer poolOdds(TtBoxOrnaments ornament, BoxPoolType poolType) { + return switch (poolType) { + case ANCHOR_PLAYER -> ornament.getAnchorOdds(); + case NORMAL_PLAYER -> ornament.getRealOdds(); + case ROBOT_PLAYER -> ornament.getRobotOdds(); + case ROBOT_PLAYER_LUCKY -> ornament.getRobotOddsLucky(); + case ROBOT_PLAYER_UNLUCKY -> ornament.getRobotOddsUnlucky(); + case WHITE_PLAYER -> ornament.getWhiteOdds(); + default -> null; + }; + } + + /** + * 该奖池的开箱次数 + */ + private static Integer poolOpenedNum(TtBoxRecordsNum recordsNum, BoxPoolType poolType) { + if (recordsNum == null) { + return 0; + } + return switch (poolType) { + case ANCHOR_PLAYER -> recordsNum.getAnchorNum(); + case NORMAL_PLAYER -> recordsNum.getRealNum(); + case ROBOT_PLAYER -> recordsNum.getRobotNum(); + case ROBOT_PLAYER_LUCKY -> recordsNum.getRobotLuckyNum(); + case ROBOT_PLAYER_UNLUCKY -> recordsNum.getRobotUnluckyNum(); + case WHITE_PLAYER -> recordsNum.getWhiteNum(); + default -> 0; + }; + } + + /** + * 本轮剩余:库存 - (已开箱次数 % 库存),与明细页剩余列一致;库存为 0 时剩余 0 + */ + private static int poolRemaining(Integer odds, Integer openedNum) { + if (odds == null || odds <= 0) { + return 0; + } + int opened = openedNum == null ? 0 : openedNum; + return odds - (opened % odds); + } + + private static int poolTodayOpenNum(TtBoxRecordsPoolStat stat, BoxPoolType poolType) { + if (stat == null) { + return 0; + } + Integer num = switch (poolType) { + case ANCHOR_PLAYER -> stat.getAnchorNum(); + case NORMAL_PLAYER -> stat.getRealNum(); + case ROBOT_PLAYER -> stat.getRobotNum(); + case ROBOT_PLAYER_LUCKY -> stat.getRobotLuckyNum(); + case ROBOT_PLAYER_UNLUCKY -> stat.getRobotUnluckyNum(); + case WHITE_PLAYER -> stat.getWhiteNum(); + default -> 0; + }; + return num == null ? 0 : num; + } + + private static BigDecimal poolTodayPrice(TtBoxRecordsPoolStat stat, BoxPoolType poolType) { + if (stat == null) { + return BigDecimal.ZERO; + } + BigDecimal price = switch (poolType) { + case ANCHOR_PLAYER -> stat.getAnchorPrice(); + case NORMAL_PLAYER -> stat.getRealPrice(); + case ROBOT_PLAYER -> stat.getRobotPrice(); + case ROBOT_PLAYER_LUCKY -> stat.getRobotLuckyPrice(); + case ROBOT_PLAYER_UNLUCKY -> stat.getRobotUnluckyPrice(); + case WHITE_PLAYER -> stat.getWhitePrice(); + default -> null; + }; + return price == null ? BigDecimal.ZERO : price; + } + + private static String poolDisplayName(BoxPoolType poolType) { + return switch (poolType) { + case ANCHOR_PLAYER -> "主播池"; + case NORMAL_PLAYER -> "真实池"; + case ROBOT_PLAYER -> "机器人池"; + case ROBOT_PLAYER_LUCKY -> "机器人欧皇池"; + case ROBOT_PLAYER_UNLUCKY -> "机器人非酋池"; + case WHITE_PLAYER -> "白号池"; + default -> poolType.getDesc(); + }; + } + + @Override + public List getRealList(Integer boxId, int flag) { + List boxOrnamentsDataList = new LambdaQueryChainWrapper<>(boxOrnamentsMapper) + .eq(TtBoxOrnaments::getBoxId, boxId) + .list(); + + Map realDataParam = new HashMap<>(); + Map anchorDataParam = new HashMap<>(); + for (TtBoxOrnaments boxOrnaments : boxOrnamentsDataList) { + Long ornamentsId = boxOrnaments.getOrnamentId(); + Integer realOdds = boxOrnaments.getRealOdds(); + realDataParam.put(ornamentsId, realOdds); + Integer anchorOdds = boxOrnaments.getAnchorOdds(); + anchorDataParam.put(ornamentsId, anchorOdds); + } + if (flag == 0) return RandomUtils.toList(realDataParam); + if (flag == 1) return RandomUtils.toList(anchorDataParam); + return null; + } + + @Override + public String createTtBoxThirdExplosiveUsers(TtBoxUserVO vo) { + if (vo == null || vo.getBoxId() == null || CollectionUtils.isEmpty(vo.getUsers())) { + return "请求参数不合法"; + } + for (TtUser ttUser : vo.getUsers()) { + TtBoxThirdExplosiveUser user = new TtBoxThirdExplosiveUser(); + user.setBoxId(vo.getBoxId()); + user.setUserId(ttUser.getUserId()); + user.setCreateTime(new Date()); + ttBoxThirdExplosiveUserMapper.insert(user); + } + return ""; + } + + @Override + public AjaxResult queryTtBoxThirdExplosiveUsers(Integer boxId) { + if (boxId == null) { + return AjaxResult.error("箱子ID为空"); + } + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(TtBoxThirdExplosiveUser::getBoxId, boxId); + List users = ttBoxThirdExplosiveUserMapper.selectList(query); + List userIds = users.stream().filter(Objects::nonNull).map(TtBoxThirdExplosiveUser::getUserId).collect(Collectors.toList()); + TtBoxUserVO vo = new TtBoxUserVO(); + vo.setBoxId(boxId); + if (CollectionUtils.isEmpty(userIds)) { + return AjaxResult.success(vo); + } + List ttUsers = ttUserMapper.selectBatchIds(userIds); + vo.setUsers(ttUsers); + return AjaxResult.success(vo); + } + + @Override + public AjaxResult deleteTtBoxThirdExplosiveUsers(TtBoxUserVO vo) { + if (vo == null || vo.getBoxId() == null || CollectionUtils.isEmpty(vo.getUsers())) { + return AjaxResult.error("请求参数不合法"); + } + List userIds = vo.getUsers().stream().filter(Objects::nonNull).map(TtUser::getUserId).collect(Collectors.toList()); + LambdaUpdateWrapper update = new LambdaUpdateWrapper<>(); + update.eq(TtBoxThirdExplosiveUser::getBoxId, vo.getBoxId()); + update.in(TtBoxThirdExplosiveUser::getUserId, userIds); + ttBoxThirdExplosiveUserMapper.delete(update); + return AjaxResult.success(); + } + + @Override + public AjaxResult resetBox(Integer boxId) { + return null; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxTypeServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxTypeServiceImpl.java new file mode 100644 index 0000000..941182b --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtBoxTypeServiceImpl.java @@ -0,0 +1,22 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtBoxType; +import com.ruoyi.admin.mapper.TtBoxTypeMapper; +import com.ruoyi.admin.service.TtBoxTypeService; +import com.ruoyi.common.config.RuoYiConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class TtBoxTypeServiceImpl extends ServiceImpl implements TtBoxTypeService { + + @Override + public String updateBoxTypeById(TtBoxType ttBoxType) { + String icon = ttBoxType.getIcon(); + ttBoxType.setIcon(RuoYiConfig.getDomainName() + icon); + this.updateById(ttBoxType); + return ""; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtContentServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtContentServiceImpl.java new file mode 100644 index 0000000..9b4caa3 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtContentServiceImpl.java @@ -0,0 +1,28 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtContent; +import com.ruoyi.admin.mapper.TtContentMapper; +import com.ruoyi.admin.service.TtContentService; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TtContentServiceImpl extends ServiceImpl implements TtContentService { + + @Override + public List queryList(TtContent ttContent) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotNull(ttContent.getId())) wrapper.eq(TtContent::getId, ttContent.getId()); + if (StringUtils.isNotNull(ttContent.getTypeId())) wrapper.eq(TtContent::getTypeId, ttContent.getTypeId()); + if (StringUtils.isNotEmpty(ttContent.getTitle())) wrapper.like(TtContent::getTitle, ttContent.getTitle()); + if (StringUtils.isNotEmpty(ttContent.getContent())) wrapper.like(TtContent::getContent, ttContent.getContent()); + if (StringUtils.isNotEmpty(ttContent.getStatus())) wrapper.eq(TtContent::getStatus, ttContent.getStatus()); + wrapper.orderByDesc(TtContent::getCreateTime); + return this.list(wrapper); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtContentTypeServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtContentTypeServiceImpl.java new file mode 100644 index 0000000..a26888e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtContentTypeServiceImpl.java @@ -0,0 +1,27 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtContentType; +import com.ruoyi.admin.mapper.TtContentTypeMapper; +import com.ruoyi.admin.service.TtContentTypeService; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TtContentTypeServiceImpl extends ServiceImpl implements TtContentTypeService { + + @Override + public List queryList(TtContentType ttContentType) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotNull(ttContentType.getId())) wrapper.eq(TtContentType::getId, ttContentType.getId()); + if (StringUtils.isNotNull(ttContentType.getName())) wrapper.like(TtContentType::getName, ttContentType.getName()); + if (StringUtils.isNotEmpty(ttContentType.getAlias())) wrapper.like(TtContentType::getAlias, ttContentType.getAlias()); + if (StringUtils.isNotEmpty(ttContentType.getStatus())) wrapper.eq(TtContentType::getStatus, ttContentType.getStatus()); + wrapper.orderByDesc(TtContentType::getCreateTime); + return this.list(wrapper); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtDeliveryRecordServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtDeliveryRecordServiceImpl.java new file mode 100644 index 0000000..aa4ffce --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtDeliveryRecordServiceImpl.java @@ -0,0 +1,259 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.service.TtUserTransferDailyService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.common.constant.DeliveryOrderStatus; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.dto.userRecord.DeliveryRecordsConfition; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtDeliveryRecordMapper; +import com.ruoyi.admin.service.TtDeliveryRecordService; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtDeliveryApplyBody; +import com.ruoyi.domain.vo.DeliveryApplyVO; +import com.ruoyi.domain.other.TtDeliveryRecordBody; +import com.ruoyi.domain.vo.TtDeliveryRecordDataVO; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.vo.delivery.DeliveryRecordVO; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; + +@Service +public class TtDeliveryRecordServiceImpl extends ServiceImpl implements TtDeliveryRecordService { + + private final TtBoxRecordsMapper boxRecordsMapper; + private final TtUserTransferDailyService userTransferDailyService; + private final RedisTemplate redisTemplate; + private final TtUserService userService; + + public TtDeliveryRecordServiceImpl(TtBoxRecordsMapper boxRecordsMapper, + TtUserTransferDailyService userTransferDailyService, + RedisTemplate redisTemplate, + TtUserService userService) { + this.boxRecordsMapper = boxRecordsMapper; + this.userTransferDailyService = userTransferDailyService; + this.redisTemplate = redisTemplate; + this.userService = userService; + } + + @Override + public List getDeliveryApplyList(TtDeliveryApplyBody deliveryApplyBody) { + return baseMapper.getDeliveryApplyList(deliveryApplyBody); + } + + /** + * 审核通过提货申请(确认消耗次数) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String deliveryApprove(Integer deliveryRecordId) { + TtDeliveryRecord record = this.getById(deliveryRecordId); + if (record == null) { + return "提货记录不存在"; + } + + // 1. 更新当前记录状态为完成 + record.setStatus(DeliveryOrderStatus.ORDER_COMPLETE.getCode()); + record.setMessage(DeliveryOrderStatus.ORDER_COMPLETE.getMsg()); + record.setUpdateBy(SecurityUtils.getUsername()); + record.setUpdateTime(DateUtils.getNowDate()); + this.updateById(record); + + // 2. 查询同批次的所有记录 + String batchId = record.getBatchId(); + if (batchId == null || batchId.isEmpty()) { + // 兼容旧数据:没有 batchId 的记录不处理次数 + return ""; + } + + List batchRecords = this.lambdaQuery() + .eq(TtDeliveryRecord::getBatchId, batchId) + .orderByAsc(TtDeliveryRecord::getId) + .list(); + + // 3. 数据库幂等:检查该批次是否已处理过次数(防止 Redis 过期后重复扣次) + long alreadyDeductedCount = this.lambdaQuery() + .eq(TtDeliveryRecord::getBatchId, batchId) + .eq(TtDeliveryRecord::getTimesDeducted, 1) + .count(); + if (alreadyDeductedCount > 0) { + return ""; + } + + // 4. Redis 快速防重(短期) + String processedKey = "delivery:batch:processed:" + batchId; + Boolean alreadyProcessed = (Boolean) redisTemplate.opsForValue().get(processedKey); + if (alreadyProcessed != null && alreadyProcessed) { + return ""; + } + + // 5. 判断这批里有没有非取消状态的记录 + boolean hasNonCancelled = batchRecords.stream() + .anyMatch(r -> !DeliveryOrderStatus.ORDER_CANCEL.getCode().equals(r.getStatus())); + + if (hasNonCancelled) { + // ========== 有非取消状态的记录,需要确认消耗次数 ========== + + // 读取申请时的 VIP 等级 + Integer applyVipLevelId = record.getApplyVipLevelId(); + + // 兼容旧数据(如果没有 applyVipLevelId 字段,fallback 到当前 VIP 等级) + if (applyVipLevelId == null || applyVipLevelId == 0) { + TtUser currentUser = userService.getById(record.getUserId()); + applyVipLevelId = currentUser != null && currentUser.getVipLevel() != null + ? currentUser.getVipLevel() : 0; + } + + // 将 Date 转为 LocalDate + LocalDate applyDate = record.getCreateTime().toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate(); + + // 确认消耗(在【申请时 VIP】的记录上操作) + boolean success = userTransferDailyService.confirmUsedDelivery( + record.getUserId(), + applyVipLevelId, + applyDate + ); + + if (!success) { + return "确认消耗次数失败"; + } + + // 6. 标记该批次次数已处理(数据库幂等兜底) + this.lambdaUpdate() + .eq(TtDeliveryRecord::getBatchId, batchId) + .set(TtDeliveryRecord::getTimesDeducted, 1) + .update(); + + // 7. 数据库操作成功后再标记 Redis(短期快速防重) + redisTemplate.opsForValue().set(processedKey, true, 7, java.util.concurrent.TimeUnit.DAYS); + } + + return ""; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String deliveryFail(Integer deliveryRecordId, String message) { + + TtDeliveryRecord record = this.getById(deliveryRecordId); + if (record == null) { + return "提货申请记录不存在"; + } + + // 1. 更新当前记录状态为取消 + record.setStatus(DeliveryOrderStatus.ORDER_CANCEL.getCode()); + record.setMessage(DeliveryOrderStatus.ORDER_CANCEL.getMsg()); + record.setUpdateBy(SecurityUtils.getUsername()); + record.setUpdateTime(DateUtils.getNowDate()); + this.updateById(record); + + // 2. 查询同批次的所有记录 + String batchId = record.getBatchId(); + if (batchId != null && !batchId.isEmpty()) { + + // 2.1 数据库幂等:该批次次数是否已处理过(防止 Redis 过期后重复释放) + long alreadyDeductedCount = this.lambdaQuery() + .eq(TtDeliveryRecord::getBatchId, batchId) + .eq(TtDeliveryRecord::getTimesDeducted, 1) + .count(); + if (alreadyDeductedCount > 0) { + return ""; + } + + List batchRecords = this.lambdaQuery() + .eq(TtDeliveryRecord::getBatchId, batchId) + .orderByAsc(TtDeliveryRecord::getId) + .list(); + + // 3. 判断是否所有订单都取消了 + boolean allCancelled = batchRecords.stream() + .allMatch(r -> DeliveryOrderStatus.ORDER_CANCEL.getCode().equals(r.getStatus())); + + if (allCancelled) { + // ========== 所有都驳回了,释放次数 ========== + + // 读取申请时的 VIP 等级 + Integer applyVipLevelId = record.getApplyVipLevelId(); + + // 兼容旧数据(如果没有 applyVipLevelId 字段,fallback 到当前 VIP 等级) + if (applyVipLevelId == null || applyVipLevelId == 0) { + TtUser currentUser = userService.getById(record.getUserId()); + applyVipLevelId = currentUser != null && currentUser.getVipLevel() != null + ? currentUser.getVipLevel() : 0; + } + + // 将 Date 转为 LocalDate + LocalDate applyDate = record.getCreateTime().toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate(); + + // 释放 pending_count(仅清空冻结,不动 total 和 used) + boolean success = userTransferDailyService.releasePendingDelivery( + record.getUserId(), + applyVipLevelId, + applyDate + ); + + if (!success) { + return "释放次数失败"; + } + + // 标记该批次次数已处理(数据库幂等兜底) + this.lambdaUpdate() + .eq(TtDeliveryRecord::getBatchId, batchId) + .set(TtDeliveryRecord::getTimesDeducted, 1) + .update(); + + // 清除 Redis 短期标记(如果有的话) + String processedKey = "delivery:batch:processed:" + batchId; + redisTemplate.delete(processedKey); + } + } + + // 退回申请不再修改 box_records.status: + // 1) 「我的库存」/「提货背包」的列表及统计均靠 NOT EXISTS 过滤 delivery_record.status != 11, + // 退回后 delivery_record.status=11,饰品会自然重新出现。 + // 2) 保持 box_records.status 不动(例如商城购买饰品仍为 2),可让「提货背包」(status=2) 同步展示, + // 避免原本退回后只出现在「我的库存」、「提货背包」不出现的问题。 + return ""; + } + + @Override + public List getDeliveryRecordList(TtDeliveryRecordBody deliveryRecordBody) { + return baseMapper.getDeliveryRecordList(deliveryRecordBody); + } + + @Override + public List getDeliveryRecordByUserList(TtDeliveryRecordBody deliveryRecordBody) { + return baseMapper.getDeliveryRecordByUserList(deliveryRecordBody); + } + + @Override + public List byCondition(DeliveryRecordsConfition param) { + + param.setLimit((param.getPage()-1)*param.getSize()); + + List list = baseMapper.byCondition( + param.getStatusList(), + param.getUIdList(), + param.getLimit(), + param.getSize()); + + return list; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFightRankingRewardServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFightRankingRewardServiceImpl.java new file mode 100644 index 0000000..37ef7db --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFightRankingRewardServiceImpl.java @@ -0,0 +1,115 @@ +package com.ruoyi.admin.service.impl; + +import java.math.BigDecimal; +import java.util.List; + +import com.ruoyi.admin.service.TtFightRankingRewardService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.admin.mapper.TtFightRankingRewardMapper; +import com.ruoyi.domain.other.TtFightRankingReward; +import org.springframework.transaction.annotation.Transactional; + +/** + * 对战奖励金额Service业务层处理 + * + * @author ruoyi + * @date 2024-07-04 + */ +@Service +public class TtFightRankingRewardServiceImpl implements TtFightRankingRewardService +{ + @Autowired + private TtFightRankingRewardMapper ttFightRankingRewardMapper; + + /** + * 查询对战奖励金额 + * + * @param id 对战奖励金额主键 + * @return 对战奖励金额 + */ + @Override + public TtFightRankingReward selectTtFightRankingRewardById(Integer id) + { + return ttFightRankingRewardMapper.selectTtFightRankingRewardById(id); + } + + /** + * 查询对战奖励金额列表 + * + * @param ttFightRankingReward 对战奖励金额 + * @return 对战奖励金额 + */ + @Override + public List selectTtFightRankingRewardList(TtFightRankingReward ttFightRankingReward) + { + return ttFightRankingRewardMapper.selectTtFightRankingRewardList(ttFightRankingReward); + } + + @Override + @Transactional + public String generateRankingReward() + { + ttFightRankingRewardMapper.truncateRankingReward(); + for (int i = 1; i <= 10; i++) { + TtFightRankingReward ttFightRankingReward = new TtFightRankingReward(); + ttFightRankingReward.setName("TOP" + i); + ttFightRankingReward.setReward(BigDecimal.ZERO); + ttFightRankingRewardMapper.insertTtFightRankingReward(ttFightRankingReward); + } + return ""; + } + + // /** + // * 新增对战奖励金额 + // * + // * @param ttFightRankingReward 对战奖励金额 + // * @return 结果 + // */ + // @Override + // public int insertTtFightRankingReward(TtFightRankingReward ttFightRankingReward) + // { + // return ttFightRankingRewardMapper.insertTtFightRankingReward(ttFightRankingReward); + // } + + /** + * 修改对战奖励金额 + * + * @param ttFightRankingReward 对战奖励金额 + * @return 结果 + */ + @Override + public int updateTtFightRankingReward(TtFightRankingReward ttFightRankingReward) + { + return ttFightRankingRewardMapper.updateTtFightRankingReward(ttFightRankingReward); + } + + /** + * 批量删除对战奖励金额 + * + * @param ids 需要删除的对战奖励金额主键 + * @return 结果 + */ + @Override + public int deleteTtFightRankingRewardByIds(Integer[] ids) + { + return ttFightRankingRewardMapper.deleteTtFightRankingRewardByIds(ids); + } + + @Override + public void truncateRankingReward() { + ttFightRankingRewardMapper.truncateRankingReward(); + } + + // /** + // * 删除对战奖励金额信息 + // * + // * @param id 对战奖励金额主键 + // * @return 结果 + // */ + // @Override + // public int deleteTtFightRankingRewardById(Integer id) + // { + // return ttFightRankingRewardMapper.deleteTtFightRankingRewardById(id); + // } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFightResultServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFightResultServiceImpl.java new file mode 100644 index 0000000..0abfcb9 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFightResultServiceImpl.java @@ -0,0 +1,533 @@ +package com.ruoyi.admin.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtFightMapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtFightService; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.fight.FightSeat; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtFightResult; +import com.ruoyi.admin.mapper.TtFightResultMapper; +import com.ruoyi.admin.service.TtFightResultService; +import com.ruoyi.domain.vo.FightResultDataVO; +import com.ruoyi.domain.vo.fight.FightBoxVO; +import com.ruoyi.domain.vo.fight.FightResultVO; +import com.ruoyi.domain.vo.fight.FightState; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class TtFightResultServiceImpl extends ServiceImpl implements TtFightResultService { + + private static final Comparator ORNAMENT_PRICE_DESC = Comparator + .comparing(TtBoxRecords::getOrnamentsPrice, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(TtBoxRecords::getId, Comparator.nullsLast(Comparator.reverseOrder())); + + private final TtFightMapper fightMapper; + + public TtFightResultServiceImpl(TtFightMapper fightMapper) { + this.fightMapper = fightMapper; + } + + @Autowired + private TtFightService ttFightService; + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private TtBoxMapper boxMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + /** + * 磨损度归一化:仅保留 5 种标准磨损度,其余任何值(如"普通"、空字符串、null)统一为"崭新出厂"。 + */ + private static String normalizeExterior(String exterior) { + if (exterior == null) return "崭新出厂"; + boolean isStandard = "崭新出厂".equals(exterior) + || "略有磨损".equals(exterior) + || "久经沙场".equals(exterior) + || "破损不堪".equals(exterior) + || "战痕累累".equals(exterior); + return isStandard ? exterior : "崭新出厂"; + } + + @Autowired + private RedisCache redisCache; + + // @Override + // public FightResultDataVO getFightResult(Integer fightId) { + // + // // new LambdaUpdateChainWrapper<>(fightMapper).eq(TtFight::getId, fightId).set(TtFight::getStatus, "2") + // // .set(TtFight::getUpdateTime, new Date()).update(); + // // TtFightResult fightResult = new LambdaQueryChainWrapper<>(baseMapper).eq(TtFightResult::getFightId, fightId).one(); + // + // LambdaUpdateWrapper ttFightUpdate = new LambdaUpdateWrapper<>(); + // ttFightUpdate + // .eq(TtFight::getId, fightId).set(TtFight::getStatus, "2") + // .set(TtFight::getUpdateTime, new Date()); + // ttFightService.update(ttFightUpdate); + // + // LambdaQueryWrapper ttFightResultQuery = new LambdaQueryWrapper<>(); + // ttFightResultQuery.eq(TtFightResult::getFightId, fightId); + // TtFightResult fightResult = getOne(ttFightResultQuery); + // + // log.info("fightId="+fightId); + // String fightResultStr = fightResult.getFightResult(); + // return JSONObject.parseObject(fightResultStr, FightResultDataVO.class); + // } + + @Override + public FightResultVO getFightResult(Integer fightId) { + LambdaQueryWrapper fightQuery = new LambdaQueryWrapper<>(); + fightQuery + .eq(TtFight::getId, fightId); + TtFight fight = ttFightService.getOne(fightQuery); + + // 补充 seats 中玩家的 vipLevel + if (fight.getSeats() != null && !fight.getSeats().isEmpty()) { + List playerIds = fight.getSeats().stream() + .map(FightSeat::getPlayerId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (!playerIds.isEmpty()) { + List users = new LambdaQueryChainWrapper<>(ttUserMapper) + .in(TtUser::getUserId, playerIds) + .select(TtUser::getUserId, TtUser::getVipLevel) + .list(); + Map vipMap = users.stream() + .collect(Collectors.toMap(TtUser::getUserId, u -> u.getVipLevel() != null && u.getVipLevel() > 0 ? u.getVipLevel() - 1 : 0, (a, b) -> a)); + for (FightSeat seat : fight.getSeats()) { + if (seat.getPlayerId() != null) { + seat.setVipLevel(vipMap.getOrDefault(seat.getPlayerId(), 0)); + } + } + } + } + + // 收集本场对战的所有玩家ID + List fightPlayerIds = fight.getSeats().stream() + .map(FightSeat::getPlayerId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + LambdaQueryWrapper boxRecordsQuery = new LambdaQueryWrapper<>(); + boxRecordsQuery + .eq(TtBoxRecords::getFightId, fightId) + .in(TtBoxRecords::getUserId, fightPlayerIds); + // 按对战开始时间过滤,排除同 fight_id 的旧数据 + if (fight.getCreateTime() != null) { + boxRecordsQuery.ge(TtBoxRecords::getCreateTime, fight.getCreateTime()); + } + List allBoxRecords = boxRecordsService.list(boxRecordsQuery); + + // 过滤:回合记录不超过实际总回合数(防止旧数据残留) + int maxRound = fight.getRoundNumber() != null ? fight.getRoundNumber() : 0; + allBoxRecords = allBoxRecords.stream() + .filter(r -> r.getFightRoundNumber() == null || r.getFightRoundNumber() <= maxRound) + .collect(Collectors.toList()); + + // 按持有人汇总饰品金额,填充座位 awardTotalPrices(观战/历史查询场景兜底) + Map holderPriceMap = allBoxRecords.stream() + .filter(r -> r.getHolderUserId() != null && r.getOrnamentsPrice() != null) + .collect(Collectors.groupingBy( + TtBoxRecords::getHolderUserId, + Collectors.mapping(TtBoxRecords::getOrnamentsPrice, + Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)))); + if (fight.getSeats() != null) { + for (FightSeat seat : fight.getSeats()) { + if (seat.getPlayerId() != null) { + seat.setAwardTotalPrices(holderPriceMap.getOrDefault(seat.getPlayerId(), BigDecimal.ZERO)); + } + } + } + + Map boxData = fight.getBoxData(); + ArrayList fightBoxVOList = new ArrayList<>(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxData.keySet()); + List boxes = boxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + Map boxNameMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, b -> b.getBoxName() != null ? b.getBoxName() : "")); + boxData.keySet().stream() + .sorted(Comparator.comparingInt(Integer::parseInt)) + .forEach(boxId -> { + Object value =boxData.get(boxId); + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + FightBoxVO vo = new FightBoxVO(map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), BigDecimal.ZERO)); + vo.setBoxName(boxNameMap.getOrDefault(vo.getBoxId(), "")); + fightBoxVOList.add(vo); + } else { + FightBoxVO vo = boxData.get(boxId); + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), BigDecimal.ZERO)); + vo.setBoxName(boxNameMap.getOrDefault(vo.getBoxId(), "")); + fightBoxVOList.add(vo); + } + }); + + // 分离正常回合结果和赛后奖励(安慰奖/平分奖) + List roundResults = allBoxRecords.stream() + .filter(r -> r.getFightRoundNumber() != null && r.getFightRoundNumber() > 0) + .collect(Collectors.toList()); + List endGameRecords = allBoxRecords.stream() + .filter(r -> r.getFightRoundNumber() == null || r.getFightRoundNumber() <= 0) + .collect(Collectors.toList()); + + // 胜利方获得的回合饰品(排除赛后奖励) + List winnerIds = fight.getWinnerIds() != null ? fight.getWinnerIds() : new ArrayList<>(); + // 数据库 winner_ids 为空时,从 Redis 对战状态快照兜底(对战进行中/刚结束时DB可能未就绪) + if (winnerIds.isEmpty()) { + try { + FightState state = redisCache.getCacheObject("fight_state:" + fightId); + if (state != null && state.getWinnerIds() != null && !state.getWinnerIds().isEmpty()) { + winnerIds = state.getWinnerIds(); + log.warn("对战{} DB中winner_ids为空,已从Redis状态快照兜底: {}, fight.status={}", fightId, winnerIds, fight.getStatus()); + } else { + log.warn("对战{} DB与Redis中winner_ids均为空, fight.status={}", fightId, fight.getStatus()); + } + } catch (Exception e) { + log.warn("对战{} Redis兜底获取winnerIds失败: {}", fightId, e.getMessage()); + } + } + final List finalWinnerIds = winnerIds; + List winnerPrize = roundResults.stream() + .filter(r -> r.getHolderUserId() != null && finalWinnerIds.contains(r.getHolderUserId())) + .filter(r -> r.getOrnamentId() != null) + .collect(Collectors.toList()); + // 平分奖:赛后奖励中持有人为赢家的记录(多赢家+有输家时输家饰品价值平分给赢家),计入赢家所得 + winnerPrize.addAll(endGameRecords.stream() + .filter(r -> r.getHolderUserId() != null && finalWinnerIds.contains(r.getHolderUserId())) + .filter(r -> r.getOrnamentId() != null) + .collect(Collectors.toList())); + + // 失败方安慰奖(赛后奖励中属于非赢家的) + List endGamePrize = endGameRecords.stream() + .filter(r -> r.getHolderUserId() != null && !finalWinnerIds.contains(r.getHolderUserId())) + .collect(Collectors.toList()); + + // 批量填充 exteriorName + List allPrizeRecords = new ArrayList<>(); + allPrizeRecords.addAll(winnerPrize); + allPrizeRecords.addAll(endGamePrize); + List ornamentIds = allPrizeRecords.stream() + .map(TtBoxRecords::getOrnamentId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (!ornamentIds.isEmpty()) { + List ornaments = new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .in(TtOrnament::getId, ornamentIds) + .select(TtOrnament::getId, TtOrnament::getExteriorName) + .list(); + Map exteriorNameMap = ornaments.stream() + .collect(Collectors.toMap(TtOrnament::getId, + o -> normalizeExterior(o.getExteriorName()), + (a, b) -> a)); + allPrizeRecords.forEach(r -> { + if (r.getOrnamentId() != null) { + r.setExteriorName(exteriorNameMap.getOrDefault(r.getOrnamentId(), "崭新出厂")); + } + }); + } + + // 获得的饰品按价格降序,空价格放最后 + roundResults.sort(ORNAMENT_PRICE_DESC); + winnerPrize.sort(ORNAMENT_PRICE_DESC); + endGamePrize.sort(ORNAMENT_PRICE_DESC); + + // 计算金额 + BigDecimal winnerTotalPrice = winnerPrize.stream() + .map(r -> r.getOrnamentsPrice() != null ? r.getOrnamentsPrice() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal loserTotalPrice = endGamePrize.stream() + .map(r -> r.getOrnamentsPrice() != null ? r.getOrnamentsPrice() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal totalOrnamentsPrice = allBoxRecords.stream() + .filter(r -> r.getOrnamentsPrice() != null) + .map(TtBoxRecords::getOrnamentsPrice) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + FightResultVO resultVO = FightResultVO.builder() + .fight(fight) + .winnerIds(winnerIds) + .fightResult(roundResults) + .winnerPrize(winnerPrize) + .endGamePrize(endGamePrize) + .winnerTotalPrice(winnerTotalPrice) + .loserTotalPrice(loserTotalPrice) + .totalOrnamentsPrice(totalOrnamentsPrice) + .fightBoxVOList(fightBoxVOList) + .build(); + return resultVO; + } + + @Override + public List getFightResultBatch(List fightIds) { + if (fightIds == null || fightIds.isEmpty()) { + return Collections.emptyList(); + } + + // 1. 批量查询所有 TtFight + LambdaQueryWrapper fightQuery = new LambdaQueryWrapper<>(); + fightQuery.in(TtFight::getId, fightIds); + List fights = ttFightService.list(fightQuery); + if (fights.isEmpty()) { + return Collections.emptyList(); + } + Map fightMap = fights.stream() + .collect(Collectors.toMap(TtFight::getId, f -> f, (a, b) -> a)); + + // 2. 批量查询所有座位玩家的 vipLevel + List allPlayerIds = fights.stream() + .filter(f -> f.getSeats() != null) + .flatMap(f -> f.getSeats().stream()) + .map(FightSeat::getPlayerId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + Map vipMap = new HashMap<>(); + if (!allPlayerIds.isEmpty()) { + List users = new LambdaQueryChainWrapper<>(ttUserMapper) + .in(TtUser::getUserId, allPlayerIds) + .select(TtUser::getUserId, TtUser::getVipLevel) + .list(); + vipMap = users.stream() + .collect(Collectors.toMap(TtUser::getUserId, + u -> u.getVipLevel() != null && u.getVipLevel() > 0 ? u.getVipLevel() - 1 : 0, (a, b) -> a)); + } + for (TtFight fight : fights) { + if (fight.getSeats() != null) { + for (FightSeat seat : fight.getSeats()) { + if (seat.getPlayerId() != null) { + seat.setVipLevel(vipMap.getOrDefault(seat.getPlayerId(), 0)); + } + } + } + } + + // 3. 批量查询所有 TtBoxRecords(按 fight_id 分组,只取本场玩家的记录) + LambdaQueryWrapper boxRecordsQuery = new LambdaQueryWrapper<>(); + boxRecordsQuery.in(TtBoxRecords::getFightId, fightIds) + .in(TtBoxRecords::getUserId, allPlayerIds); + List allBoxRecords = boxRecordsService.list(boxRecordsQuery); + + // 过滤旧数据:回合数不超过对战实际总回合数 + 时间过滤 + allBoxRecords = allBoxRecords.stream() + .filter(r -> { + TtFight f = fightMap.get(r.getFightId()); + if (f == null) return false; + // 时间过滤:只保留对战创建时间之后的记录 + if (f.getCreateTime() != null && r.getCreateTime() != null + && r.getCreateTime().before(f.getCreateTime())) { + return false; + } + int maxRound = f.getRoundNumber() != null ? f.getRoundNumber() : 0; + return r.getFightRoundNumber() == null || r.getFightRoundNumber() <= maxRound; + }) + .collect(Collectors.toList()); + + Map> recordsByFightId = allBoxRecords.stream() + .collect(Collectors.groupingBy(TtBoxRecords::getFightId)); + + // 4. 批量查询所有宝箱价格 + List allBoxIds = fights.stream() + .filter(f -> f.getBoxData() != null && !f.getBoxData().isEmpty()) + .flatMap(f -> f.getBoxData().keySet().stream()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + Map boxPriceMap = new HashMap<>(); + Map batchBoxNameMap = new HashMap<>(); + if (!allBoxIds.isEmpty()) { + LambdaQueryWrapper boxQuery = new LambdaQueryWrapper<>(); + boxQuery.in(TtBox::getBoxId, allBoxIds); + List boxes = boxMapper.selectList(boxQuery); + boxPriceMap = boxes.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice, (a, b) -> a)); + batchBoxNameMap = boxes.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, b -> b.getBoxName() != null ? b.getBoxName() : "", (a, b) -> a)); + } + + // 5. 批量查询所有饰品 exteriorName + List allOrnamentIds = allBoxRecords.stream() + .filter(r -> r.getOrnamentId() != null) + .map(TtBoxRecords::getOrnamentId) + .distinct() + .collect(Collectors.toList()); + Map exteriorNameMap = new HashMap<>(); + if (!allOrnamentIds.isEmpty()) { + List ornaments = new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .in(TtOrnament::getId, allOrnamentIds) + .select(TtOrnament::getId, TtOrnament::getExteriorName) + .list(); + exteriorNameMap = ornaments.stream() + .collect(Collectors.toMap(TtOrnament::getId, + o -> normalizeExterior(o.getExteriorName()), + (a, b) -> a)); + } + + // 6. 逐个构建 FightResultVO + List result = new ArrayList<>(); + for (Integer fightId : fightIds) { + TtFight fight = fightMap.get(fightId); + if (fight == null) { + continue; + } + + List fightBoxRecords = recordsByFightId.getOrDefault(fightId, Collections.emptyList()); + + // 按持有人汇总饰品金额,填充座位 awardTotalPrices(观战/历史查询场景兜底) + Map holderPriceMap = fightBoxRecords.stream() + .filter(r -> r.getHolderUserId() != null && r.getOrnamentsPrice() != null) + .collect(Collectors.groupingBy( + TtBoxRecords::getHolderUserId, + Collectors.mapping(TtBoxRecords::getOrnamentsPrice, + Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)))); + if (fight.getSeats() != null) { + for (FightSeat seat : fight.getSeats()) { + if (seat.getPlayerId() != null) { + seat.setAwardTotalPrices(holderPriceMap.getOrDefault(seat.getPlayerId(), BigDecimal.ZERO)); + } + } + } + + // 构建 fightBoxVOList + Map boxData = fight.getBoxData(); + ArrayList fightBoxVOList = new ArrayList<>(); + if (boxData != null) { + final Map finalBoxPriceMap = boxPriceMap; + final Map finalBoxNameMap = batchBoxNameMap; + boxData.keySet().stream() + .sorted(Comparator.comparingInt(Integer::parseInt)) + .forEach(boxId -> { + Object value = boxData.get(boxId); + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + FightBoxVO vo = new FightBoxVO( + map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + vo.setPrice(finalBoxPriceMap.getOrDefault(vo.getBoxId(), BigDecimal.ZERO)); + vo.setBoxName(finalBoxNameMap.getOrDefault(vo.getBoxId(), "")); + fightBoxVOList.add(vo); + } else { + FightBoxVO vo = boxData.get(boxId); + vo.setPrice(finalBoxPriceMap.getOrDefault(vo.getBoxId(), BigDecimal.ZERO)); + vo.setBoxName(finalBoxNameMap.getOrDefault(vo.getBoxId(), "")); + fightBoxVOList.add(vo); + } + }); + } + + // 分离正常回合结果和赛后奖励 + List roundResults = fightBoxRecords.stream() + .filter(r -> r.getFightRoundNumber() != null && r.getFightRoundNumber() > 0) + .collect(Collectors.toList()); + List endGameRecords = fightBoxRecords.stream() + .filter(r -> r.getFightRoundNumber() == null || r.getFightRoundNumber() <= 0) + .collect(Collectors.toList()); + + // 胜利方获得的回合饰品(排除赛后奖励) + List winnerIds = fight.getWinnerIds() != null ? fight.getWinnerIds() : new ArrayList<>(); + // 数据库 winner_ids 为空时,从 Redis 对战状态快照兜底(单个 fight 处理,异常不影响批量流程) + if (winnerIds.isEmpty()) { + try { + FightState state = redisCache.getCacheObject("fight_state:" + fightId); + if (state != null && state.getWinnerIds() != null && !state.getWinnerIds().isEmpty()) { + winnerIds = state.getWinnerIds(); + log.warn("对战{} DB中winner_ids为空,已从Redis状态快照兜底获取: {}", fightId, winnerIds); + } + } catch (Exception e) { + log.warn("对战{} 从Redis获取winnerIds兜底失败: {}", fightId, e.getMessage()); + } + } + final List finalWinnerIds = winnerIds; + List winnerPrize = roundResults.stream() + .filter(r -> r.getHolderUserId() != null && finalWinnerIds.contains(r.getHolderUserId())) + .filter(r -> r.getOrnamentId() != null) + .collect(Collectors.toList()); + // 平分奖:赛后奖励中持有人为赢家的记录,计入赢家所得 + winnerPrize.addAll(endGameRecords.stream() + .filter(r -> r.getHolderUserId() != null && finalWinnerIds.contains(r.getHolderUserId())) + .filter(r -> r.getOrnamentId() != null) + .collect(Collectors.toList())); + + // 失败方安慰奖 + List endGamePrize = endGameRecords.stream() + .filter(r -> r.getHolderUserId() != null && !finalWinnerIds.contains(r.getHolderUserId())) + .collect(Collectors.toList()); + + // 填充 exteriorName + List allPrizeRecords = new ArrayList<>(); + allPrizeRecords.addAll(winnerPrize); + allPrizeRecords.addAll(endGamePrize); + final Map finalExteriorNameMap = exteriorNameMap; + allPrizeRecords.forEach(r -> { + if (r.getOrnamentId() != null) { + r.setExteriorName(finalExteriorNameMap.getOrDefault(r.getOrnamentId(), "崭新出厂")); + } + }); + + // 获得的饰品按价格降序,空价格放最后 + roundResults.sort(ORNAMENT_PRICE_DESC); + winnerPrize.sort(ORNAMENT_PRICE_DESC); + endGamePrize.sort(ORNAMENT_PRICE_DESC); + + // 计算金额 + BigDecimal winnerTotalPrice = winnerPrize.stream() + .map(r -> r.getOrnamentsPrice() != null ? r.getOrnamentsPrice() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal loserTotalPrice = endGamePrize.stream() + .map(r -> r.getOrnamentsPrice() != null ? r.getOrnamentsPrice() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal totalOrnamentsPrice = fightBoxRecords.stream() + .filter(r -> r.getOrnamentsPrice() != null) + .map(TtBoxRecords::getOrnamentsPrice) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + FightResultVO resultVO = FightResultVO.builder() + .fight(fight) + .winnerIds(winnerIds) + .fightResult(roundResults) + .winnerPrize(winnerPrize) + .endGamePrize(endGamePrize) + .winnerTotalPrice(winnerTotalPrice) + .loserTotalPrice(loserTotalPrice) + .totalOrnamentsPrice(totalOrnamentsPrice) + .fightBoxVOList(fightBoxVOList) + .build(); + result.add(resultVO); + } + return result; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFightServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFightServiceImpl.java new file mode 100644 index 0000000..d93c106 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFightServiceImpl.java @@ -0,0 +1,108 @@ +package com.ruoyi.admin.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtFightMapper; +import com.ruoyi.admin.service.TtFightService; +import com.ruoyi.domain.vo.FightBoxDataVO; +import com.ruoyi.domain.other.TtFightBody; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.domain.other.BoxDataBodyA; +import com.ruoyi.domain.vo.fight.FightBoxVO; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +public class TtFightServiceImpl extends ServiceImpl implements TtFightService { + + private final TtBoxMapper boxMapper; + + private final TtFightMapper ttFightMapper; + + public TtFightServiceImpl(TtBoxMapper boxMapper,TtFightMapper ttFightMapper) { + this.boxMapper = boxMapper; + this.ttFightMapper = ttFightMapper; + } + + @Override + public List selectFightList(TtFightBody ttFightBody) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotNull(ttFightBody.getId())) wrapper.eq(TtFight::getId, ttFightBody.getId()); + if (StringUtils.isNotEmpty(ttFightBody.getStatus())) wrapper.eq(TtFight::getStatus, ttFightBody.getStatus()); + if (StringUtils.isNotNull(ttFightBody.getUserId())) wrapper.eq(TtFight::getUserId, ttFightBody.getUserId()); + // 排除白号发起的对战 + wrapper.notInSql(TtFight::getUserId, "SELECT user_id FROM tt_user WHERE is_blank_account = 1"); + wrapper.orderByDesc(TtFight::getCreateTime); + return this.list(wrapper); + } + + // @Override + // public List selectFightBoxList(Integer fightId) { + // TtFight ttFight = this.getById(fightId); + // String boxData = JSON.toJSONString(ttFight.getBoxData()); + // List boxDataBodyAList = JSONObject.parseObject(boxData, new TypeReference>() { + // }); + // List resultList = new ArrayList<>(); + // for (BoxDataBodyA boxDataBodyA : boxDataBodyAList) { + // System.err.println(boxDataBodyA); + // // 这里输出的全是BoxDataBodyA(boxId=null, boxNum=null, boxName=null, boxImg01=null, boxImg02=null) + // TtBox ttBox = new LambdaQueryChainWrapper<>(boxMapper).eq(TtBox::getBoxId, boxDataBodyA.getBoxId()).one(); + // for (int i = 0; i < boxDataBodyA.getBoxNum(); i++) { + // FightBoxDataVO fightBoxDataVO = FightBoxDataVO.builder().build(); + // BeanUtils.copyBeanProp(fightBoxDataVO, ttBox); + // resultList.add(fightBoxDataVO); + // } + // } + // return resultList; + // } + + @Override + public List selectFightBoxList(Integer fightId) { + TtFight ttFight = this.getById(fightId); + String boxDataJson = JSON.toJSONString(ttFight.getBoxData()); + + // 解析 JSON 字符串为 Map + Map boxDataMap = JSONObject.parseObject(boxDataJson, new TypeReference>() {}); + + List resultList = new ArrayList<>(); + boxDataMap.forEach((boxId, boxDataBodyA) -> { + + // 根据 boxId 查询对应的 TtBox 对象 + TtBox ttBox = new LambdaQueryChainWrapper<>(boxMapper) + .eq(TtBox::getBoxId, boxDataBodyA.getBoxId()) + .one(); + + if (ttBox != null) { + // 根据 boxNum 创建多个 FightBoxDataVO 对象 + for (int i = 0; i < boxDataBodyA.getNumber(); i++) { + FightBoxDataVO fightBoxDataVO = new FightBoxDataVO(); + BeanUtils.copyProperties(ttBox, fightBoxDataVO); + resultList.add(fightBoxDataVO); + } + } + }); + + return resultList; + } + + @Override + public int endFight(String fightId) { + TtFight ttFight = ttFightMapper.selectOne(new QueryWrapper().eq("fight_id", fightId)); + ttFight.setStatus(2); + return ttFightMapper.updateById(ttFight); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFirstRechargeServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFirstRechargeServiceImpl.java new file mode 100644 index 0000000..5caf9f2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtFirstRechargeServiceImpl.java @@ -0,0 +1,96 @@ +package com.ruoyi.admin.service.impl; + +import java.util.List; +import com.ruoyi.common.utils.DateUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.admin.mapper.TtFirstRechargeMapper; +import com.ruoyi.domain.other.TtFirstRecharge; +import com.ruoyi.admin.service.TtFirstRechargeService; + +/** + * 首充赠送Service业务层处理 + * + * @author ruoyi + * @date 2024-06-21 + */ +@Service +public class TtFirstRechargeServiceImpl implements TtFirstRechargeService +{ + @Autowired + private TtFirstRechargeMapper ttFirstRechargeMapper; + + /** + * 查询首充赠送 + * + * @param id 首充赠送主键 + * @return 首充赠送 + */ + @Override + public TtFirstRecharge selectTtFirstRechargeById(Integer id) + { + return ttFirstRechargeMapper.selectTtFirstRechargeById(id); + } + + /** + * 查询首充赠送列表 + * + * @param ttFirstRecharge 首充赠送 + * @return 首充赠送 + */ + @Override + public List selectTtFirstRechargeList(TtFirstRecharge ttFirstRecharge) + { + return ttFirstRechargeMapper.selectTtFirstRechargeList(ttFirstRecharge); + } + + /** + * 新增首充赠送 + * + * @param ttFirstRecharge 首充赠送 + * @return 结果 + */ + @Override + public int insertTtFirstRecharge(TtFirstRecharge ttFirstRecharge) + { + ttFirstRecharge.setCreateTime(DateUtils.getNowDate()); + return ttFirstRechargeMapper.insertTtFirstRecharge(ttFirstRecharge); + } + + /** + * 修改首充赠送 + * + * @param ttFirstRecharge 首充赠送 + * @return 结果 + */ + @Override + public int updateTtFirstRecharge(TtFirstRecharge ttFirstRecharge) + { + ttFirstRecharge.setUpdateTime(DateUtils.getNowDate()); + return ttFirstRechargeMapper.updateTtFirstRecharge(ttFirstRecharge); + } + + /** + * 批量删除首充赠送 + * + * @param ids 需要删除的首充赠送主键 + * @return 结果 + */ + @Override + public int deleteTtFirstRechargeByIds(Integer[] ids) + { + return ttFirstRechargeMapper.deleteTtFirstRechargeByIds(ids); + } + + /** + * 删除首充赠送信息 + * + * @param id 首充赠送主键 + * @return 结果 + */ + @Override + public int deleteTtFirstRechargeById(Integer id) + { + return ttFirstRechargeMapper.deleteTtFirstRechargeById(id); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtMessageSendServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtMessageSendServiceImpl.java new file mode 100644 index 0000000..49e101c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtMessageSendServiceImpl.java @@ -0,0 +1,11 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtMessageSend; +import com.ruoyi.admin.mapper.TtMessageSendMapper; +import com.ruoyi.admin.service.TtMessageSendService; +import org.springframework.stereotype.Service; + +@Service +public class TtMessageSendServiceImpl extends ServiceImpl implements TtMessageSendService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtMessageServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtMessageServiceImpl.java new file mode 100644 index 0000000..99a5320 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtMessageServiceImpl.java @@ -0,0 +1,74 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtMessage; +import com.ruoyi.domain.other.TtMessageSend; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.mapper.TtMessageMapper; +import com.ruoyi.admin.service.TtMessageSendService; +import com.ruoyi.admin.service.TtMessageService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class TtMessageServiceImpl extends ServiceImpl implements TtMessageService { + + private final TtUserService userService; + private final TtMessageSendService messageSendService; + + public TtMessageServiceImpl(TtUserService userService, + TtMessageSendService messageSendService) { + this.userService = userService; + this.messageSendService = messageSendService; + } + + @Override + public List queryList(TtMessage ttMessage) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotNull(ttMessage.getId())) wrapper.eq(TtMessage::getId, ttMessage.getId()); + if (StringUtils.isNotEmpty(ttMessage.getMessage())) wrapper.like(TtMessage::getMessage, ttMessage.getMessage()); + return this.list(wrapper); + } + + @Override + public String delByIds(List ids) { + this.removeByIds(ids); + return ""; + } + + @Override + public String singleMessage(Integer messageId, List userIds) { + List messageSends = new ArrayList<>(); + for (Integer userId : userIds) { + TtMessageSend messageSend = TtMessageSend.builder().build(); + messageSend.setRecId(userId); + messageSend.setMessageId(messageId); + messageSend.setSendTime(DateUtils.getNowDate()); + messageSends.add(messageSend); + } + messageSendService.saveBatch(messageSends, 1); + return ""; + } + + @Override + public String massMessaging(Integer messageId) { + List list = userService.list(); + List messageSends = new ArrayList<>(); + for (TtUser ttUser : list) { + TtMessageSend messageSend = TtMessageSend.builder().build(); + messageSend.setRecId(ttUser.getUserId()); + messageSend.setMessageId(messageId); + messageSend.setSendTime(DateUtils.getNowDate()); + messageSends.add(messageSend); + } + messageSendService.saveBatch(messageSends, 1); + return ""; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOfficialRollConfigServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOfficialRollConfigServiceImpl.java new file mode 100644 index 0000000..0f56381 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOfficialRollConfigServiceImpl.java @@ -0,0 +1,77 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtOfficialRollConfigMapper; +import com.ruoyi.admin.mapper.TtRollJackpotMapper; +import com.ruoyi.admin.service.TtOfficialRollConfigService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.roll.TtOfficialRollConfig; +import com.ruoyi.domain.entity.roll.TtRollJackpot; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class TtOfficialRollConfigServiceImpl + extends ServiceImpl + implements TtOfficialRollConfigService { + + /** 周期类型:每日 */ + public static final String PERIOD_DAILY = "DAILY"; + /** 周期类型:每周 */ + public static final String PERIOD_WEEKLY = "WEEKLY"; + + @Autowired + private TtRollJackpotMapper ttRollJackpotMapper; + + @Override + public AjaxResult addConfig(TtOfficialRollConfig config) { + AjaxResult check = validate(config); + if (check != null) return check; + normalize(config); + return save(config) ? AjaxResult.success() : AjaxResult.error("新增失败"); + } + + @Override + public AjaxResult editConfig(TtOfficialRollConfig config) { + if (config.getId() == null) return AjaxResult.error("配置ID不能为空"); + AjaxResult check = validate(config); + if (check != null) return check; + return updateById(config) ? AjaxResult.success() : AjaxResult.error("修改失败"); + } + + /** + * 基本校验:配置名、奖池存在、周期合法、人数>0;返回 null 表示通过 + */ + private AjaxResult validate(TtOfficialRollConfig config) { + if (config == null) return AjaxResult.error("参数不能为空"); + if (StringUtils.isEmpty(config.getConfigName())) return AjaxResult.error("配置标识不能为空"); + if (StringUtils.isEmpty(config.getRoomName())) return AjaxResult.error("房间名称不能为空"); + if (config.getJackpotId() == null) return AjaxResult.error("请选择奖池"); + TtRollJackpot jackpot = ttRollJackpotMapper.selectById(config.getJackpotId()); + if (jackpot == null) return AjaxResult.error("指定的奖池不存在"); + String period = config.getPeriodType(); + if (!PERIOD_DAILY.equals(period) && !PERIOD_WEEKLY.equals(period)) { + return AjaxResult.error("周期类型不合法,仅支持 DAILY 或 WEEKLY"); + } + if (config.getPeopleNum() == null || config.getPeopleNum() <= 0) { + return AjaxResult.error("人数上限必须大于0"); + } + if (config.getMinRecharge() != null && config.getMinRecharge().compareTo(BigDecimal.ZERO) < 0) { + return AjaxResult.error("充值门槛不能为负数"); + } + return null; + } + + /** + * 归一化默认值 + */ + private void normalize(TtOfficialRollConfig config) { + if (config.getMinRecharge() == null) config.setMinRecharge(BigDecimal.ZERO); + if (config.getVipLevel() == null) config.setVipLevel(0); + if (config.getSortBy() == null) config.setSortBy(0); + if (StringUtils.isEmpty(config.getStatus())) config.setStatus("0"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrderServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrderServiceImpl.java new file mode 100644 index 0000000..bd8f44f --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrderServiceImpl.java @@ -0,0 +1,195 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.service.TtOrderService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.dto.sys.OrderQueryCondition; +import com.ruoyi.domain.dto.sys.TeamUsersParam; +import com.ruoyi.domain.dto.userRecord.OrderCondition; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.vo.order.TtOrderVO; +import com.ruoyi.domain.vo.sys.SimpleTtUserVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.sql.Timestamp; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +@Service +@Slf4j +public class TtOrderServiceImpl extends ServiceImpl implements TtOrderService { + + private final TtOrderMapper ttOrderMapper; + + public TtOrderServiceImpl(TtOrderMapper ttOrderMapper) { + this.ttOrderMapper = ttOrderMapper; + } + + @Override + public List queryList(TtOrder ttOrder) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotNull(ttOrder.getStatus())) + wrapper.eq(TtOrder::getStatus, ttOrder.getStatus()); + if (StringUtils.isNotNull(ttOrder.getUserId())) + wrapper.eq(TtOrder::getUserId, ttOrder.getUserId()); + return this.list(wrapper); + } + + @Override + public List byCondition(OrderCondition param) { + + param.setLimit((param.getPage() - 1) * param.getSize()); + List list = baseMapper.byCondition(param); + + Comparator comparator = null; + if (ObjectUtil.isNull(param.getOrderBy()) || param.getOrderBy().equals(1)) { + comparator = new Comparator() { + @Override + public int compare(TtOrderVO o1, TtOrderVO o2) { + return o1.getCreateTime().compareTo(o2.getCreateTime()); + } + }; + } else if (param.getOrderBy().equals(2)) { + comparator = new Comparator() { + @Override + public int compare(TtOrderVO o1, TtOrderVO o2) { + return o2.getCreateTime().compareTo(o1.getCreateTime()); + } + }; + } + + Collections.sort(list, comparator); + + return list; + + } + + // 批量统计消费 + @Override + public List batchRechargeTotal(List allEmployeesId, + String beginTime, + String endTime, + Integer orderType, + Integer page, + Integer size) { + + int limit = (page - 1) * size; + + if (StringUtils.isBlank(beginTime)) beginTime = null; + if (StringUtils.isBlank(endTime)) endTime = null; + if (ObjectUtil.isNull(orderType)) orderType = 1; + + return baseMapper.batchRechargeTotal( + allEmployeesId, + beginTime, + endTime, + orderType, + limit, + size); + } + + @Override + public R clientList(Integer page, Integer size, int uid) { + + Page pageInfo = new Page<>(page, size); + pageInfo.setOptimizeCountSql(false); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtOrder::getUserId,uid) + .eq(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .orderByDesc(TtOrder::getCreateTime); + pageInfo = this.page(pageInfo, wrapper); + + return R.ok(pageInfo); + } + + @Override + public R adminList(OrderQueryCondition con) { + + if (ObjectUtil.isNull(con.getPage()) || ObjectUtil.isNull(con.getSize())){ + return R.fail("分页参数不能为空"); + } + + Page pageInfo = new Page<>(con.getPage(), con.getSize()); + pageInfo.setOptimizeCountSql(false); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + wrapper + .in(ObjectUtil.isNotEmpty(con.getUidList()),TtOrder::getUserId,con.getUidList()) + .in(ObjectUtil.isNotEmpty(con.getStatusList()),TtOrder::getStatus,con.getStatusList()) + .eq(StringUtils.isNotBlank(con.getOrderId()), TtOrder::getOrderId, con.getOrderId()) + .eq(StringUtils.isNotBlank(con.getOutTradeNo()), TtOrder::getOutTradeNo, con.getOutTradeNo()); + + // 排除白号、机器人、封禁、逻辑删除、物理删除账号 + wrapper.inSql(TtOrder::getUserId, + "SELECT user_id FROM tt_user WHERE is_blank_account != 1 AND user_type != '03' AND status != '1' AND del_flag = '0'"); + + if (ObjectUtil.isNotEmpty(con.getBeginTime()) && ObjectUtil.isNotEmpty(con.getEndTime())){ + wrapper.between(TtOrder::getCreateTime,con.getBeginTime(),con.getEndTime()); + } + + if (ObjectUtil.isNull(con.getOrderByFie())) con.setOrderByFie("time"); + if (ObjectUtil.isNull(con.getOrderByType())) con.setOrderByType(1); + + if (con.getOrderByFie().equals("time")){ + + if (con.getOrderByType().equals(1)){ + wrapper.orderByDesc(TtOrder::getCreateTime); + }else if (con.getOrderByType().equals(2)){ + wrapper.orderByAsc(TtOrder::getCreateTime); + }else { + wrapper.orderByDesc(TtOrder::getCreateTime); + } + + }else if (con.getOrderByFie().equals("totalAmount")){ + if (con.getOrderByType().equals(1)){ + wrapper.orderByDesc(TtOrder::getTotalAmount); + }else if (con.getOrderByType().equals(2)){ + wrapper.orderByAsc(TtOrder::getTotalAmount); + }else { + wrapper.orderByDesc(TtOrder::getTotalAmount); + } + }else { + if (con.getOrderByType().equals(1)){ + wrapper.orderByDesc(TtOrder::getCreateTime); + }else if (con.getOrderByType().equals(2)){ + wrapper.orderByAsc(TtOrder::getCreateTime); + }else { + wrapper.orderByDesc(TtOrder::getCreateTime); + } + } + + Page page = this.page(pageInfo, wrapper); + + return R.ok(page); + } + + @Override + public SimpleTtUserVO rechargeTotalOfBoss(Integer employeeId, Timestamp beginTime, String endTime, Integer orderType, Integer page, Integer size) { + + int limit = (page - 1) * size; + + if (StringUtils.isBlank(endTime)) endTime = null; + if (ObjectUtil.isNull(orderType)) orderType = 1; + + return baseMapper.rechargeTotalOfBoss( + employeeId, + beginTime, + endTime, + orderType, + limit, + size); + + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrnamentServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrnamentServiceImpl.java new file mode 100644 index 0000000..15936ef --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrnamentServiceImpl.java @@ -0,0 +1,222 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.admin.service.TtOrnamentYYService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.dto.queryCondition.OrnamentCondition; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.other.TtOrnamentsBody; +import com.ruoyi.domain.vo.TtOrnamentVO; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; +import com.ruoyi.system.mapper.SysDictDataMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordSource.SYS_GRANT; + +@Service +public class TtOrnamentServiceImpl extends ServiceImpl implements TtOrnamentService { + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private TtOrnamentYYService ttOrnamentYYService; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private SysDictDataMapper dictDataMapper; + + @Override + public Page listByParam(TtOrnamentsBody param) { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // id + wrapper.eq(ObjectUtil.isNotEmpty(param.getId()), TtOrnament::getId, param.getId()); + + // 饰品名称(支持长名称name和短名称shortName模糊搜索) + String searchName = ObjectUtil.isNotEmpty(param.getItemName()) ? param.getItemName() : param.getName(); + if (ObjectUtil.isNotEmpty(searchName)) { + wrapper.and(w -> w.like(TtOrnament::getName, searchName) + .or().like(TtOrnament::getShortName, searchName)); + } + + // 外观 + wrapper.eq(ObjectUtil.isNotEmpty(param.getExterior()), TtOrnament::getExterior, param.getExterior()); + + // 类型 + wrapper.eq(ObjectUtil.isNotEmpty(param.getType()), TtOrnament::getType, param.getType()); + + // 价格区间 + wrapper + .ge(ObjectUtil.isNotEmpty(param.getMinPrice()), TtOrnament::getUsePrice, param.getMinPrice()) + .le(ObjectUtil.isNotEmpty(param.getMaxPrice()), TtOrnament::getUsePrice, param.getMaxPrice()); + + // 批量填货场景:只展示有价格的饰品,过滤 use_price 为空或为 0 的无价饰品,并按价格从小到大排序 + if (Boolean.TRUE.equals(param.getFilterNoPrice())) { + wrapper.isNotNull(TtOrnament::getUsePrice).gt(TtOrnament::getUsePrice, BigDecimal.ZERO) + .orderByAsc(TtOrnament::getUsePrice); + } + + if (ObjectUtil.isNull(param.getPageNum())) { + param.setPageNum(1); + } + if (ObjectUtil.isNull(param.getPageSize())) { + param.setPageSize(10); + } + Page pageInfo = new Page<>(param.getPageNum(), param.getPageSize()); + pageInfo.setOptimizeCountSql(false); + pageInfo = this.page(pageInfo, wrapper); + + List list = pageInfo.getRecords(); + + List res = list.stream().map(item -> { + TtOrnamentVO vo = new TtOrnamentVO(); + BeanUtil.copyProperties(item, vo); + return vo; + }).collect(Collectors.toList()); + + Page resPage = new Page<>(); + BeanUtil.copyProperties(pageInfo, resPage); + resPage.setRecords(res); + + return resPage; + + } + + @Override + public List selectTtOrnamentsList(TtOrnamentsBody param) { + + + return null; + + // if (param.getPartyType().equals(PartyType.ZBT.getCode())){ + // + // LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // if (ObjectUtils.isNotEmpty(param.getId())) wrapper.eq(TtOrnament::getId, param.getId()); + // if (StringUtils.isNotNull(param.getName())) wrapper.like(TtOrnament::getName, param.getName()); + // if (StringUtils.isNotNull(param.getTypeId())) wrapper.eq(TtOrnament::getType, param.getTypeId()); + // if (StringUtils.isNotNull(param.getExterior())) wrapper.eq(TtOrnament::getExterior, param.getExterior()); + // if (StringUtils.isNotNull(param.getMaxPrice())) wrapper.between(TtOrnament::getUsePrice, param.getMinPrice(), param.getMaxPrice()); + // wrapper.eq(TtOrnament::getIsProprietaryProperty, "1"); + // wrapper.orderByDesc(TtOrnament::getQuantity); + // List list = this.list(wrapper); + // + // return list.stream().map(item->{ + // CommonOrnamentVO build = CommonOrnamentVO.builder() + // .build(); + // BeanUtil.copyProperties(item,build); + // return build; + // }).collect(Collectors.toList()); + // + // }else if (param.getPartyType().equals(PartyType.YY_YOU_PING.getCode())){ + // + // LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // if (ObjectUtils.isNotEmpty(param.getId())) wrapper.eq(TtOrnamentsYY::getId, param.getId()); + // if (StringUtils.isNotNull(param.getName())) wrapper.like(TtOrnamentsYY::getName, param.getName()); + // if (StringUtils.isNotNull(param.getTypeId())) wrapper.eq(TtOrnamentsYY::getTypeId, param.getTypeId()); + // if (StringUtils.isNotNull(param.getTypeName())) wrapper.eq(TtOrnamentsYY::getTypeName, param.getTypeName()); + // // if (StringUtils.isNotNull(param.getMaxPrice())) wrapper.between(TtOrnamentsZBT::getUsePrice, param.getMinPrice(), param.getMaxPrice()); + // wrapper.orderByDesc(TtOrnamentsYY::getId); + // List list = ttOrnamentYYService.list(wrapper); + // + // return list.stream().map(item->{ + // CommonOrnamentVO build = CommonOrnamentVO.builder() + // .build(); + // BeanUtil.copyProperties(item,build); + // return build; + // }).collect(Collectors.toList()); + // + // }else { + // log.warn("非法的平台类型参数。"); + // return null; + // } + + } + + @Override + public List selectOrnamentsItemIdList() { + return getBaseMapper().selectOrnamentsItemIdList(); + } + + @Override + public AjaxResult grantOrnaments(Integer userId, Long ornamentId, Integer ornamentsLevelId, Integer num) { + TtOrnament ttOrnament = new LambdaQueryChainWrapper<>(ttOrnamentMapper).eq(TtOrnament::getId, ornamentId).one(); + if (ObjectUtil.isEmpty(ttOrnament)) { + return AjaxResult.error("不存在的饰品"); + } + // 校验在售数量是否充足 + if (ttOrnament.getQuantity() != null && ttOrnament.getQuantity() < num) { + return AjaxResult.error("在售数量不足,当前库存:" + ttOrnament.getQuantity()); + } + for (int i = 0; i < num; i++) { + TtBoxRecords boxRecords = TtBoxRecords.builder().build(); + boxRecords.setUserId(userId); + boxRecords.setOrnamentId(ornamentId); + boxRecords.setOrnamentsPrice(ttOrnament.getUsePrice()); + boxRecords.setOrnamentsLevelId(ornamentsLevelId); + boxRecords.setCreateTime(DateUtils.getNowDate()); + boxRecords.setSource(SYS_GRANT.getCode()); + boxRecords.setHolderUserId(userId); + boxRecordsService.save(boxRecords); + } + // 扣减在售数量 + if (ttOrnament.getQuantity() != null) { + ttOrnament.setQuantity(ttOrnament.getQuantity() - num); + ttOrnamentMapper.updateById(ttOrnament); + } + return AjaxResult.success(); + } + + @Override + public List byCondition(OrnamentCondition condition) { + // condition.setLimit((condition.getPageNum() - 1) * condition.getPageSize()); + return baseMapper.byCondition(condition); + // Integer total = baseMapper.countByCondition(condition); + // + // Page pageInfo = new Page<>(condition.getPageNum(), condition.getPageSize()); + // pageInfo.setRecords(list); + // pageInfo.setTotal(total); + // + // return R.ok(pageInfo); + } + + @Override + public List selectOrnamentsIdList() { + return ttOrnamentMapper.selectOrnamentsIdList(); + } + + @Override + public List selectOrnamentsMarketHashNameList() { + return ttOrnamentMapper.selectOrnamentsMarketHashNameList(); + } + + @Override + public AjaxResult updateOrnamentPrice(Long ornamentId, BigDecimal price) { + TtOrnament ttOrnament = new LambdaQueryChainWrapper<>(ttOrnamentMapper).eq(TtOrnament::getId, ornamentId).one(); + if (ObjectUtil.isEmpty(ttOrnament)) { + return AjaxResult.error("不存在的饰品"); + } + ttOrnament.setUsePrice(price); + ttOrnamentMapper.updateById(ttOrnament); + return AjaxResult.success("价格更新成功"); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrnamentsLevelServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrnamentsLevelServiceImpl.java new file mode 100644 index 0000000..4547c8e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrnamentsLevelServiceImpl.java @@ -0,0 +1,44 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; +import com.ruoyi.admin.mapper.WebsiteSetupMapper; +import com.ruoyi.admin.service.TtOrnamentsLevelService; +import com.ruoyi.common.config.RuoYiConfig; +import org.springframework.stereotype.Service; + + +@Service +public class TtOrnamentsLevelServiceImpl extends ServiceImpl implements TtOrnamentsLevelService { + + private final WebsiteSetupMapper websiteSetupMapper; + + public TtOrnamentsLevelServiceImpl(WebsiteSetupMapper websiteSetupMapper) { + this.websiteSetupMapper = websiteSetupMapper; + } + + @Override + public String generateOrnamentsLevel(Integer num) { + for (int i = 1; i <= num; i++) { + TtOrnamentsLevel ttOrnamentsLevel = TtOrnamentsLevel.builder().build(); + ttOrnamentsLevel.setLevel(""); + ttOrnamentsLevel.setLevelImg(""); + this.save(ttOrnamentsLevel); + } + return ""; + } + + @Override + public String updateOrnamentsLevelById(TtOrnamentsLevel ttOrnamentsLevel) { + String levelImg = ttOrnamentsLevel.getLevelImg(); + ttOrnamentsLevel.setLevelImg(RuoYiConfig.getDomainName() + levelImg); + this.updateById(ttOrnamentsLevel); + return ""; + } + + @Override + public void truncateOrnamentsLevel() { + websiteSetupMapper.truncateTtOrnamentsLevel(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrnamentsYYServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrnamentsYYServiceImpl.java new file mode 100644 index 0000000..20342e2 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtOrnamentsYYServiceImpl.java @@ -0,0 +1,12 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtOrnamentsYYMapper; +import com.ruoyi.admin.service.TtOrnamentYYService; +import com.ruoyi.domain.entity.TtOrnamentsYY; +import org.springframework.stereotype.Service; + +@Service +public class TtOrnamentsYYServiceImpl extends ServiceImpl implements TtOrnamentYYService { + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtPromoTurnoverServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtPromoTurnoverServiceImpl.java new file mode 100644 index 0000000..bf60183 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtPromoTurnoverServiceImpl.java @@ -0,0 +1,123 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtPromoTurnoverService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.param.promotion.AnchorDailyTurnoverParam; +import com.ruoyi.domain.param.promotion.AnchorRechargeParam; +import com.ruoyi.domain.vo.promotion.AnchorDailyTurnoverVo; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +public class TtPromoTurnoverServiceImpl extends ServiceImpl implements TtPromoTurnoverService { + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Override + public List getAnchorDailyTurnover(AnchorDailyTurnoverParam param) { + // 查出来所有主播 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(param.getParentId() != null, TtUser::getUserId, param.getParentId()); + query.like(param.getParentNickName() != null, TtUser::getNickName, param.getParentNickName()); + query.eq(TtUser::getUserType, "01"); + List users = list(query); + if (CollectionUtils.isEmpty(users)) { + return new ArrayList<>(); + } + List anchorDayTurnoverVos = new ArrayList<>(); + for (TtUser user : users) { + // 递归名下所有玩家 + List allPlayer = recursionAllPlayer(user.getUserId()); + List userIds = new ArrayList<>(); + for (TtUser ttUser : allPlayer) { + userIds.add(ttUser.getUserId()); + } + if (CollectionUtils.isEmpty(userIds)) { + continue; + } + param.setUserIds(userIds); + anchorDayTurnoverVos.addAll(baseMapper.getAnchorDailyTurnover(param)); + } + if (CollectionUtils.isEmpty(anchorDayTurnoverVos)) { + return new ArrayList<>(); + } + anchorDayTurnoverVos.sort((o1, o2) -> o2.getDate().compareTo(o1.getDate())); + return anchorDayTurnoverVos; + } + + @Override + public AjaxResult recharge(AnchorRechargeParam param) { + if (param == null || param.getParentId() == null || param.getRecharge() == null) { + return AjaxResult.error("参数不合法!"); + } + if (param.getRecharge().compareTo(new BigDecimal(0)) <= 0) { + return AjaxResult.error("奖励金额小于0!"); + } + // 查出来所有主播 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(param.getParentId() != null, TtUser::getUserId, param.getParentId()); + query.eq(TtUser::getUserType, "01"); + List users = list(query); + if (CollectionUtils.isEmpty(users) || users.get(0) == null) { + return AjaxResult.error("主播用户不存在!"); + } + TtUser ttUser = users.get(0); + ttUser.setAccountCredits(ttUser.getAccountCredits() != null ? ttUser.getAccountCredits().add(param.getRecharge()) : param.getRecharge()); + baseMapper.updateById(ttUser); + + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .credits(param.getRecharge().compareTo(BigDecimal.ZERO) > 0 ? param.getRecharge() : null) + .finalCredits(param.getRecharge().compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountCredits().add(param.getRecharge()) : null) + .total(param.getRecharge()) // 收支合计 + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.PROMOTION_RECHARGE.getCode()) + .remark(TtAccountRecordSource.PROMOTION_RECHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + ttUserBlendErcashMapper.insert(blendErcash); + + return AjaxResult.success(); + } + + public List recursionAllPlayer(Integer userId) { + List players = new ArrayList<>(); + getAllPlayersByAnchorIdRecursive(userId, players); + return players; + } + + /** + * 递归名下所有玩家 + */ + private void getAllPlayersByAnchorIdRecursive(int parentId, List players) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(TtUser::getParentId, parentId); + List users = list(query); + for (TtUser user : users) { + if ("02".equals(user.getUserType())) { // 如果是玩家 + players.add(user); + } else if ("01".equals(user.getUserType())) { // 如果是主播,继续递归 + getAllPlayersByAnchorIdRecursive(user.getUserId(), players); + } + } + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtPromotionLevelServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtPromotionLevelServiceImpl.java new file mode 100644 index 0000000..d02fccd --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtPromotionLevelServiceImpl.java @@ -0,0 +1,60 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.entity.TtPromotionLevel; +import com.ruoyi.admin.mapper.TtPromotionLevelMapper; +import com.ruoyi.admin.mapper.WebsiteSetupMapper; +import com.ruoyi.admin.service.TtPromotionLevelService; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.utils.DateUtils; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class TtPromotionLevelServiceImpl extends ServiceImpl implements TtPromotionLevelService { + + private final WebsiteSetupMapper websiteSetupMapper; + + public TtPromotionLevelServiceImpl(WebsiteSetupMapper websiteSetupMapper) { + this.websiteSetupMapper = websiteSetupMapper; + } + + @Override + public String generateVipLevel(Integer num) { + if (num == null || num < 0) { + return "最高推广等级不能为负数"; + } + // num 表示「最高推广等级」,从 LV0 到 LVnum,共 num+1 条 + long currentCount = this.count(); + long target = num + 1L; + if (currentCount >= target) { + return "当前已存在 " + currentCount + " 个推广等级,不低于设置值,无需生成;如需重新生成请先点击「重置」。"; + } + for (int i = (int) currentCount; i <= num; i++) { + TtPromotionLevel ttPromotionLevel = TtPromotionLevel.builder().build(); + ttPromotionLevel.setName("LV" + i); + ttPromotionLevel.setIcon(""); + ttPromotionLevel.setRechargeThreshold(BigDecimal.ZERO); + ttPromotionLevel.setCommissions(BigDecimal.ZERO); + ttPromotionLevel.setAddedBonus(BigDecimal.ZERO); + ttPromotionLevel.setRechargeGiftRatio(BigDecimal.ZERO); + ttPromotionLevel.setCreateTime(DateUtils.getNowDate()); + this.save(ttPromotionLevel); + } + return ""; + } + + @Override + public String updatePromotionLevelById(TtPromotionLevel ttPromotionLevel) { + String icon = ttPromotionLevel.getIcon(); + ttPromotionLevel.setIcon(RuoYiConfig.getDomainName() + icon); + this.updateById(ttPromotionLevel); + return ""; + } + + @Override + public void truncatePromotionLevel() { + websiteSetupMapper.truncateTtPromotionLevel(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtPromotionRecordServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtPromotionRecordServiceImpl.java new file mode 100644 index 0000000..6bab859 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtPromotionRecordServiceImpl.java @@ -0,0 +1,48 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtUserAmountRecordsMapper; +import com.ruoyi.domain.entity.TtPromotionRecord; +import com.ruoyi.admin.mapper.TtPromotionRecordMapper; +import com.ruoyi.admin.service.TtPromotionRecordService; +import com.ruoyi.domain.vo.PromotionDataVO; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; + +@Service +public class TtPromotionRecordServiceImpl extends ServiceImpl implements TtPromotionRecordService { + + @Autowired + private TtUserAmountRecordsMapper ttUserAmountRecordsMapper; + + @Override + public List getPromotionRecord(TtPromotionRecord ttPromotionRecord) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotNull(ttPromotionRecord.getUserId())) wrapper.eq(TtPromotionRecord::getUserId, ttPromotionRecord.getUserId()); + if (StringUtils.isNotEmpty(ttPromotionRecord.getStatus())) wrapper.eq(TtPromotionRecord::getStatus, ttPromotionRecord.getStatus()); + if (StringUtils.isNotNull(ttPromotionRecord.getRechargeRecordId())) wrapper.eq(TtPromotionRecord::getRechargeRecordId, ttPromotionRecord.getRechargeRecordId()); + wrapper.orderByDesc(TtPromotionRecord::getId); + List ttPromotionRecordList = this.list(wrapper); + for (TtPromotionRecord promotionRecord : ttPromotionRecordList) { + BigDecimal totalConsumption = ttUserAmountRecordsMapper.getTotalConsumptionByUserId(promotionRecord.getSubordinateUserId()); + if (Objects.isNull(totalConsumption)) { + promotionRecord.setTotalConsumption(new BigDecimal(0)); + } else { + promotionRecord.setTotalConsumption(totalConsumption); + } + } + return ttPromotionRecordList; + } + + @Override + public PromotionDataVO statisticsPromotionData(Integer userId) { + return baseMapper.statisticsPromotionData(userId); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeCardServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeCardServiceImpl.java new file mode 100644 index 0000000..96dbf2d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeCardServiceImpl.java @@ -0,0 +1,126 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtRechargeCard; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.admin.mapper.TtRechargeCardMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.service.TtRechargeCardService; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import org.springframework.stereotype.Service; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class TtRechargeCardServiceImpl extends ServiceImpl implements TtRechargeCardService { + + private final TtRechargeProdMapper rechargeListMapper; + + public TtRechargeCardServiceImpl(TtRechargeProdMapper rechargeListMapper) { + this.rechargeListMapper = rechargeListMapper; + } + + @Override + public List queryList(TtRechargeCard ttRechargeCard) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotNull(ttRechargeCard.getId())) wrapper.eq(TtRechargeCard::getId, ttRechargeCard.getId()); + if (StringUtils.isNotNull(ttRechargeCard.getRechargeListId())) wrapper.eq(TtRechargeCard::getRechargeListId, ttRechargeCard.getRechargeListId()); + if (StringUtils.isNotEmpty(ttRechargeCard.getPassword())) wrapper.eq(TtRechargeCard::getPassword, ttRechargeCard.getPassword()); + if (StringUtils.isNotEmpty(ttRechargeCard.getStatus())) wrapper.eq(TtRechargeCard::getStatus, ttRechargeCard.getStatus()); + if (StringUtils.isNotNull(ttRechargeCard.getUseUserId())) wrapper.eq(TtRechargeCard::getUseUserId, ttRechargeCard.getUseUserId()); + wrapper.orderByDesc(TtRechargeCard::getCreateTime); + return this.list(wrapper); + } + + @Override + public List generateCard(Integer rechargeListId, Integer num) { + TtRechargeProd recharge = rechargeListMapper.selectById(rechargeListId); + if (recharge == null) return new ArrayList<>(); + + List cardList = new ArrayList<>(); + for (int i = 0; i < num; i++) { + String password = IdUtils.fastSimpleUUID().toUpperCase(); + cardList.add(password); + TtRechargeCard ttRechargeCard = TtRechargeCard.builder().build(); + ttRechargeCard.setRechargeListId(rechargeListId); + ttRechargeCard.setPrice(recharge.getPrice()); + ttRechargeCard.setPassword(password); + ttRechargeCard.setCreateBy(SecurityUtils.getUsername()); + ttRechargeCard.setCreateTime(DateUtils.getNowDate()); + this.save(ttRechargeCard); + } + return cardList; + } + + @Override + public List generateBlankCard(Integer rechargeListId, Integer num) { + TtRechargeProd recharge = rechargeListMapper.selectById(rechargeListId); + if (recharge == null) return new ArrayList<>(); + + List cardList = new ArrayList<>(); + for (int i = 0; i < num; i++) { + String password = IdUtils.fastSimpleUUID().toUpperCase(); + cardList.add(password); + TtRechargeCard ttRechargeCard = TtRechargeCard.builder().build(); + ttRechargeCard.setRechargeListId(rechargeListId); + ttRechargeCard.setPrice(recharge.getPrice()); + ttRechargeCard.setPassword(password); + ttRechargeCard.setIsBlank(1); + ttRechargeCard.setCreateBy(SecurityUtils.getUsername()); + ttRechargeCard.setCreateTime(DateUtils.getNowDate()); + this.save(ttRechargeCard); + } + return cardList; + } + + @Override + public void export(HttpServletResponse response, TtRechargeCard ttRechargeCard) { + List ttRechargeCards = this.queryList(ttRechargeCard); + List cardList = ttRechargeCards.stream().map(TtRechargeCard::getPassword).collect(Collectors.toList()); + if (cardList.isEmpty()) return; + String folderPath = RuoYiConfig.getDownloadPath() + "/"; + File folder = new File(folderPath); + if (!folder.exists()) { + folder.mkdirs(); + } + ServletOutputStream os = null; + BufferedWriter bw = null; + String uuid = IdUtils.fastSimpleUUID(); + try { + bw = new BufferedWriter(new FileWriter(folderPath + uuid + ".txt")); + for (String card : cardList) { + bw.write(card); + bw.newLine(); + bw.flush(); + } + File uploadFile = new File(folderPath + uuid + ".txt"); + os = response.getOutputStream(); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType("application/octet-stream"); + response.addHeader("Content-disposition", "attachment"); + os.write(FileUtil.readBytes(uploadFile)); + IoUtil.flush(os); + } catch (IOException ignored) { + } finally { + if (os != null) IoUtil.close(os); + if (bw != null) IoUtil.close(bw); + } + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeConfigServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeConfigServiceImpl.java new file mode 100644 index 0000000..6f9160a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeConfigServiceImpl.java @@ -0,0 +1,11 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtRechargeConfigMapper; +import com.ruoyi.admin.service.TtRechargeConfigService; +import com.ruoyi.domain.entity.TtRechargeConfig; +import org.springframework.stereotype.Service; + +@Service +public class TtRechargeConfigServiceImpl extends ServiceImpl implements TtRechargeConfigService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeProdServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeProdServiceImpl.java new file mode 100644 index 0000000..e8f8f56 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeProdServiceImpl.java @@ -0,0 +1,11 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.service.TtRechargeProdService; +import org.springframework.stereotype.Service; + +@Service +public class TtRechargeProdServiceImpl extends ServiceImpl implements TtRechargeProdService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeRankingRewardServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeRankingRewardServiceImpl.java new file mode 100644 index 0000000..5a11342 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeRankingRewardServiceImpl.java @@ -0,0 +1,115 @@ +package com.ruoyi.admin.service.impl; + +import com.ruoyi.admin.mapper.TtRechargeRankingRewardMapper; +import com.ruoyi.admin.service.TtRechargeRankingRewardService; +import com.ruoyi.domain.other.TtRechargeRankingReward; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 充值奖励金额Service业务层处理 + * + * @author ruoyi + * @date 2024-07-04 + */ +@Service +public class TtRechargeRankingRewardServiceImpl implements TtRechargeRankingRewardService +{ + @Autowired + private TtRechargeRankingRewardMapper ttRechargeRankingRewardMapper; + + /** + * 查询充值奖励金额 + * + * @param id 充值奖励金额主键 + * @return 充值奖励金额 + */ + @Override + public TtRechargeRankingReward selectTtRechargeRankingRewardById(Integer id) + { + return ttRechargeRankingRewardMapper.selectTtRechargeRankingRewardById(id); + } + + /** + * 查询充值奖励金额列表 + * + * @param ttRechargeRankingReward 充值奖励金额 + * @return 充值奖励金额 + */ + @Override + public List selectTtRechargeRankingRewardList(TtRechargeRankingReward ttRechargeRankingReward) + { + return ttRechargeRankingRewardMapper.selectTtRechargeRankingRewardList(ttRechargeRankingReward); + } + + @Override + @Transactional + public String generateRankingReward() + { + ttRechargeRankingRewardMapper.truncateRankingReward(); + for (int i = 1; i <= 10; i++) { + TtRechargeRankingReward ttRechargeRankingReward = new TtRechargeRankingReward(); + ttRechargeRankingReward.setName("TOP" + i); + ttRechargeRankingReward.setReward(BigDecimal.ZERO); + ttRechargeRankingRewardMapper.insertTtRechargeRankingReward(ttRechargeRankingReward); + } + return ""; + } + + // /** + // * 新增充值奖励金额 + // * + // * @param ttRechargeRankingReward 充值奖励金额 + // * @return 结果 + // */ + // @Override + // public int insertTtRechargeRankingReward(TtRechargeRankingReward ttRechargeRankingReward) + // { + // return ttRechargeRankingRewardMapper.insertTtRechargeRankingReward(ttRechargeRankingReward); + // } + + /** + * 修改充值奖励金额 + * + * @param ttRechargeRankingReward 充值奖励金额 + * @return 结果 + */ + @Override + public int updateTtRechargeRankingReward(TtRechargeRankingReward ttRechargeRankingReward) + { + return ttRechargeRankingRewardMapper.updateTtRechargeRankingReward(ttRechargeRankingReward); + } + + /** + * 批量删除充值奖励金额 + * + * @param ids 需要删除的充值奖励金额主键 + * @return 结果 + */ + @Override + public int deleteTtRechargeRankingRewardByIds(Integer[] ids) + { + return ttRechargeRankingRewardMapper.deleteTtRechargeRankingRewardByIds(ids); + } + + @Override + public void truncateRankingReward() { + ttRechargeRankingRewardMapper.truncateRankingReward(); + } + + // /** + // * 删除充值奖励金额信息 + // * + // * @param id 充值奖励金额主键 + // * @return 结果 + // */ + // @Override + // public int deleteTtRechargeRankingRewardById(Integer id) + // { + // return ttRechargeRankingRewardMapper.deleteTtRechargeRankingRewardById(id); + // } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeRecordServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeRecordServiceImpl.java new file mode 100644 index 0000000..78d4e13 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRechargeRecordServiceImpl.java @@ -0,0 +1,51 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.service.TtRechargeRecordService; +import com.ruoyi.domain.other.TtRechargeRecordBody; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TtRechargeRecordServiceImpl extends ServiceImpl implements TtRechargeRecordService { + + @Override + public List queryList(TtRechargeRecordBody rechargeRecordBody) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // 过滤掉空白卡密产生的充值记录 + wrapper.and(w -> w.isNull(TtRechargeRecord::getIsBlankCard).or().ne(TtRechargeRecord::getIsBlankCard, 1)); + if (StringUtils.isNotNull(rechargeRecordBody.getUserId())) + wrapper.eq(TtRechargeRecord::getUserId, rechargeRecordBody.getUserId()); + // 主播ID过滤:仅展示「该主播直属下级普通用户本人」的充值记录(排除主播本人与二级主播)。 + // 匹配 user_id 属于「parent_id=该主播 且 user_type='02'(普通用户)」的记录;parentId 为 Integer,拼接无注入风险。 + if (StringUtils.isNotNull(rechargeRecordBody.getParentId())) + wrapper.inSql(TtRechargeRecord::getUserId, + "SELECT user_id FROM tt_user WHERE parent_id = " + rechargeRecordBody.getParentId() + " AND user_type = '02'"); + // 招商ID过滤:仅展示「该招商直属下级主播本人」的充值记录。 + // 匹配 user_id 属于「parent_id=该招商 且 user_type='01'(主播)」的记录;merchantId 为 Integer,拼接无注入风险。 + if (StringUtils.isNotNull(rechargeRecordBody.getMerchantId())) + wrapper.inSql(TtRechargeRecord::getUserId, + "SELECT user_id FROM tt_user WHERE parent_id = " + rechargeRecordBody.getMerchantId() + " AND user_type = '01'"); + if (StringUtils.isNotNull(rechargeRecordBody.getCardMerchantId())) + wrapper.eq(TtRechargeRecord::getCardMerchantId, rechargeRecordBody.getCardMerchantId()); + if (StringUtils.isNotEmpty(rechargeRecordBody.getOrderId())) + wrapper.eq(TtRechargeRecord::getOrderId, rechargeRecordBody.getOrderId()); + if (StringUtils.isNotEmpty(rechargeRecordBody.getOutTradeNo())) + wrapper.eq(TtRechargeRecord::getOutTradeNo, rechargeRecordBody.getOutTradeNo()); + if (StringUtils.isNotEmpty(rechargeRecordBody.getChannelType())) + wrapper.eq(TtRechargeRecord::getChannelType, rechargeRecordBody.getChannelType()); + if (StringUtils.isNotNull(rechargeRecordBody.getStartTime()) && StringUtils.isNotNull(rechargeRecordBody.getEndTime())) + wrapper.between(TtRechargeRecord::getCreateTime, rechargeRecordBody.getStartTime(), rechargeRecordBody.getEndTime()); + // 排除白号、机器人、封禁、逻辑删除、物理删除用户 + wrapper.inSql(TtRechargeRecord::getUserId, + "SELECT user_id FROM tt_user WHERE is_blank_account != 1 AND user_type != '03' AND status != '1' AND del_flag = '0'"); + wrapper.orderByDesc(TtRechargeRecord::getCreateTime); + return this.list(wrapper); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRedPackServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRedPackServiceImpl.java new file mode 100644 index 0000000..21eeb97 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRedPackServiceImpl.java @@ -0,0 +1,97 @@ +package com.ruoyi.admin.service.impl; + +import com.ruoyi.admin.mapper.TtRedPackMapper; +import com.ruoyi.admin.service.TtRedPackService; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.other.TtRedPack; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 【请填写功能名称】Service业务层处理 + * + * @author ruoyi + * @date 2023-06-29 + */ +@Service +public class TtRedPackServiceImpl implements TtRedPackService +{ + @Autowired + private TtRedPackMapper ttRedPackMapper; + + /** + * 查询【请填写功能名称】 + * + * @param id 【请填写功能名称】主键 + * @return 【请填写功能名称】 + */ + @Override + public TtRedPack selectTtRedPackById(Long id) + { + return ttRedPackMapper.selectTtRedPackById(id); + } + + /** + * 查询【请填写功能名称】列表 + * + * @param ttRedPack 【请填写功能名称】 + * @return 【请填写功能名称】 + */ + @Override + public List selectTtRedPackList(TtRedPack ttRedPack) + { + return ttRedPackMapper.selectTtRedPackList(ttRedPack); + } + + /** + * 新增【请填写功能名称】 + * + * @param ttRedPack 【请填写功能名称】 + * @return 结果 + */ + @Override + public int insertTtRedPack(TtRedPack ttRedPack) + { + ttRedPack.setCreateTime(DateUtils.getNowDate()); + return ttRedPackMapper.insertTtRedPack(ttRedPack); + } + + /** + * 修改【请填写功能名称】 + * + * @param ttRedPack 【请填写功能名称】 + * @return 结果 + */ + @Override + public int updateTtRedPack(TtRedPack ttRedPack) + { + ttRedPack.setUpdateTime(DateUtils.getNowDate()); + return ttRedPackMapper.updateTtRedPack(ttRedPack); + } + + /** + * 批量删除【请填写功能名称】 + * + * @param ids 需要删除的【请填写功能名称】主键 + * @return 结果 + */ + @Override + public int deleteTtRedPackByIds(Long[] ids) + { + return ttRedPackMapper.deleteTtRedPackByIds(ids); + } + + /** + * 删除【请填写功能名称】信息 + * + * @param id 【请填写功能名称】主键 + * @return 结果 + */ + @Override + public int deleteTtRedPackById(Long id) + { + return ttRedPackMapper.deleteTtRedPackById(id); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRedPacketRecordServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRedPacketRecordServiceImpl.java new file mode 100644 index 0000000..3f9066c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRedPacketRecordServiceImpl.java @@ -0,0 +1,20 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtRedPacketRecord; +import com.ruoyi.admin.mapper.TtRedPacketRecordMapper; +import com.ruoyi.admin.service.TtRedPacketRecordService; +import com.ruoyi.domain.other.TtRedPacketRecordBody; +import com.ruoyi.domain.vo.TtRedPacketRecordDataVO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TtRedPacketRecordServiceImpl extends ServiceImpl implements TtRedPacketRecordService { + + @Override + public List queryList(TtRedPacketRecordBody ttRedPacketRecordBody) { + return baseMapper.queryList(ttRedPacketRecordBody); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRedPacketServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRedPacketServiceImpl.java new file mode 100644 index 0000000..df33eef --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRedPacketServiceImpl.java @@ -0,0 +1,201 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRedPacket; +import com.ruoyi.admin.mapper.TtRedPacketMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtRedPacketService; +import com.ruoyi.domain.other.TtRedPacketBody; +import com.ruoyi.domain.vo.TtRedPacketDataVO; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Service +public class TtRedPacketServiceImpl extends ServiceImpl implements TtRedPacketService { + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtUserBlendErcashMapper blendErcashMapper; + + @Override + public List queryList(TtRedPacketBody ttRedPacketBody) { + + List list = baseMapper.queryList(ttRedPacketBody); + + List redPacketList = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtRedPacket::getStatus, 0) + .list(); + + List expiredRedPack = new ArrayList<>(); + for (TtRedPacket ttRedPacket : redPacketList) { + + Date validity = ttRedPacket.getValidity(); + if (ObjectUtil.isNull(validity) || DateUtil.compare(validity, DateUtils.getNowDate()) > 0) continue; + + expiredRedPack.add(ttRedPacket.getId()); + ttRedPacket.setStatus(1); + } + + if (!expiredRedPack.isEmpty()){ + new LambdaUpdateChainWrapper<>(baseMapper) + .in(TtRedPacket::getId,expiredRedPack) + .set(TtRedPacket::getStatus,1) + .update(); + } + + return list; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List insertRedPacket(TtRedPacketDataVO ttRedPacketDataVO) { + + // 当选择了具体主播(userId > 0)时,执行扣款流程 + Integer userId = ttRedPacketDataVO.getUserId(); + if (userId != null && userId > 0) { + deductAnchorBalance(ttRedPacketDataVO, userId.longValue()); + } + + List pwList = new ArrayList<>(); + List rpList = new ArrayList<>(); + + for (int i = 0; i < ttRedPacketDataVO.getCreateNum(); i++) { + + TtRedPacket redPacket = new TtRedPacket(); + + BeanUtil.copyProperties(ttRedPacketDataVO,redPacket); + + redPacket.setPassword(IdUtils.fastSimpleUUID().toUpperCase()); + + pwList.add(redPacket.getPassword()); + redPacket.setCreateBy(SecurityUtils.getUsername()); + redPacket.setCreateTime(DateUtils.getNowDate()); + redPacket.setUseStatus(0); + + rpList.add(redPacket); + + } + + this.saveBatch(rpList,1); + + return pwList; + } + + /** + * 扣除主播余额(金币+积分双轨),并写入流水记录。 + * 扣款总额 = amount2(最高金额)× num(红包个数)× createNum(生成数量) + */ + private void deductAnchorBalance(TtRedPacketDataVO dataVO, Long userId) { + // 解析 amount 字段,格式如 "[1,10]",提取最高金额 amount2 + String amountStr = dataVO.getAmount(); + if (amountStr == null || amountStr.isEmpty()) { + throw new RuntimeException("红包金额区间不能为空"); + } + BigDecimal amount2 = parseAmountMax(amountStr); + if (amount2 == null || amount2.compareTo(BigDecimal.ZERO) <= 0) { + throw new RuntimeException("红包最高金额不合法"); + } + + int num = dataVO.getNum() != null ? dataVO.getNum() : 0; + int createNum = dataVO.getCreateNum() != null ? dataVO.getCreateNum() : 0; + if (num <= 0 || createNum <= 0) { + throw new RuntimeException("红包个数和生成数量必须大于0"); + } + + // 计算总扣款金额 + BigDecimal totalCost = amount2.multiply(BigDecimal.valueOf(num)).multiply(BigDecimal.valueOf(createNum)); + + // 查询主播余额 + TtUser anchorUser = ttUserMapper.selectById(userId); + if (anchorUser == null) { + throw new RuntimeException("主播账号不存在"); + } + BigDecimal currentBalance = anchorUser.getAccountAmount() != null ? anchorUser.getAccountAmount() : BigDecimal.ZERO; + BigDecimal currentCredits = anchorUser.getAccountCredits() != null ? anchorUser.getAccountCredits() : BigDecimal.ZERO; + if (currentBalance.compareTo(totalCost) < 0) { + throw new RuntimeException("余额不足,需要 " + totalCost.toPlainString() + " 金币,当前余额 " + currentBalance.toPlainString()); + } + if (currentCredits.compareTo(totalCost) < 0) { + throw new RuntimeException("积分不足,需要 " + totalCost.toPlainString() + " 积分,当前积分 " + currentCredits.toPlainString()); + } + + // 扣减金币 + int rows = ttUserMapper.subtractOnlyAccountAmount(userId, totalCost); + if (rows <= 0) { + throw new RuntimeException("余额扣减失败,余额不足"); + } + // 扣减积分 + rows = ttUserMapper.subtractOnlyAccountCredits(userId, totalCost); + if (rows <= 0) { + throw new RuntimeException("积分扣减失败,积分不足"); + } + + // 查询扣减后余额,写入流水 + TtUser afterUser = ttUserMapper.selectById(userId); + BigDecimal newBalance = afterUser.getAccountAmount() != null ? afterUser.getAccountAmount() : BigDecimal.ZERO; + BigDecimal newCredits = afterUser.getAccountCredits() != null ? afterUser.getAccountCredits() : BigDecimal.ZERO; + Timestamp now = new Timestamp(System.currentTimeMillis()); + TtUserBlendErcash ercash = TtUserBlendErcash.builder() + .userId(afterUser.getUserId()) + .amount(totalCost.negate()) + .finalAmount(newBalance) + .credits(totalCost.negate()) + .finalCredits(newCredits) + .total(totalCost.negate()) + .type(TtAccountRecordType.OUTPUT.getCode()) + .source(TtAccountRecordSource.ADMIN_RED_PACKET_GENERATE.getCode()) + .remark(TtAccountRecordSource.ADMIN_RED_PACKET_GENERATE.getMsg() + "(" + createNum + "个码×" + num + "次)") + .createTime(now) + .updateTime(now) + .build(); + blendErcashMapper.insert(ercash); + } + + /** + * 从金额区间字符串(如 "[1,10]")中解析最高金额 + */ + private BigDecimal parseAmountMax(String amountStr) { + try { + String cleaned = amountStr.replace("[", "").replace("]", "").trim(); + String[] parts = cleaned.split(","); + if (parts.length >= 2) { + return new BigDecimal(parts[1].trim()); + } + // 如果只有一个值,当作最高金额 + return new BigDecimal(parts[0].trim()); + } catch (Exception e) { + return null; + } + } + + @Override + public AjaxResult updateRedPacketById(TtRedPacket redPacket) { + if (this.updateById(redPacket)){ + return AjaxResult.success(); + } + return AjaxResult.error(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtReplacementRecordServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtReplacementRecordServiceImpl.java new file mode 100644 index 0000000..1d10d6e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtReplacementRecordServiceImpl.java @@ -0,0 +1,97 @@ +package com.ruoyi.admin.service.impl; + +import com.ruoyi.admin.mapper.TtReplacementRecordMapper; +import com.ruoyi.admin.service.TtReplacementRecordService; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.other.TtReplacementRecord; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 汰换记录Service业务层处理 + * + * @author junhai + * @date 2023-09-10 + */ +@Service +public class TtReplacementRecordServiceImpl implements TtReplacementRecordService +{ + @Autowired + private TtReplacementRecordMapper ttReplacementRecordMapper; + + /** + * 查询汰换记录 + * + * @param id 汰换记录主键 + * @return 汰换记录 + */ + @Override + public TtReplacementRecord selectTtReplacementRecordById(Long id) + { + return ttReplacementRecordMapper.selectTtReplacementRecordById(id); + } + + /** + * 查询汰换记录列表 + * + * @param ttReplacementRecord 汰换记录 + * @return 汰换记录 + */ + @Override + public List selectTtReplacementRecordList(TtReplacementRecord ttReplacementRecord) + { + return ttReplacementRecordMapper.selectTtReplacementRecordList(ttReplacementRecord); + } + + /** + * 新增汰换记录 + * + * @param ttReplacementRecord 汰换记录 + * @return 结果 + */ + @Override + public int insertTtReplacementRecord(TtReplacementRecord ttReplacementRecord) + { + ttReplacementRecord.setCreateTime(DateUtils.getNowDate()); + return ttReplacementRecordMapper.insertTtReplacementRecord(ttReplacementRecord); + } + + /** + * 修改汰换记录 + * + * @param ttReplacementRecord 汰换记录 + * @return 结果 + */ + @Override + public int updateTtReplacementRecord(TtReplacementRecord ttReplacementRecord) + { + ttReplacementRecord.setUpdateTime(DateUtils.getNowDate()); + return ttReplacementRecordMapper.updateTtReplacementRecord(ttReplacementRecord); + } + + /** + * 批量删除汰换记录 + * + * @param ids 需要删除的汰换记录主键 + * @return 结果 + */ + @Override + public int deleteTtReplacementRecordByIds(Long[] ids) + { + return ttReplacementRecordMapper.deleteTtReplacementRecordByIds(ids); + } + + /** + * 删除汰换记录信息 + * + * @param id 汰换记录主键 + * @return 结果 + */ + @Override + public int deleteTtReplacementRecordById(Long id) + { + return ttReplacementRecordMapper.deleteTtReplacementRecordById(id); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightGroupBoxServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightGroupBoxServiceImpl.java new file mode 100644 index 0000000..7e80a60 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightGroupBoxServiceImpl.java @@ -0,0 +1,94 @@ +package com.ruoyi.admin.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.admin.mapper.TtRobotFightGroupBoxMapper; +import com.ruoyi.domain.other.TtRobotFightGroupBox; +import com.ruoyi.admin.service.TtRobotFightGroupBoxService; + +/** + * 机器人对战分组宝箱Service业务层处理 + * + * @author ruoyi + * @date 2024-06-28 + */ +@Service +public class TtRobotFightGroupBoxServiceImpl implements TtRobotFightGroupBoxService +{ + @Autowired + private TtRobotFightGroupBoxMapper ttRobotFightGroupBoxMapper; + + /** + * 查询机器人对战分组宝箱 + * + * @param groupId 机器人对战分组宝箱主键 + * @return 机器人对战分组宝箱 + */ + @Override + public TtRobotFightGroupBox selectTtRobotFightGroupBoxByBoxId(Integer groupId, Integer boxId) + { + return ttRobotFightGroupBoxMapper.selectTtRobotFightGroupBoxByBoxId(groupId, boxId); + } + + /** + * 查询机器人对战分组宝箱列表 + * + * @param ttRobotFightGroupBox 机器人对战分组宝箱 + * @return 机器人对战分组宝箱 + */ + @Override + public List selectTtRobotFightGroupBoxList( + Integer groupId, + TtRobotFightGroupBox ttRobotFightGroupBox + ) + { + return ttRobotFightGroupBoxMapper + .selectTtRobotFightGroupBoxList(groupId, ttRobotFightGroupBox); + } + + /** + * 新增机器人对战分组宝箱 + * + * @param ttRobotFightGroupBox 机器人对战分组宝箱 + * @return 结果 + */ + @Override + public int insertTtRobotFightGroupBox(TtRobotFightGroupBox ttRobotFightGroupBox) + { + return ttRobotFightGroupBoxMapper.insertTtRobotFightGroupBox(ttRobotFightGroupBox); + } + + /** + * 修改机器人对战分组宝箱 + * + * @param ttRobotFightGroupBox 机器人对战分组宝箱 + * @return 结果 + */ + @Override + public int updateTtRobotFightGroupBox(TtRobotFightGroupBox ttRobotFightGroupBox) + { + return ttRobotFightGroupBoxMapper.updateTtRobotFightGroupBox(ttRobotFightGroupBox); + } + + /** + * 批量删除机器人对战分组宝箱 + */ + @Override + public int deleteTtRobotFightGroupBoxByGroupIds(Integer groupId, Integer[] boxIds) + { + return ttRobotFightGroupBoxMapper.deleteTtRobotFightGroupBoxByGroupIds(groupId, boxIds); + } + + /** + * 删除机器人对战分组宝箱信息 + * + * @param groupId 机器人对战分组宝箱主键 + * @return 结果 + */ + @Override + public int deleteTtRobotFightGroupBoxByGroupId(Integer groupId, Integer boxId) + { + return ttRobotFightGroupBoxMapper.deleteTtRobotFightGroupBoxByGroupId(groupId, boxId); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightGroupServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightGroupServiceImpl.java new file mode 100644 index 0000000..182b08d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightGroupServiceImpl.java @@ -0,0 +1,104 @@ +package com.ruoyi.admin.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.admin.mapper.TtRobotFightGroupBoxMapper; +import com.ruoyi.admin.mapper.TtRobotFightGroupMapper; +import com.ruoyi.domain.other.TtRobotFightGroup; +import com.ruoyi.admin.service.TtRobotFightGroupService; + +/** + * 机器人对战分组Service业务层处理 + * + * @author ruoyi + * @date 2024-06-28 + */ +@Service +public class TtRobotFightGroupServiceImpl implements TtRobotFightGroupService +{ + @Autowired + private TtRobotFightGroupMapper ttRobotFightGroupMapper; + + @Autowired + private TtRobotFightGroupBoxMapper ttRobotFightGroupBoxMapper; + + /** + * 查询机器人对战分组 + * + * @param groupId 机器人对战分组主键 + * @return 机器人对战分组 + */ + @Override + public TtRobotFightGroup selectTtRobotFightGroupByGroupId(Integer groupId) + { + return ttRobotFightGroupMapper.selectTtRobotFightGroupByGroupId(groupId); + } + + /** + * 查询机器人对战分组列表 + * + * @param ttRobotFightGroup 机器人对战分组 + * @return 机器人对战分组 + */ + @Override + public List selectTtRobotFightGroupList(TtRobotFightGroup ttRobotFightGroup) + { + return ttRobotFightGroupMapper.selectTtRobotFightGroupList(ttRobotFightGroup); + } + + /** + * 新增机器人对战分组 + * + * @param ttRobotFightGroup 机器人对战分组 + * @return 结果 + */ + @Override + public int insertTtRobotFightGroup(TtRobotFightGroup ttRobotFightGroup) + { + return ttRobotFightGroupMapper.insertTtRobotFightGroup(ttRobotFightGroup); + } + + /** + * 修改机器人对战分组 + * + * @param ttRobotFightGroup 机器人对战分组 + * @return 结果 + */ + @Override + public int updateTtRobotFightGroup(TtRobotFightGroup ttRobotFightGroup) + { + return ttRobotFightGroupMapper.updateTtRobotFightGroup(ttRobotFightGroup); + } + + /** + * 批量删除机器人对战分组 + * + * @param groupIds 需要删除的机器人对战分组主键 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteTtRobotFightGroupByGroupIds(Integer[] groupIds) + { + for (Integer groupId : groupIds) { + ttRobotFightGroupBoxMapper.deleteTtRobotFightGroupBoxByGroupIdAll(groupId); + } + return ttRobotFightGroupMapper.deleteTtRobotFightGroupByGroupIds(groupIds); + } + + /** + * 删除机器人对战分组信息 + * + * @param groupId 机器人对战分组主键 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteTtRobotFightGroupByGroupId(Integer groupId) + { + ttRobotFightGroupBoxMapper.deleteTtRobotFightGroupBoxByGroupIdAll(groupId); + return ttRobotFightGroupMapper.deleteTtRobotFightGroupByGroupId(groupId); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightJoinConfigServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightJoinConfigServiceImpl.java new file mode 100644 index 0000000..bcc8545 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightJoinConfigServiceImpl.java @@ -0,0 +1,93 @@ +package com.ruoyi.admin.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.admin.mapper.TtRobotFightJoinConfigMapper; +import com.ruoyi.domain.other.TtRobotFightJoinConfig; +import com.ruoyi.admin.service.TtRobotFightJoinConfigService; + +/** + * 机器人对战进房时间配置Service业务层处理 + * + * @author ruoyi + * @date 2026-07-23 + */ +@Service +public class TtRobotFightJoinConfigServiceImpl implements TtRobotFightJoinConfigService +{ + @Autowired + private TtRobotFightJoinConfigMapper ttRobotFightJoinConfigMapper; + + /** + * 查询机器人对战进房时间配置 + * + * @param id 主键 + * @return 机器人对战进房时间配置 + */ + @Override + public TtRobotFightJoinConfig selectTtRobotFightJoinConfigById(Integer id) + { + return ttRobotFightJoinConfigMapper.selectTtRobotFightJoinConfigById(id); + } + + /** + * 查询机器人对战进房时间配置列表 + * + * @param ttRobotFightJoinConfig 机器人对战进房时间配置 + * @return 机器人对战进房时间配置 + */ + @Override + public List selectTtRobotFightJoinConfigList(TtRobotFightJoinConfig ttRobotFightJoinConfig) + { + return ttRobotFightJoinConfigMapper.selectTtRobotFightJoinConfigList(ttRobotFightJoinConfig); + } + + /** + * 新增机器人对战进房时间配置 + * + * @param ttRobotFightJoinConfig 机器人对战进房时间配置 + * @return 结果 + */ + @Override + public int insertTtRobotFightJoinConfig(TtRobotFightJoinConfig ttRobotFightJoinConfig) + { + return ttRobotFightJoinConfigMapper.insertTtRobotFightJoinConfig(ttRobotFightJoinConfig); + } + + /** + * 修改机器人对战进房时间配置 + * + * @param ttRobotFightJoinConfig 机器人对战进房时间配置 + * @return 结果 + */ + @Override + public int updateTtRobotFightJoinConfig(TtRobotFightJoinConfig ttRobotFightJoinConfig) + { + return ttRobotFightJoinConfigMapper.updateTtRobotFightJoinConfig(ttRobotFightJoinConfig); + } + + /** + * 批量删除机器人对战进房时间配置 + * + * @param ids 需要删除的机器人对战进房时间配置主键 + * @return 结果 + */ + @Override + public int deleteTtRobotFightJoinConfigByIds(Integer[] ids) + { + return ttRobotFightJoinConfigMapper.deleteTtRobotFightJoinConfigByIds(ids); + } + + /** + * 删除机器人对战进房时间配置信息 + * + * @param id 主键 + * @return 结果 + */ + @Override + public int deleteTtRobotFightJoinConfigById(Integer id) + { + return ttRobotFightJoinConfigMapper.deleteTtRobotFightJoinConfigById(id); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightPriceConfigServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightPriceConfigServiceImpl.java new file mode 100644 index 0000000..f36b462 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRobotFightPriceConfigServiceImpl.java @@ -0,0 +1,93 @@ +package com.ruoyi.admin.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.admin.mapper.TtRobotFightPriceConfigMapper; +import com.ruoyi.domain.other.TtRobotFightPriceConfig; +import com.ruoyi.admin.service.TtRobotFightPriceConfigService; + +/** + * 机器人对战价格档位配置Service业务层处理 + * + * @author ruoyi + * @date 2026-08-13 + */ +@Service +public class TtRobotFightPriceConfigServiceImpl implements TtRobotFightPriceConfigService +{ + @Autowired + private TtRobotFightPriceConfigMapper ttRobotFightPriceConfigMapper; + + /** + * 查询机器人对战价格档位配置 + * + * @param id 主键 + * @return 机器人对战价格档位配置 + */ + @Override + public TtRobotFightPriceConfig selectTtRobotFightPriceConfigById(Integer id) + { + return ttRobotFightPriceConfigMapper.selectTtRobotFightPriceConfigById(id); + } + + /** + * 查询机器人对战价格档位配置列表 + * + * @param ttRobotFightPriceConfig 机器人对战价格档位配置 + * @return 机器人对战价格档位配置 + */ + @Override + public List selectTtRobotFightPriceConfigList(TtRobotFightPriceConfig ttRobotFightPriceConfig) + { + return ttRobotFightPriceConfigMapper.selectTtRobotFightPriceConfigList(ttRobotFightPriceConfig); + } + + /** + * 新增机器人对战价格档位配置 + * + * @param ttRobotFightPriceConfig 机器人对战价格档位配置 + * @return 结果 + */ + @Override + public int insertTtRobotFightPriceConfig(TtRobotFightPriceConfig ttRobotFightPriceConfig) + { + return ttRobotFightPriceConfigMapper.insertTtRobotFightPriceConfig(ttRobotFightPriceConfig); + } + + /** + * 修改机器人对战价格档位配置 + * + * @param ttRobotFightPriceConfig 机器人对战价格档位配置 + * @return 结果 + */ + @Override + public int updateTtRobotFightPriceConfig(TtRobotFightPriceConfig ttRobotFightPriceConfig) + { + return ttRobotFightPriceConfigMapper.updateTtRobotFightPriceConfig(ttRobotFightPriceConfig); + } + + /** + * 批量删除机器人对战价格档位配置 + * + * @param ids 需要删除的机器人对战价格档位配置主键 + * @return 结果 + */ + @Override + public int deleteTtRobotFightPriceConfigByIds(Integer[] ids) + { + return ttRobotFightPriceConfigMapper.deleteTtRobotFightPriceConfigByIds(ids); + } + + /** + * 删除机器人对战价格档位配置信息 + * + * @param id 主键 + * @return 结果 + */ + @Override + public int deleteTtRobotFightPriceConfigById(Integer id) + { + return ttRobotFightPriceConfigMapper.deleteTtRobotFightPriceConfigById(id); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollJackpotOrnamentsServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollJackpotOrnamentsServiceImpl.java new file mode 100644 index 0000000..72f1a10 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollJackpotOrnamentsServiceImpl.java @@ -0,0 +1,103 @@ +package com.ruoyi.admin.service.impl; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.TtRollJackpotMapper; +import com.ruoyi.domain.dto.rollJackpotOrnament.RollJOEdit; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.roll.TtRollJackpot; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnaments; +import com.ruoyi.admin.mapper.TtRollJackpotOrnamentsMapper; +import com.ruoyi.admin.service.TtRollJackpotOrnamentsService; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnamentsBody; +import com.ruoyi.domain.vo.TtRollJackpotOrnamentsDataVO; +import com.ruoyi.common.utils.DateUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Service +public class TtRollJackpotOrnamentsServiceImpl extends ServiceImpl implements TtRollJackpotOrnamentsService { + + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private TtRollJackpotMapper ttRollJackpotMapper; + + @Override + public List queryList(TtRollJackpotOrnamentsBody rollJackpotOrnamentsBody) { + return baseMapper.queryList(rollJackpotOrnamentsBody); + } + + @Override + public String insertRollJackpotOrnaments(TtRollJackpotOrnaments ttRollJackpotOrnaments) { + ttRollJackpotOrnaments.setCreateTime(DateUtils.getNowDate()); + this.save(ttRollJackpotOrnaments); + recalcJackpotTotalPrice(ttRollJackpotOrnaments.getJackpotId()); + return ""; + } + + @Override + public String updateRollJackpotOrnamentsById(TtRollJackpotOrnaments rollJOEdit) { + + rollJOEdit.setUpdateTime(DateUtils.getNowDate()); + this.updateById(rollJOEdit); + + // 重新计算奖池总价 + recalcJackpotTotalPrice(rollJOEdit.getJackpotId()); + return ""; + } + + @Override + public String batchAdd(Integer rollJackpotId, List OrnamentsIds) { + + List ornIds = new ArrayList<>(OrnamentsIds); + + List ornamentList = new LambdaQueryChainWrapper<>(ttOrnamentMapper).in(TtOrnament::getId, ornIds).list(); + + for (TtOrnament ornament : ornamentList) { + TtRollJackpotOrnaments ttRollJackpotOrnaments = TtRollJackpotOrnaments.builder().build(); + ttRollJackpotOrnaments.setJackpotId(rollJackpotId); + ttRollJackpotOrnaments.setOrnamentsId(ornament.getId()); + ttRollJackpotOrnaments.setOrnamentsNum(1); + ttRollJackpotOrnaments.setPrice(ObjectUtil.isNotEmpty(ornament.getUsePrice())?ornament.getUsePrice():ornament.getPrice()); + ttRollJackpotOrnaments.setImgUrl(ornament.getImageUrl()); + ttRollJackpotOrnaments.setOrnamentName(ornament.getName()); + ttRollJackpotOrnaments.setCreateTime(DateUtils.getNowDate()); + this.save(ttRollJackpotOrnaments); + } + // 批量填货后重算奖池总价 + recalcJackpotTotalPrice(rollJackpotId); + return ""; + } + + /** + * 重新计算并更新奖池总价(price * num 累加),带空值防护 + */ + @Override + public void recalcJackpotTotalPrice(Integer jackpotId) { + if (jackpotId == null) return; + List list = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtRollJackpotOrnaments::getJackpotId, jackpotId) + .list(); + BigDecimal total = BigDecimal.ZERO; + for (TtRollJackpotOrnaments item : list) { + if (item.getPrice() != null && item.getOrnamentsNum() != null) { + total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getOrnamentsNum()))); + } + } + new LambdaUpdateChainWrapper<>(ttRollJackpotMapper) + .eq(TtRollJackpot::getJackpotId, jackpotId) + .set(TtRollJackpot::getTotalPrice, total) + .set(TtRollJackpot::getUpdateTime, new Date()) + .update(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollJackpotServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollJackpotServiceImpl.java new file mode 100644 index 0000000..a247283 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollJackpotServiceImpl.java @@ -0,0 +1,17 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.entity.roll.TtRollJackpot; +import com.ruoyi.admin.mapper.TtRollJackpotMapper; +import com.ruoyi.admin.service.TtRollJackpotService; +import org.springframework.stereotype.Service; + +@Service +public class TtRollJackpotServiceImpl extends ServiceImpl implements TtRollJackpotService { + + @Override + public String removeRollJackpotById(Long jackpotId) { + this.removeById(jackpotId); + return ""; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollServiceImpl.java new file mode 100644 index 0000000..d010441 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollServiceImpl.java @@ -0,0 +1,850 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.domain.body.AddRobotBody; +import com.ruoyi.admin.domain.body.AddUserBody; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtRollJackpotService; +import com.ruoyi.admin.service.TtRollUserService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.common.constant.roll.RollGetPrizeWay; +import com.ruoyi.domain.common.constant.roll.RollType; +import com.ruoyi.domain.dto.roll.GetRollPrizePool; +import com.ruoyi.domain.dto.roll.InviteRollUser; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.roll.*; +import com.ruoyi.admin.service.TtRollService; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.domain.vo.SimpleUserVO; +import com.ruoyi.domain.vo.TtRollPrizeDataVO; +import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsByPageVO; +import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsVO; +import com.ruoyi.domain.vo.roll.RollUserPrizeVO; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.validation.constraints.NotNull; +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordSource.ROLL; + +@Service +@Slf4j +public class TtRollServiceImpl extends ServiceImpl implements TtRollService { + + @Autowired + private TtRollUserMapper rollUserMapper; + + @Autowired + private TtRollUserService rollUserService; + + @Autowired + private TtBoxRecordsMapper boxRecordsMapper; + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private TtRollMapper ttRollMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtRechargeRecordMapper rechargeRecordMapper; + + // public TtRollServiceImpl(TtRollUserMapper rollUserMapper, + // TtBoxRecordsMapper boxRecordsMapper, + // RabbitTemplate rabbitTemplate) { + // this.rollUserMapper = rollUserMapper; + // this.boxRecordsMapper = boxRecordsMapper; + // this.rabbitTemplate = rabbitTemplate; + // } + + @Autowired + private TtRollJackpotService ttRollJackpotService; + + @Autowired + private TtRollJackpotMapper ttRollJackpotMapper; + + @Autowired + private TtRollJackpotOrnamentsMapper ttRollJackpotOrnamentsMapper; + + @Autowired + private TtRollUserPrizeMapper ttRollUserPrizeMapper; + + // @Autowired + // private TtRollUserMapper ttRollUserMapper; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private RedisLock redisLock; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtUserService userService; + + @Override + public List queryList(TtRollBody ttRollBody) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotNull(ttRollBody.getId())) wrapper.eq(TtRoll::getId, ttRollBody.getId()); + if (StringUtils.isNotEmpty(ttRollBody.getRollName())) + wrapper.like(TtRoll::getRollName, ttRollBody.getRollName()); + if (StringUtils.isNotEmpty(ttRollBody.getRollStatus())) + wrapper.eq(TtRoll::getRollStatus, ttRollBody.getRollStatus()); + // 排除白号创建的 Roll 房(保留平台/无创建者房间) + wrapper.and(w -> w.isNull(TtRoll::getUserId) + .or().notInSql(TtRoll::getUserId, "SELECT user_id FROM tt_user WHERE is_blank_account = 1")); + wrapper.orderByAsc(TtRoll::getRollStatus).orderByDesc(TtRoll::getCreateTime); + return this.list(wrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public AjaxResult createRoll(TtRoll ttRoll) { + + // 创建roll房 + Calendar c = Calendar.getInstance(); + c.add(Calendar.MINUTE, 5); + Timestamp criticalTime = new Timestamp(c.getTimeInMillis()); + + long endTime = ttRoll.getEndTime().getTime(); + + // long nowDate = DateUtils.getNowDate().getTime(); + long difference = endTime - criticalTime.getTime(); + if (difference < 0) return AjaxResult.error("开奖时间在临界保护区【5分钟】内。"); + + // 主播房(rollType=1)按关联奖池总价值扣减主播余额(金币+积分),逻辑对齐推广后台 + BigDecimal jackpotTotal = BigDecimal.ZERO; + TtUser anchor = null; + BigDecimal creditsCut = BigDecimal.ZERO; + boolean needDeduct = "1".equals(ttRoll.getRollType()) + && ttRoll.getUserId() != null && ttRoll.getJackpotId() != null; + if (needDeduct) { + TtRollJackpot jackpot = ttRollJackpotMapper.selectById(ttRoll.getJackpotId()); + if (jackpot != null && jackpot.getTotalPrice() != null) { + jackpotTotal = jackpot.getTotalPrice(); + } + if (jackpotTotal.compareTo(BigDecimal.ZERO) > 0) { + anchor = userMapper.selectById(ttRoll.getUserId()); + if (anchor == null) return AjaxResult.error("主播用户不存在"); + BigDecimal balance = anchor.getAccountAmount() != null ? anchor.getAccountAmount() : BigDecimal.ZERO; + if (jackpotTotal.compareTo(balance) > 0) { + return AjaxResult.error("余额不足:奖池总价值 " + jackpotTotal.stripTrailingZeros().toPlainString() + + " 金币,超过主播当前金币余额 " + balance.stripTrailingZeros().toPlainString()); + } + // 积分不足与金币一样拒绝创建 + BigDecimal anchorCredits = anchor.getAccountCredits() != null ? anchor.getAccountCredits() : BigDecimal.ZERO; + if (jackpotTotal.compareTo(anchorCredits) > 0) { + return AjaxResult.error("积分不足:奖池总价值 " + jackpotTotal.stripTrailingZeros().toPlainString() + + " 积分,超过主播当前积分余额 " + anchorCredits.stripTrailingZeros().toPlainString()); + } + // 积分已校验充足,实际扣除积分恒等于奖池全额(deductCredits=全额,保证退还与分解释放对称、全链路守恒) + creditsCut = jackpotTotal.min(anchorCredits); + } + } + // 记录本次创建实际扣除的积分(官方房/无扣费为0) + ttRoll.setDeductCredits(creditsCut); + + baseMapper.insert(ttRoll); + + // 扣减主播账户:金币全扣,积分扣 min(总价值, 当前积分)(对齐推广后台/商城扣减逻辑) + // 使用4参数版本,一笔流水同时记录金币与积分扣减,保持账变对称 + if (needDeduct && jackpotTotal.compareTo(BigDecimal.ZERO) > 0 && anchor != null) { + // 注意:不能使用 GAME_TYPE_03,否则建房扣款会被计入游戏投注流水(VIP返利/消费排行榜) + R deduct = userService.updateUserAccountKeepCredits(ttRoll.getUserId(), jackpotTotal.negate(), TtAccountRecordSource.PROMO_ROLL_DEDUCT, creditsCut); + if (R.isError(deduct)) { + throw new RuntimeException(deduct.getMsg() != null ? deduct.getMsg() : "扣减主播余额失败"); + } + } + + System.out.println("创建roll房:" + ttRoll.getId() + ttRoll.getRollName()); + + // 延时队列实现开奖 + // rabbitTemplate.convertAndSend( + // DelayedQueueConfig.DELAYED_EXCHANGE, + // DelayedQueueConfig.DELAYED_ROUTING_KEY, + // String.valueOf(ttRoll.getId()), + // (msg -> { + // msg.getMessageProperties().setDelay((int) difference); + // return msg; + // })); + + return AjaxResult.success(); + } + + @Override + public AjaxResult updateRollById(TtRoll ttRoll) { + + TtRoll one = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtRoll::getId, ttRoll.getId()) + .eq(TtRoll::getRollStatus, 0) + .eq(TtRoll::getDelFlag, 0) + .one(); + + Date oldEndTime = one.getEndTime(); + + // 此刻 + Calendar c = Calendar.getInstance(); + + long endTime = ttRoll.getEndTime().getTime(); + long difference = endTime - c.getTimeInMillis(); + if (difference < 10) return AjaxResult.error("开奖时间不能早于现在。"); + + // 临界区检查 + c.add(Calendar.MINUTE, 5); + long criticalTime = c.getTimeInMillis(); + if (criticalTime > oldEndTime.getTime()) { + return AjaxResult.error("该roll房开奖时间已处于临界保护区【5分钟】内,请勿修改开奖时间。"); + } + + baseMapper.updateById(ttRoll); + return AjaxResult.success(); + } + + /** + * 删除Roll房:主播房且未开奖时,退还创建时扣减的主播余额(金币+积分),逻辑对齐推广后台 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public AjaxResult removeRollById(Long rollId, String updateBy) { + TtRoll ttRoll = this.getById(rollId); + if (ttRoll == null) return AjaxResult.error("Roll房不存在"); + + // 主播房且未开奖时退款(官方房创建时未扣款,不退) + if ("1".equals(ttRoll.getRollType()) && !"1".equals(ttRoll.getRollStatus()) + && ttRoll.getUserId() != null && ttRoll.getJackpotId() != null) { + TtRollJackpot jackpot = ttRollJackpotMapper.selectById(ttRoll.getJackpotId()); + if (jackpot != null && jackpot.getTotalPrice() != null + && jackpot.getTotalPrice().compareTo(BigDecimal.ZERO) > 0) { + // 未开奖删除全额退回:金币退奖池总价值,积分退创建时实际扣除额(避免积分虚增) + userService.updateOnlyUserAccount(ttRoll.getUserId(), jackpot.getTotalPrice(), TtAccountRecordSource.ROLL_UNISSUED_REFUND); + BigDecimal refundCredits = ttRoll.getDeductCredits() != null ? ttRoll.getDeductCredits() : BigDecimal.ZERO; + if (refundCredits.compareTo(BigDecimal.ZERO) > 0) { + userService.updateUserCredits(ttRoll.getUserId(), refundCredits, TtAccountRecordSource.ROLL_UNISSUED_REFUND); + } + } + } + + ttRoll.setUpdateBy(updateBy); + ttRoll.setUpdateTime(DateUtils.getNowDate()); + this.updateById(ttRoll); + boolean ok = this.removeById(rollId); + return ok ? AjaxResult.success() : AjaxResult.error("删除失败"); + } + + @Override + public List getRollPrizeList(Integer rollId) { + + TtRoll ttRoll = this.getById(rollId); + + // TtRollJackpot jackpot = new LambdaQueryChainWrapper<>(ttRollJackpotMapper) + // .eq(TtRollJackpot::getJackpotId, ttRoll.getJackpotId()) + // .one(); + + List prizeList = new LambdaQueryChainWrapper<>(ttRollJackpotOrnamentsMapper) + .eq(TtRollJackpotOrnaments::getJackpotId, ttRoll.getJackpotId()) + .list(); + + // 所有系统指定 + List rollUserPrizes = ttRollUserPrizeMapper.byRollId(rollId); + + List res = new ArrayList<>(); + for (TtRollJackpotOrnaments orn : prizeList) { + for (int i = 0; i < orn.getOrnamentsNum(); i++) { + + TtOrnament ornament = new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .eq(TtOrnament::getId, orn.getOrnamentsId()) + .one(); + + TtRollPrizeDataVO vo = TtRollPrizeDataVO.builder() + .rollId(rollId) + .rollUserId(ttRoll.getUserId()) + .rollJackpotOrnamentId(orn.getId()) + .ornamentsId(orn.getOrnamentsId()) + .usePrice(ObjectUtil.isNotEmpty(ornament.getUsePrice()) ? ornament.getUsePrice() : ornament.getPrice()) + .shortName(ornament.getName()) + .itemName(ornament.getName()) + .imageUrl(ornament.getImageUrl()) + .ornamentNum(1) + .build(); + + if (rollUserPrizes.isEmpty()) { + res.add(vo); + continue; + } + + for (RollUserPrizeVO prize : rollUserPrizes) { + if (prize.getOrnamentId().equals(ornament.getId())) { + vo.setUserId(prize.getUserId()); + vo.setNickName(prize.getNickName()); + vo.setRollUserPrizeId(prize.getRollUserPrizeId()); + vo.setDesignatedBy(prize.getDesignatedBy()); + + rollUserPrizes.remove(prize); + break; + } + } + + res.add(vo); + } + } + + if (res.isEmpty()) { + return res; + } + + // 排序 + Collections.sort(res, new Comparator() { + @Override + public int compare(TtRollPrizeDataVO o1, TtRollPrizeDataVO o2) { + return o1.getUsePrice().compareTo(o2.getUsePrice()); + } + }); + + return res; + + // roll房奖池关联的饰品列表 + // List rollJackpotOrnamentsList = baseMapper.getRollJackpotOrnamentsList(rollId); + // + // List list = new ArrayList<>(); + // for (TtRollPrizeDataVO rollPrizeData : rollJackpotOrnamentsList) { + // Integer count = new LambdaQueryChainWrapper<>(rollUserMapper) + // .eq(TtRollUser::getRollId, rollPrizeData.getRollId()) + // .eq(TtRollUser::getJackpotOrnamentsId, rollPrizeData.getJackpotOrnamentsListId()) + // .count(); + // for (int i = 0; i < rollPrizeData.getOrnamentsNum() - count; i++) { + // list.add(rollPrizeData); + // } + // } + // List specifiedRollJackpotOrnamentsList = baseMapper.getSpecifiedRollJackpotOrnamentsList(rollId); + // + // list.addAll(specifiedRollJackpotOrnamentsList); + // + // List collect = list.stream().peek(rollPrizeData -> { + // rollPrizeData.setOrnamentsNum(1); + // }).collect(Collectors.toList()); + // + // return collect; + } + + // roll指定获奖 + @Override + public AjaxResult namedWinner(TtRollPrizeDataVO param) { + + // roll成员 + TtRollUser ttRollUser = rollUserMapper.selectOne( + Wrappers.lambdaQuery(TtRollUser.class) + .eq(TtRollUser::getRollId, param.getRollId()) + .eq(TtRollUser::getUserId, param.getUserId()) + ); + if (ObjectUtil.isEmpty(ttRollUser)) { + return AjaxResult.error("不存在的roll房成员。"); + } + + // roll房 + TtRoll roll = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtRoll::getId, ttRollUser.getRollId()) + .eq(TtRoll::getRollStatus, 0) + .eq(TtRoll::getDelFlag, 0) + .one(); + + if (ObjectUtil.isEmpty(roll)) return AjaxResult.error("roll房已结束。"); + + // 检查roll是否已结束 + Date endTime = roll.getEndTime(); + // 此刻 + Calendar c = Calendar.getInstance(); + if (c.getTime().compareTo(endTime) >= 0) return AjaxResult.error("该roll房已经结束。"); + // 临界区检查(要不要都无所谓) + c.add(Calendar.MINUTE, 5); + long criticalTime = c.getTimeInMillis(); + if (criticalTime > endTime.getTime()) + return AjaxResult.error("该roll房开奖时间已处于临界保护区【5分钟】内,禁止修改。"); + + + ttRollUser.setGetPrizeWay(RollGetPrizeWay.SYS.getCode()); + ttRollUser.setUpdateTime(new Date()); + + // 检查是否已指定 + List userPrizes = new LambdaQueryChainWrapper<>(ttRollUserPrizeMapper) + .eq(TtRollUserPrize::getRollUserId, ttRollUser.getId()) + .list(); + if (!userPrizes.isEmpty()) + return AjaxResult.error("已为roll房成员【" + ttRollUser.getNickName() + "】已指定奖品,请勿重复操作。"); + + // 奖品信息 + TtRollJackpotOrnaments prizeInfo = new LambdaQueryChainWrapper<>(ttRollJackpotOrnamentsMapper) + .eq(TtRollJackpotOrnaments::getId, param.getRollJackpotOrnamentId()) + .one(); + + // 检查已指定的物品数量 + Integer ownOrnamentNumber = ttRollUserPrizeMapper.ownOrnamentNumber(prizeInfo.getId(), ttRollUser.getRollId()); + ownOrnamentNumber = ObjectUtil.isNotEmpty(ownOrnamentNumber) ? ownOrnamentNumber : 0; + if (prizeInfo.getOrnamentsNum() <= ownOrnamentNumber) { + return AjaxResult.error("该物品已经被指派完,没有多余的物品可以指定。"); + } + + // 指定奖品 + TtRollUserPrize build = TtRollUserPrize.builder() + .rollUserId(ttRollUser.getId()) + .rollJackpotId(prizeInfo.getJackpotId()) + .rollJackpotOrnamentId(param.getRollJackpotOrnamentId()) + .ornamentId(prizeInfo.getOrnamentsId()) + .ornamentName(prizeInfo.getOrnamentName()) + .price(prizeInfo.getPrice()) + .imgUrl(prizeInfo.getImgUrl()) + .number(param.getOrnamentNum()) + .build(); + + new LambdaUpdateChainWrapper<>(rollUserMapper) + .eq(TtRollUser::getId, ttRollUser.getId()) + .set(TtRollUser::getGetPrizeWay, RollGetPrizeWay.SYS.getCode()) + .set(TtRollUser::getUpdateTime, new Date()) + .set(TtRollUser::getDesignatedBy, SecurityUtils.getUsername()) + .update(); + ttRollUserPrizeMapper.insert(build); + + return AjaxResult.success(); + + } + + @Override + public AjaxResult getRollUsers(Integer rollId) { + + List list = new LambdaQueryChainWrapper<>(rollUserMapper).eq(TtRollUser::getRollId, rollId).list(); + + List collect = list.stream().map(item -> { + return SimpleUserVO.builder() + .rollUserId(item.getId()) + .userId(item.getUserId()) + .nickName(item.getNickName()) + .userName(item.getUserName()) + .avatar(item.getAvatar()) + .build(); + }).collect(Collectors.toList()); + for (SimpleUserVO simpleUserVO : collect) { + System.err.println(simpleUserVO); + } + + return AjaxResult.success(collect); + } + + @Override + public R cancelNamedWinner(List rollUserPrizeIds) { + + int i = ttRollUserPrizeMapper.deleteBatchIds(rollUserPrizeIds); + if (i >= 0) return R.ok("成功取消" + i + "条奖品指定记录。"); + return R.fail(); + + } + + @Override + public R getRollPrizePool(GetRollPrizePool param) { + + param.setLimit((param.getPage() - 1) * param.getSize()); + + TtRoll one = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtRoll::getId, param.getRollId()) + .eq(TtRoll::getDelFlag, 0) + .one(); + if (ObjectUtil.isNull(one)) return R.fail("不存在的roll房。"); + + param.setJackpotId(one.getJackpotId()); + List list = ttRollJackpotOrnamentsMapper.listByRollId(param); + + Integer total = ttRollJackpotOrnamentsMapper.totalByCondition(param); + + RollJackpotOrnamentsByPageVO build = RollJackpotOrnamentsByPageVO.builder() + .list(list) + .total(total) + .build(); + + return R.ok(build); + } + + public R inviteRollUserCheck(InviteRollUser param) { + + // roll房 + TtRoll ttRoll = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtRoll::getId, param.getRollId()) + .eq(TtRoll::getRollStatus, "0") + .gt(TtRoll::getEndTime, DateUtils.getNowDate()) + .one(); + if (StringUtils.isNull(ttRoll)) return R.fail("当前Roll房已结束"); + + // + List rollUserList = new LambdaQueryChainWrapper<>(rollUserMapper) + .eq(TtRollUser::getRollId, param.getRollId()) + .list(); + if (ttRoll.getPeopleNum() <= rollUserList.size()) { + return R.fail("roll房人满。"); + } + + if (ttRoll.getPeopleNum() < rollUserList.size() + param.getUserIds().size()) { + return R.fail("加入人数超过roll房人数上限。"); + } + + // + List rollUsers = new LambdaQueryChainWrapper<>(rollUserMapper) + .in(TtRollUser::getUserId, param.getUserIds()) + .eq(TtRollUser::getRollId, param.getRollId()) + .list(); + if (!rollUsers.isEmpty()) return R.fail("已加入的成员不能重复加入。"); + + return R.ok(ttRoll); + } + + @Override + public R inviteRollUser(InviteRollUser param) { + + // roll房 + // TtRoll ttRoll = new LambdaQueryChainWrapper<>(baseMapper) + // .eq(TtRoll::getId, param.getRollId()) + // .eq(TtRoll::getRollStatus, "0") + // .gt(TtRoll::getEndTime, DateUtils.getNowDate()) + // .one(); + // roll房成员 + // TtRollUser rollUser = new LambdaQueryChainWrapper<>(ttRollUserMapper) + // .eq(TtRollUser::getUserId, player.getUserId()) + // .eq(TtRollUser::getRollId, param.getRollId()) + // .one(); + + // 检查 + // R check = inviteRollUserCheck(param); + // if (!check.getCode().equals(200)) return check; + + // 如果是主播房 + // if (ttRoll.getRollType().equals(RollType.ANCHOR.getCode())) { + // TtUser anchor = new LambdaQueryChainWrapper<>(userMapper) + // .eq(TtUser::getUserId, ttRoll.getUserId()) + // .eq(TtUser::getUserType, UserType.ANCHOR.getCode()) + // .eq(TtUser::getDelFlag, "0") + // .one(); + // // 检查是否粉丝 + // Integer parentId = player.getParentId(); + // if (StringUtils.isNull(parentId) || !anchor.getUserId().equals(parentId)) { + // return R.fail("您不是该主播的粉丝,请先加入主播粉丝团才可加入房间!"); + // } + // } + + // 加入房间 + Boolean lock = false; + for (int i = 0; i < 3; i++) { + + lock = redisLock.tryLock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId(), 2L, 15L, TimeUnit.SECONDS); + + if (lock) break; + + } + + if (lock) { + try { + + R check = inviteRollUserCheck(param); + if (!check.getCode().equals(200)) return check; + + TtRoll roll = check.getData(); + + List userList = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getDelFlag, "0") + .eq(TtUser::getStatus, "0") + .in(TtUser::getUserId, param.getUserIds()) + .list(); + + List rollUserList = new ArrayList<>(); + for (TtUser player : userList) { + TtRollUser ttRollUser = TtRollUser.builder() + .rollId(param.getRollId()) + .userId(player.getUserId()) + .userName(player.getUserName()) + .nickName(player.getNickName()) + .avatar(player.getAvatar()) + .joinTime(DateUtils.getNowDate()) + .build(); + rollUserList.add(ttRollUser); + } + + if (rollUserService.saveBatch(rollUserList, 1)) return R.ok(); + + return R.fail("Sava db fail"); + + } catch (Exception e) { + e.printStackTrace(); + return R.fail("加入失败,请稍后重试。"); + } finally { + redisLock.unlock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId()); + } + } + + // redisLock.unlock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId()); + return R.ok("服务器繁忙,请稍后重试。"); + + } + + @Override + public List getRollUser(Integer rollId) { + return ttRollMapper.getRollUser(rollId); + } + + /** + * Roll房实际中奖记录(开奖后参与用户中了什么饰品) + * 数据口径:tt_box_records 中 source=roll房(3) 且 rollId 匹配的记录,为开奖后实际发放的饰品 + * 管理端展示全部中奖记录(含机器人/白号),与 getRollUser 口径一致 + */ + @Override + public R>> winningRecords(Integer rollId) { + // 房间参与用户,用于补充昵称/用户名 + List rollUsers = new LambdaQueryChainWrapper<>(rollUserMapper) + .eq(TtRollUser::getRollId, rollId) + .list(); + Map userMap = new HashMap<>(); + for (TtRollUser u : rollUsers) { + if (u.getUserId() != null) { + userMap.put(u.getUserId(), u); + } + } + + // 中奖饰品记录(开奖实际发放) + List records = new LambdaQueryChainWrapper<>(boxRecordsMapper) + .eq(TtBoxRecords::getRollId, rollId) + .eq(TtBoxRecords::getSource, ROLL.getCode()) + .orderByAsc(TtBoxRecords::getCreateTime) + .orderByAsc(TtBoxRecords::getId) + .list(); + + List> result = new ArrayList<>(); + for (TtBoxRecords r : records) { + TtRollUser u = userMap.get(r.getUserId()); + Map item = new HashMap<>(); + item.put("userId", r.getUserId()); + item.put("nickName", u != null ? u.getNickName() : null); + item.put("userName", u != null ? u.getUserName() : null); + item.put("ornamentId", r.getOrnamentId()); + item.put("ornamentName", r.getOrnamentName()); + item.put("imageUrl", r.getImageUrl()); + item.put("ornamentsPrice", r.getOrnamentsPrice()); + item.put("createTime", r.getCreateTime() != null + ? DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, r.getCreateTime()) : null); + result.add(item); + } + return R.ok(result); + } + + @Override + public AjaxResult addRobot(AddRobotBody addRobotBody) { + // 查询所有机器人ID + List robotIdList = ttUserMapper.selectObjs( + new QueryWrapper() + .select("user_id") + .eq("user_type", "03") + ); + if (robotIdList.isEmpty()) { + return AjaxResult.error("用户表中没有机器人,请先添加机器人用户"); + } + + // 查询已经在房间中的机器人ID + List rollRobotIds = ttRollMapper.getRollRobotIds(addRobotBody.getRollId()); + + // 过滤掉已经在房间中的机器人ID + List filteredRobotIdList = robotIdList.stream() + .filter(robotId -> !rollRobotIds.contains(robotId)) // 过滤掉已经存在的机器人ID + .collect(Collectors.toList()); + if (filteredRobotIdList.size() < addRobotBody.getRobotNum()) { + return AjaxResult.error("可用机器人数量不足"); + } + + // 打乱机器人ID顺序 + Collections.shuffle(filteredRobotIdList); + + for (int i = 0; i < addRobotBody.getRobotNum(); i++) { + TtUser player = userService.getById((Serializable) filteredRobotIdList.get(i)); + // 加入ROLL房 + JoinRollParam joinRollParam = new JoinRollParam(); + joinRollParam.setRollId(addRobotBody.getRollId()); + R r = joinRoll(joinRollParam, player); + log.info("r is {}", r != null ? r.getCode() + ":" + r.getMsg() : null); + } + return AjaxResult.success(); + } + + @Override + public AjaxResult addUser(AddUserBody addUserBody) { + TtUser player = userService.getById(addUserBody.getUserId()); + if (player == null) { + return AjaxResult.error("用户不存在"); + } + JoinRollParam joinRollParam = new JoinRollParam(); + joinRollParam.setRollId(addUserBody.getRollId()); + R r = joinRoll(joinRollParam, player); + if (r == null || !r.getCode().equals(200)) { + return AjaxResult.error(r != null ? r.getMsg() : "加入失败"); + } + return AjaxResult.success(); + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class JoinRollParam { + @NotNull(message = "ROLL房ID不能为空。") + private Integer rollId; + private String rollPassword; + } + + private R joinRoll(JoinRollParam param, TtUser player) { + + // roll房 + TtRoll ttRoll = new LambdaQueryChainWrapper<>(ttRollMapper) + .eq(TtRoll::getId, param.getRollId()) + .eq(TtRoll::getRollStatus, "0") + .gt(TtRoll::getEndTime, DateUtils.getNowDate()) + .one(); + + // roll房成员 + TtRollUser rollUser = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper()) + .eq(TtRollUser::getUserId, player.getUserId()) + .eq(TtRollUser::getRollId, param.getRollId()) + .one(); + + // 检查 + R check = joinRollCheck(ttRoll, rollUser, player, param); + if (!check.getCode().equals(200)) return check; + + // 如果是主播房 + if (ttRoll.getRollType().equals(RollType.ANCHOR.getCode())) { + TtUser anchor = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, ttRoll.getUserId()) + .eq(TtUser::getUserType, UserType.ANCHOR.getCode()) + .eq(TtUser::getDelFlag, "0") + .one(); + // 检查是否粉丝 + Integer parentId = player.getParentId(); + if (StringUtils.isNull(parentId) || !anchor.getUserId().equals(parentId)) { + return R.fail("您不是该主播的粉丝,请先加入主播粉丝团才可加入房间!"); + } + } + + // 加入房间 + Boolean lock = false; + for (int i = 0; i < 2; i++) { + + lock = redisLock.tryLock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId(), 2L, 7L, TimeUnit.SECONDS); + + if (!lock) { + try { + TimeUnit.SECONDS.sleep(1L); + } catch (Exception e) { + log.warn("try lock sleep warn"); + } finally { + continue; + } + } else { + break; + } + + } + + if (lock) { + try { + + List rollUserList = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper()) + .eq(TtRollUser::getRollId, param.getRollId()) + .list(); + + if (ttRoll.getPeopleNum() <= rollUserList.size()) { + return R.fail("roll房人满。"); + } + + TtRollUser ttRollUser = TtRollUser.builder() + .rollId(param.getRollId()) + .userId(player.getUserId()) + .userName(player.getUserName()) + .nickName(player.getNickName()) + .avatar(player.getAvatar()) + .joinTime(DateUtils.getNowDate()) + .build(); + + rollUserService.getBaseMapper().insert(ttRollUser); + + return R.ok(); + + } catch (Exception e) { + e.printStackTrace(); + return R.fail("加入失败,请稍后重试。"); + } finally { + redisLock.unlock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId()); + } + } + + redisLock.unlock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId()); + return R.fail("服务器繁忙。"); + } + + private R joinRollCheck(TtRoll ttRoll, TtRollUser rollUser, TtUser player, JoinRollParam param) { + + if (StringUtils.isNull(ttRoll)) return R.fail("当前Roll房已结束"); + if (StringUtils.isNotNull(rollUser)) return R.fail("您当前已在该房间内,请勿重复加入房间!"); + + String password = ttRoll.getRollPassword(); + if (StringUtils.isNotEmpty(password)) { + if (StringUtils.isEmpty(param.getRollPassword())) return R.fail("请输入密码!"); + if (!password.equals(param.getRollPassword())) return R.fail("密码错误!"); + } + + // 检查加入条件:充值下限 + BigDecimal minRecharge = ttRoll.getMinRecharge(); + if (minRecharge.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal rechargeTotalAmount = getRechargeTotalAmount(player.getUserId(), ttRoll.getRechargeStartTime()); + if (!"03".equals(player.getUserType()) && minRecharge.compareTo(rechargeTotalAmount) > 0) return R.fail("未满足该房间的充值条件!"); + } + + return R.ok(); + } + + private BigDecimal getRechargeTotalAmount(Integer userId, Date rechargeStartTime) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(TtRechargeRecord::getUserId, userId) + .eq(TtRechargeRecord::getStatus, "0") + .eq(TtRechargeRecord::getDelFlag, "0") + .and(w -> w.isNull(TtRechargeRecord::getIsBlankCard).or().ne(TtRechargeRecord::getIsBlankCard, 1)); + if (StringUtils.isNotNull(rechargeStartTime)) wrapper.gt(TtRechargeRecord::getCreateTime, rechargeStartTime); + return rechargeRecordMapper.selectList(wrapper).stream() + .map(e -> e.getAmountActuallyPaid() == null ? BigDecimal.ZERO : e.getAmountActuallyPaid()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollUserPrizeServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollUserPrizeServiceImpl.java new file mode 100644 index 0000000..19fd567 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollUserPrizeServiceImpl.java @@ -0,0 +1,14 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtRollUserMapper; +import com.ruoyi.admin.mapper.TtRollUserPrizeMapper; +import com.ruoyi.admin.service.TtRollUserPrizeService; +import com.ruoyi.admin.service.TtRollUserService; +import com.ruoyi.domain.entity.roll.TtRollUser; +import com.ruoyi.domain.entity.roll.TtRollUserPrize; +import org.springframework.stereotype.Service; + +@Service +public class TtRollUserPrizeServiceImpl extends ServiceImpl implements TtRollUserPrizeService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollUserServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollUserServiceImpl.java new file mode 100644 index 0000000..d675d26 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtRollUserServiceImpl.java @@ -0,0 +1,23 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.roll.TtRollUser; +import com.ruoyi.admin.mapper.TtRollUserMapper; +import com.ruoyi.admin.service.TtRollUserService; +import com.ruoyi.domain.vo.roll.RollUserPrizeVO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TtRollUserServiceImpl extends ServiceImpl implements TtRollUserService { + @Override + public R rollWinners(Integer rollId) { + + List list = baseMapper.rollWinners(rollId); + + return R.ok(list); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTaskCenterServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTaskCenterServiceImpl.java new file mode 100644 index 0000000..cc2a500 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTaskCenterServiceImpl.java @@ -0,0 +1,96 @@ +package com.ruoyi.admin.service.impl; + +import java.util.List; +import com.ruoyi.common.utils.DateUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.admin.mapper.TtTaskCenterMapper; +import com.ruoyi.domain.other.TtTaskCenter; +import com.ruoyi.admin.service.TtTaskCenterService; + +/** + * 任务中心Service业务层处理 + * + * @author ruoyi + * @date 2024-05-25 + */ +@Service +public class TtTaskCenterServiceImpl implements TtTaskCenterService +{ + @Autowired + private TtTaskCenterMapper ttTaskCenterMapper; + + /** + * 查询任务中心 + * + * @param taskId 任务中心主键 + * @return 任务中心 + */ + @Override + public TtTaskCenter selectTtTaskCenterByTaskId(Integer taskId) + { + return ttTaskCenterMapper.selectTtTaskCenterByTaskId(taskId); + } + + /** + * 查询任务中心列表 + * + * @param ttTaskCenter 任务中心 + * @return 任务中心 + */ + @Override + public List selectTtTaskCenterList(TtTaskCenter ttTaskCenter) + { + return ttTaskCenterMapper.selectTtTaskCenterList(ttTaskCenter); + } + + /** + * 新增任务中心 + * + * @param ttTaskCenter 任务中心 + * @return 结果 + */ + @Override + public int insertTtTaskCenter(TtTaskCenter ttTaskCenter) + { + ttTaskCenter.setCreateTime(DateUtils.getNowDate()); + return ttTaskCenterMapper.insertTtTaskCenter(ttTaskCenter); + } + + /** + * 修改任务中心 + * + * @param ttTaskCenter 任务中心 + * @return 结果 + */ + @Override + public int updateTtTaskCenter(TtTaskCenter ttTaskCenter) + { + ttTaskCenter.setUpdateTime(DateUtils.getNowDate()); + return ttTaskCenterMapper.updateTtTaskCenter(ttTaskCenter); + } + + /** + * 批量删除任务中心 + * + * @param taskIds 需要删除的任务中心主键 + * @return 结果 + */ + @Override + public int deleteTtTaskCenterByTaskIds(Integer[] taskIds) + { + return ttTaskCenterMapper.deleteTtTaskCenterByTaskIds(taskIds); + } + + /** + * 删除任务中心信息 + * + * @param taskId 任务中心主键 + * @return 结果 + */ + @Override + public int deleteTtTaskCenterByTaskId(Integer taskId) + { + return ttTaskCenterMapper.deleteTtTaskCenterByTaskId(taskId); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTimeRollServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTimeRollServiceImpl.java new file mode 100644 index 0000000..4f0f34c --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTimeRollServiceImpl.java @@ -0,0 +1,150 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.roll.TtTimeRoll; +import com.ruoyi.domain.entity.roll.TtTimeRollUser; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtTimeRollMapper; +import com.ruoyi.admin.service.TtTimeRollService; +import com.ruoyi.admin.service.TtTimeRollUserService; +import com.ruoyi.domain.vo.TtRollPrizeDataVO; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.service.ISysJobService; +import org.quartz.SchedulerException; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordSource.ROLL; + +@Service +public class TtTimeRollServiceImpl extends ServiceImpl implements TtTimeRollService { + + private final ISysJobService sysJobService; + private final TtTimeRollUserService timeRollUserService; + private final TtBoxRecordsMapper boxRecordsMapper; + + public TtTimeRollServiceImpl(ISysJobService sysJobService, + TtTimeRollUserService timeRollUserService, + TtBoxRecordsMapper boxRecordsMapper) { + this.sysJobService = sysJobService; + this.timeRollUserService = timeRollUserService; + this.boxRecordsMapper = boxRecordsMapper; + } + + @Override + public String insertTimeRoll(TtTimeRoll ttTimeRoll) throws SchedulerException, TaskException { + this.save(ttTimeRoll); + SysJob sysJob = new SysJob(); + sysJob.setJobGroup("TimeRollTask"); + sysJob.setInvokeTarget("TimeRollTask.timeRollSettle(" + ttTimeRoll.getId() + ")"); + sysJob.setCreateBy(SecurityUtils.getUsername()); + sysJob.setCreateTime(DateUtils.getNowDate()); + if ("0".equals(ttTimeRoll.getRechargeCondition())) { + sysJob.setJobName("0"); + sysJob.setCronExpression("0 0 * * * ?"); + } else if ("1".equals(ttTimeRoll.getRechargeCondition())) { + sysJob.setJobName("1"); + sysJob.setCronExpression("0 0 0 * * ?"); + } else if ("2".equals(ttTimeRoll.getRechargeCondition())) { + sysJob.setJobName("2"); + sysJob.setCronExpression("0 0 0 ? * 1"); + } else if ("3".equals(ttTimeRoll.getRechargeCondition())) { + sysJob.setJobName("3"); + sysJob.setCronExpression("0 0 0 1 * ?"); + } + sysJobService.insertJob(sysJob); + ttTimeRoll.setJobId(sysJob.getJobId().intValue()); + this.updateById(ttTimeRoll); + return ""; + } + + @Override + public AjaxResult removeTimeRollById(Integer id) { + this.removeById(id); + return AjaxResult.success(); + } + + @Override + public String changeStatus(TtTimeRoll ttTimeRoll) throws SchedulerException { + TtTimeRoll timeRoll = this.getById(ttTimeRoll.getId()); + timeRoll.setStatus(ttTimeRoll.getStatus()); + this.updateById(timeRoll); + SysJob newJob = sysJobService.selectJobById(timeRoll.getJobId().longValue()); + newJob.setStatus(ttTimeRoll.getStatus()); + sysJobService.changeStatus(newJob); + return ""; + } + + @Override + public List getTimeRollPrizeList(Integer id) { + List rollJackpotOrnamentsList = baseMapper.getRollJackpotOrnamentsList(id); + List list = new ArrayList<>(); + for (TtRollPrizeDataVO rollPrizeData : rollJackpotOrnamentsList) { + Long count = new LambdaQueryChainWrapper<>(timeRollUserService.getBaseMapper()) + .eq(TtTimeRollUser::getTimeRollId, rollPrizeData.getRollId()) + .eq(TtTimeRollUser::getJackpotOrnamentsId, rollPrizeData.getJackpotOrnamentsListId()) + .eq(TtTimeRollUser::getEndStatus, "0") + .count(); + for (int i = 0; i < rollPrizeData.getOrnamentNum() - count; i++) { + list.add(rollPrizeData); + } + } + List specifiedRollJackpotOrnamentsList = baseMapper.getSpecifiedTimeRollJackpotOrnamentsList(id); + list.addAll(specifiedRollJackpotOrnamentsList); + return list.stream().peek(rollPrizeData -> rollPrizeData.setOrnamentNum(1)).collect(Collectors.toList()); + } + + @Override + public String namedWinner(TtRollPrizeDataVO rollPrizeData) { + rollPrizeData.setDesignatedBy(""); + if (StringUtils.isNotNull(rollPrizeData.getRollUserId())) { + TtTimeRollUser ttTimeRollUser = timeRollUserService.getById(rollPrizeData.getRollUserId()); + if (!Objects.equals(rollPrizeData.getUserId(), ttTimeRollUser.getUserId())) { + ttTimeRollUser.setUserId(rollPrizeData.getUserId()); + ttTimeRollUser.setUpdateTime(DateUtils.getNowDate()); + timeRollUserService.updateById(ttTimeRollUser); + TtBoxRecords ttBoxRecords = boxRecordsMapper.selectById(ttTimeRollUser.getBoxRecordId()); + ttBoxRecords.setUserId(rollPrizeData.getUserId()); + ttBoxRecords.setUpdateTime(DateUtils.getNowDate()); + ttBoxRecords.setHolderUserId(rollPrizeData.getUserId()); + boxRecordsMapper.updateById(ttBoxRecords); + } + return ""; + } + TtBoxRecords boxRecords = TtBoxRecords.builder().build(); + boxRecords.setUserId(rollPrizeData.getUserId()); + boxRecords.setOrnamentId(rollPrizeData.getOrnamentsId()); + boxRecords.setOrnamentsPrice(rollPrizeData.getUsePrice()); + boxRecords.setOrnamentsLevelId(rollPrizeData.getOrnamentsLevelId()); + boxRecords.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); + boxRecords.setCreateTime(DateUtils.getNowDate()); + boxRecords.setSource(ROLL.getCode()); + boxRecords.setRollId(rollPrizeData.getRollId()); + boxRecords.setHolderUserId(rollPrizeData.getUserId()); + if (boxRecordsMapper.insert(boxRecords) > 0) { + TtTimeRollUser timeRollUser = TtTimeRollUser.builder().build(); + timeRollUser.setTimeRollId(rollPrizeData.getRollId()); + timeRollUser.setUserId(rollPrizeData.getUserId()); + timeRollUser.setJackpotOrnamentsId(rollPrizeData.getJackpotOrnamentsListId()); + timeRollUser.setOrnamentsId(rollPrizeData.getOrnamentsId()); + timeRollUser.setBoxRecordId(boxRecords.getId()); + timeRollUser.setStatus("2"); + timeRollUser.setDesignatedBy(SecurityUtils.getUsername()); + timeRollUser.setJoinTime(DateUtils.getNowDate()); + if (timeRollUserService.save(timeRollUser)) return ""; + } + return "error!"; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTimeRollUserServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTimeRollUserServiceImpl.java new file mode 100644 index 0000000..aea55c5 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTimeRollUserServiceImpl.java @@ -0,0 +1,11 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.entity.roll.TtTimeRollUser; +import com.ruoyi.admin.mapper.TtTimeRollUserMapper; +import com.ruoyi.admin.service.TtTimeRollUserService; +import org.springframework.stereotype.Service; + +@Service +public class TtTimeRollUserServiceImpl extends ServiceImpl implements TtTimeRollUserService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTransferApplyServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTransferApplyServiceImpl.java new file mode 100644 index 0000000..51fd766 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtTransferApplyServiceImpl.java @@ -0,0 +1,343 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.BoxTransferRecordAdminMapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtTransferApplyMapper; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.BoxTransferRecord; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.transfer.TtTransferApply; +import com.ruoyi.admin.service.TtTransferApplyService; +import com.ruoyi.admin.service.TtUserTransferDailyService; +import com.ruoyi.domain.other.TransferApplyBody; +import com.ruoyi.message.service.MessageService; +import com.ruoyi.domain.vo.transfer.TransferApplyBatchVO; +import com.ruoyi.domain.vo.transfer.TransferApplyVO; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Service +public class TtTransferApplyServiceImpl extends ServiceImpl implements TtTransferApplyService { + + private final TtBoxRecordsMapper boxRecordsMapper; + private final BoxTransferRecordAdminMapper boxTransferRecordAdminMapper; + private final TtUserTransferDailyService userTransferDailyService; + private final MessageService messageService; + private final RedisTemplate redisTemplate; + + public TtTransferApplyServiceImpl(TtBoxRecordsMapper boxRecordsMapper, + BoxTransferRecordAdminMapper boxTransferRecordAdminMapper, + TtUserTransferDailyService userTransferDailyService, + MessageService messageService, + RedisTemplate redisTemplate) { + this.boxRecordsMapper = boxRecordsMapper; + this.boxTransferRecordAdminMapper = boxTransferRecordAdminMapper; + this.userTransferDailyService = userTransferDailyService; + this.messageService = messageService; + this.redisTemplate = redisTemplate; + } + + @Override + public List getTransferApplyList(TransferApplyBody body) { + return baseMapper.getTransferApplyList(body); + } + + @Override + public List getTransferApplyBatchList(TransferApplyBody body) { + // PageHelper 会拦截下面这个查询进行分页(按批次数量统计) + List batchIds = baseMapper.getTransferApplyBatchIds(body); + if (CollectionUtils.isEmpty(batchIds)) { + return Collections.emptyList(); + } + return baseMapper.getTransferApplyBatchListByBatchIds(batchIds, body); + } + + @Override + @Transactional + public String approve(Integer id) { + TtTransferApply apply = this.getById(id); + String msg = doApprove(apply); + if (!msg.isEmpty()) { + return msg; + } + // 单条审核:等待整个批次全部处理完毕后再统一操作次数 + Integer submitVipLevel = apply.getVipLevel() != null ? apply.getVipLevel() : 0; + return finishBatchIfNeeded(apply.getBatchId(), apply.getFromUserId(), submitVipLevel, toLocalDate(apply.getCreateTime())); + } + + @Override + @Transactional + public String reject(Integer id, String rejectReason) { + TtTransferApply apply = this.getById(id); + String msg = doReject(apply, rejectReason); + if (!msg.isEmpty()) { + return msg; + } + Integer submitVipLevel = apply.getVipLevel() != null ? apply.getVipLevel() : 0; + return finishBatchIfNeeded(apply.getBatchId(), apply.getFromUserId(), submitVipLevel, toLocalDate(apply.getCreateTime())); + } + + @Override + @Transactional + public String approveBatch(String batchId) { + if (batchId == null || batchId.isEmpty()) { + return "批次号不能为空"; + } + List applies = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtTransferApply::getBatchId, batchId) + .eq(TtTransferApply::getStatus, 0) + .list(); + if (CollectionUtils.isEmpty(applies)) { + return "该批次没有待审核记录"; + } + for (TtTransferApply apply : applies) { + String msg = doApprove(apply); + if (!msg.isEmpty()) { + return msg; + } + } + // 批次审核:统一操作次数 + TtTransferApply first = applies.get(0); + Integer submitVipLevel = first.getVipLevel() != null ? first.getVipLevel() : 0; + return finishBatchIfNeeded(batchId, first.getFromUserId(), submitVipLevel, toLocalDate(first.getCreateTime())); + } + + @Override + @Transactional + public String rejectBatch(String batchId, String rejectReason) { + if (batchId == null || batchId.isEmpty()) { + return "批次号不能为空"; + } + List applies = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtTransferApply::getBatchId, batchId) + .eq(TtTransferApply::getStatus, 0) + .list(); + if (CollectionUtils.isEmpty(applies)) { + return "该批次没有待审核记录"; + } + for (TtTransferApply apply : applies) { + String msg = doReject(apply, rejectReason); + if (!msg.isEmpty()) { + return msg; + } + } + // 批次驳回:统一操作次数 + TtTransferApply first = applies.get(0); + Integer submitVipLevel = first.getVipLevel() != null ? first.getVipLevel() : 0; + return finishBatchIfNeeded(batchId, first.getFromUserId(), submitVipLevel, toLocalDate(first.getCreateTime())); + } + + /** + * 批次收尾:当整个批次全部处理完毕后,统一确认或返还次数。 + * 通过 Redis 标记 + 数据库 times_deducted 字段双重校验,防止并发/过期导致的重复扣次。 + */ + private String finishBatchIfNeeded(String batchId, Integer userId, Integer vipLevel, LocalDate applyDate) { + if (batchId == null || batchId.isEmpty()) { + // 旧数据没有批次号,不处理次数 + return ""; + } + + // 1. 如果批次中还有待审核记录,说明批次未处理完,不操作次数 + long pendingCount = this.lambdaQuery() + .eq(TtTransferApply::getBatchId, batchId) + .eq(TtTransferApply::getStatus, 0) + .count(); + if (pendingCount > 0) { + return ""; + } + + // 2. 数据库幂等:该批次次数是否已处理过(防止 Redis 过期后重复扣/放) + long alreadyDeductedCount = this.lambdaQuery() + .eq(TtTransferApply::getBatchId, batchId) + .eq(TtTransferApply::getTimesDeducted, 1) + .count(); + if (alreadyDeductedCount > 0) { + return ""; + } + + // 3. Redis 快速防重(短期) + String processedKey = "transfer:batch:processed:" + batchId; + Boolean alreadyProcessed = (Boolean) redisTemplate.opsForValue().get(processedKey); + if (alreadyProcessed != null && alreadyProcessed) { + return ""; + } + + // 4. 判断这批里有没有通过的记录 + long approvedCount = this.lambdaQuery() + .eq(TtTransferApply::getBatchId, batchId) + .eq(TtTransferApply::getStatus, 1) + .count(); + + boolean success; + if (approvedCount > 0) { + success = userTransferDailyService.confirmUsed(userId, vipLevel, applyDate); + } else { + success = userTransferDailyService.releasePending(userId, vipLevel, applyDate); + } + + if (!success) { + return approvedCount > 0 ? "确认消耗次数失败" : "释放次数失败"; + } + + // 5. 标记该批次次数已处理(数据库幂等兜底) + this.lambdaUpdate() + .eq(TtTransferApply::getBatchId, batchId) + .set(TtTransferApply::getTimesDeducted, 1) + .update(); + + // 6. 数据库操作成功后再标记 Redis(短期快速防重) + redisTemplate.opsForValue().set(processedKey, true, 7, TimeUnit.DAYS); + return ""; + } + + /** + * 处理单条审核通过(不操作次数) + */ + private String doApprove(TtTransferApply apply) { + if (apply == null) { + return "转赠申请记录不存在"; + } + if (apply.getStatus() != null && apply.getStatus() != 0) { + return "该申请已审核,不可重复审核"; + } + + // 1. 查找对应的背包记录 + if (apply.getBoxRecordId() == null) { + return "转赠申请缺少背包记录 ID,无法执行转赠"; + } + TtBoxRecords boxRecord = boxRecordsMapper.selectById(apply.getBoxRecordId()); + if (boxRecord == null) { + return "背包记录不存在,可能已被处理"; + } + + // 2. 校验饰品是否仍属于转赠人且在背包中 + boolean inMainAccount = apply.getFromUserId().equals(boxRecord.getHolderUserId()) + && (TtboxRecordStatus.IN_PACKSACK_ON.getCode().equals(boxRecord.getStatus()) + || TtboxRecordStatus.APPLY_DELIVERY.getCode().equals(boxRecord.getStatus()) + || TtboxRecordStatus.TRANSFER_PENDING.getCode().equals(boxRecord.getStatus())); + if (!inMainAccount) { + return "饰品状态已变更或已不在转赠人背包中,无法审核"; + } + + // 3. 执行转赠:将饰品转移给副号 + boxRecord.setUserId(apply.getToUserId()); + boxRecord.setHolderUserId(apply.getToUserId()); + boxRecord.setSource(TtboxRecordSource.TRANSFER.getCode()); + boxRecord.setStatus(TtboxRecordStatus.APPLY_DELIVERY.getCode()); + boxRecord.setUpdateTime(DateUtils.getNowDate()); + boxRecordsMapper.updateById(boxRecord); + + // 4. 同步更新转赠记录状态为成功 + BoxTransferRecord transferRecord = new LambdaQueryChainWrapper<>(boxTransferRecordAdminMapper) + .eq(BoxTransferRecord::getBoxRecordId, boxRecord.getId()) + .eq(BoxTransferRecord::getStatus, 0) + .one(); + if (transferRecord != null) { + transferRecord.setStatus(1); + transferRecord.setUpdateTime(LocalDateTime.now()); + boxTransferRecordAdminMapper.updateById(transferRecord); + } + + // 5. 更新转赠申请状态为已通过 + apply.setStatus(1); + apply.setUpdateBy(SecurityUtils.getUsername()); + apply.setUpdateTime(DateUtils.getNowDate()); + this.updateById(apply); + + // 6. 发送转赠邮件通知(赠送方 + 受赠方) + String ornamentName = apply.getOrnamentName() != null ? apply.getOrnamentName() : ""; + String category = MessageCategoryEnum.SYS_TRANSFER.getCode(); + String title = MessageCategoryEnum.SYS_TRANSFER.getTitle(); + // 受赠方 + messageService.sendNotice( + Long.valueOf(apply.getToUserId()), + title, + "玩家 " + apply.getFromUserId() + " 向您转赠了“" + ornamentName + "”,已放入背包。", + category); + // 赠送方 + messageService.sendNotice( + Long.valueOf(apply.getFromUserId()), + title, + "您向玩家 " + apply.getToUserId() + " 转赠了“" + ornamentName + "”,转赠成功。", + category); + + return ""; + } + + /** + * 处理单条审核驳回(不操作次数) + */ + private String doReject(TtTransferApply apply, String rejectReason) { + if (apply == null) { + return "转赠申请记录不存在"; + } + if (apply.getStatus() != null && apply.getStatus() != 0) { + return "该申请已审核,不可重复审核"; + } + + // 1. 查找对应的背包记录 + if (apply.getBoxRecordId() == null) { + return "转赠申请缺少背包记录 ID,无法执行退库"; + } + TtBoxRecords boxRecord = boxRecordsMapper.selectById(apply.getBoxRecordId()); + if (boxRecord == null) { + return "背包记录不存在,可能已被处理"; + } + + // 2. 校验饰品是否仍属于转赠人且在背包中 + boolean inMainAccountReject = apply.getFromUserId().equals(boxRecord.getHolderUserId()) + && (TtboxRecordStatus.IN_PACKSACK_ON.getCode().equals(boxRecord.getStatus()) + || TtboxRecordStatus.APPLY_DELIVERY.getCode().equals(boxRecord.getStatus()) + || TtboxRecordStatus.TRANSFER_PENDING.getCode().equals(boxRecord.getStatus())); + if (!inMainAccountReject) { + return "饰品状态已变更或已不在转赠人背包中,无法审核"; + } + + // 3. 驳回:饰品退回主号 + boxRecord.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); // 恢复为"在背包显示" + boxRecord.setUpdateTime(DateUtils.getNowDate()); + boxRecordsMapper.updateById(boxRecord); + + // 4. 同步更新转赠记录状态为失败 + BoxTransferRecord transferRecord = new LambdaQueryChainWrapper<>(boxTransferRecordAdminMapper) + .eq(BoxTransferRecord::getBoxRecordId, boxRecord.getId()) + .eq(BoxTransferRecord::getStatus, 0) + .one(); + if (transferRecord != null) { + transferRecord.setStatus(2); + transferRecord.setUpdateTime(LocalDateTime.now()); + boxTransferRecordAdminMapper.updateById(transferRecord); + } + + // 5. 更新转赠申请状态为已驳回,保留驳回原因 + apply.setStatus(2); + apply.setRejectReason(rejectReason); + apply.setUpdateBy(SecurityUtils.getUsername()); + apply.setUpdateTime(DateUtils.getNowDate()); + this.updateById(apply); + return ""; + } + + private LocalDate toLocalDate(Date date) { + if (date == null) { + return LocalDate.now(); + } + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUpgradeFailOrnamentsServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUpgradeFailOrnamentsServiceImpl.java new file mode 100644 index 0000000..fa10b0a --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUpgradeFailOrnamentsServiceImpl.java @@ -0,0 +1,113 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.controller.TtUpgradeFailOrnamentsController; +import com.ruoyi.admin.controller.TtUpgradeFailOrnamentsController.BatchAddParam; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.service.TtUpgradeOrnamentsService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.other.TtUpgradeFailOrnaments; +import com.ruoyi.admin.mapper.TtUpgradeFailOrnamentsMapper; +import com.ruoyi.admin.service.TtUpgradeFailOrnamentsService; +import com.ruoyi.domain.other.TtUpgradeOrnaments; +import com.ruoyi.domain.vo.TtUpgradeFailOrnamentsDataVO; +import com.ruoyi.common.utils.bean.BeanUtils; +import org.apache.ibatis.session.SqlSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class TtUpgradeFailOrnamentsServiceImpl extends ServiceImpl implements TtUpgradeFailOrnamentsService { + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private TtUpgradeOrnamentsService upgradeOrnamentsService; + + + @Override + public List queryList(TtUpgradeFailOrnamentsController.listParam param) { + return baseMapper.queryList(param); + } + + @Override + public AjaxResult batchAdd(BatchAddParam param) { + // 失败奖励每个升级饰品只允许配置一个:一次添加多个饰品直接拦截 + if (param.getOrnamentsIds() != null && param.getOrnamentsIds().size() > 1) { + return AjaxResult.error("失败奖励只允许配置一个饰品,请只选择一个后重试"); + } + Map tempList = param.getOrnamentsIds().stream().collect(Collectors.toMap(Function.identity(), Function.identity())); + + List list = new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .in(TtOrnament::getId, tempList.keySet().toArray()) + .list(); + + Map ornamentsInfo = param.getOrnamentsInfo(); + + //应用到所有 + if (param.getUseAllFlag()){ + //截断表 + baseMapper.truncateTable(); + + //查询所有幸运饰品 + List upgradeOrnamentList = upgradeOrnamentsService.list(Wrappers.lambdaQuery(TtUpgradeOrnaments.class) + .eq(TtUpgradeOrnaments::getStatus, 0)); + //每一个都要去添加 + for (TtUpgradeOrnaments upgradeOrnament : upgradeOrnamentList) { + upgradeOneBatch(upgradeOrnament.getId(), list, ornamentsInfo); + } + return AjaxResult.success(); + + } + + //单个应用 + return upgradeOneBatch(param.getUpgradeId(), list, ornamentsInfo); + } + + private AjaxResult upgradeOneBatch(Integer upgradeId, List list, Map ornamentsInfo) { + // 失败奖励唯一性校验:该升级饰品已配置过失败奖励时不允许再添加,避免一次失败发放多件 + Long existCount = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtUpgradeFailOrnaments::getUpgradeId, upgradeId) + .count(); + if (existCount != null && existCount > 0) { + return AjaxResult.error("该升级饰品已配置失败奖励,失败奖励只允许一个,请先删除后再添加"); + } + List collect = list.stream().map(ornament -> { + Integer levelId = ornamentsInfo != null ? ornamentsInfo.get(ornament.getId()) : null; + TtUpgradeFailOrnaments build = TtUpgradeFailOrnaments.builder() + .upgradeId(upgradeId) + .ornamentId(ornament.getId()) + .ornamentName(ornament.getName()) + .ornamentPrice(ornament.getUsePrice()) + .ornamentLevelId(levelId) + .createTime(new Date()) + .updateTime(new Date()) + .ornamentNumber(1) + .build(); + return build; + }).collect(Collectors.toList()); + + this.saveBatch(collect); + + return AjaxResult.success(collect); + } + + @Override + public String updateUpgradeFailOrnamentsById(TtUpgradeFailOrnamentsDataVO ttUpgradeFailOrnamentsDataVO) { + TtUpgradeFailOrnaments ttUpgradeFailOrnaments = TtUpgradeFailOrnaments.builder().build(); + BeanUtils.copyProperties(ttUpgradeFailOrnamentsDataVO, ttUpgradeFailOrnaments); + this.updateById(ttUpgradeFailOrnaments); + return ""; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUpgradeOrnamentsServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUpgradeOrnamentsServiceImpl.java new file mode 100644 index 0000000..3766bb0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUpgradeOrnamentsServiceImpl.java @@ -0,0 +1,254 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.TtUpgradeOrnamentsMapper; +import com.ruoyi.admin.service.TtUpgradeOrnamentsService; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.other.TtUpgradeOrnaments; +import com.ruoyi.domain.other.TtUpgradeOrnamentsBody; +import com.ruoyi.domain.vo.TtUpgradeOrnamentsDataVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class TtUpgradeOrnamentsServiceImpl extends ServiceImpl implements TtUpgradeOrnamentsService { + + @Value("${mkcsgo.upgrade.defaultRequired}") + private Integer defaultRequired; + + @Value("${mkcsgo.upgrade.anchorDefaultRequired}") + private Integer anchorDefaultRequired; + + private final TtOrnamentMapper ornamentsMapper; + + public TtUpgradeOrnamentsServiceImpl(TtOrnamentMapper ornamentsMapper) { + this.ornamentsMapper = ornamentsMapper; + } + + @Override + public List queryList(TtUpgradeOrnamentsBody ttUpgradeOrnamentsBody) { + return baseMapper.queryList(ttUpgradeOrnamentsBody); + } + + @Override + public String batchAdd(List ornamentsIds) { + + if (ornamentsIds == null || ornamentsIds.isEmpty()) { + return "请选择需要填货的饰品"; + } + + // 去重:同一批传入可能包含重复 id + List idList = ornamentsIds.stream().distinct().collect(java.util.stream.Collectors.toList()); + + // 默认区间 + int[] luckSection = {0, defaultRequired}; + int[] anchorLuckSection = {0, anchorDefaultRequired}; + String luckSectionStr = JSON.toJSONString(luckSection); + String anchorLuckSectionStr = JSON.toJSONString(anchorLuckSection); + + // 1. 查出已在 tt_upgrade_ornaments 中的饰品 id ,避免重复填货 + List existed = new LambdaQueryChainWrapper<>(baseMapper) + .select(TtUpgradeOrnaments::getOrnamentsId) + .in(TtUpgradeOrnaments::getOrnamentsId, idList) + .list(); + java.util.Set existedIdSet = new java.util.HashSet<>(); + for (TtUpgradeOrnaments u : existed) { + if (u.getOrnamentsId() != null) existedIdSet.add(u.getOrnamentsId()); + } + + // 2. 过滤出未填货的待处理 id + List toHandleIds = new ArrayList<>(); + for (Long id : idList) { + if (!existedIdSet.contains(id)) toHandleIds.add(id); + } + if (toHandleIds.isEmpty()) { + return "所选饰品均已填货,请勿重复操作"; + } + + // 3. 拉取饰品信息(is_putaway 为商城上下架,与幸运升级无关,故不过滤) + List list = new LambdaQueryChainWrapper<>(ornamentsMapper) + .in(TtOrnament::getId, toHandleIds) + .list(); + + int existedCnt = idList.size() - toHandleIds.size(); + int notFoundCnt = toHandleIds.size() - list.size(); + if (notFoundCnt > 0) { + log.warn("批量填货:跳过未找到的饰品 " + notFoundCnt + " 件"); + } + if (existedCnt > 0) { + log.warn("批量填货:跳过已填货饰品 " + existedCnt + " 件"); + } + + Date now = new Date(); + int savedCnt = 0; + for (TtOrnament ornament : list) { + // 统一使用 price 作为价格基准,避免 usePrice 为 null 时 NPE + BigDecimal price = ObjectUtil.isNotNull(ornament.getUsePrice()) ? ornament.getUsePrice() : ornament.getPrice(); + if (price == null) { + log.warn("批量填货:跳过价格为空的饰品 id=" + ornament.getId()); + continue; + } + + TtUpgradeOrnaments ttUpgradeOrnaments = TtUpgradeOrnaments.builder() + .ornamentsId(ornament.getId()) + .ornamentPrice(price) + + .luckSection(luckSectionStr) + .anchorLuckSection(anchorLuckSectionStr) + // 白号是独立奖池:默认先给与普通用户相同的区间,可在「饰品列表」页单独调 + .blankLuckSection(luckSectionStr) + + .amountRequired(calculateAmountRequired(price, luckSectionStr)) + .amountInvested(BigDecimal.ZERO) + .effectiveAmountInvested(BigDecimal.ZERO) + + .anchorAmountRequired(calculateAmountRequired(price, anchorLuckSectionStr)) + .anchorAmountInvested(BigDecimal.ZERO) + .anchorEffectiveAmountInvested(BigDecimal.ZERO) + + .blankAmountInvested(BigDecimal.ZERO) + .blankEffectiveAmountInvested(BigDecimal.ZERO) + .blankProbabilityInvested(0) + .createTime(now) + .updateTime(now) + .status("0") + .build(); + + boolean ok = this.save(ttUpgradeOrnaments); + if (ok) savedCnt++; + } + log.info("批量填货完成:实际新增 " + savedCnt + " 条,跳过已填货 " + existedCnt + " 条,跳过未找到 " + notFoundCnt + " 条"); + if (savedCnt == 0) { + return "所选饰品未能入库(请检查饰品是否存在或价格是否为空)"; + } + return ""; + } + + @Override + public String updateUpgradeOrnamentsById(TtUpgradeOrnamentsDataVO ttUpgradeOrnamentsDataVO) { + // 校验幸运区间:最小值不能大于最大值 + String errMsg = validateLuckSection(ttUpgradeOrnamentsDataVO.getLuckSection(), "普通用户幸运区间"); + if (StringUtils.isNotEmpty(errMsg)) return errMsg; + errMsg = validateLuckSection(ttUpgradeOrnamentsDataVO.getAnchorLuckSection(), "主播幸运区间"); + if (StringUtils.isNotEmpty(errMsg)) return errMsg; + errMsg = validateLuckSection(ttUpgradeOrnamentsDataVO.getBlankLuckSection(), "白号幸运区间"); + if (StringUtils.isNotEmpty(errMsg)) return errMsg; + + TtUpgradeOrnaments ttUpgradeOrnaments = this.getById(ttUpgradeOrnamentsDataVO.getId()); + if (ttUpgradeOrnaments == null) { + return "数据不存在"; + } + BeanUtils.copyBeanProp(ttUpgradeOrnaments, ttUpgradeOrnamentsDataVO); + + // 本轮已投金额与幸运值是游戏运行时累计值,只由幸运升级侧写入(含白号自己的池)。 + // 后台页面是把列表里的整行原样提交回来的,若照抄回写会把这一轮的最新进度覆盖成列表加载时的旧值, + // 故这里显式置空(MyBatis-Plus 默认忽略 null 字段),后台只改配置项。 + ttUpgradeOrnaments.setAmountInvested(null); + ttUpgradeOrnaments.setEffectiveAmountInvested(null); + ttUpgradeOrnaments.setAnchorAmountInvested(null); + ttUpgradeOrnaments.setAnchorEffectiveAmountInvested(null); + ttUpgradeOrnaments.setProbabilityInvested(null); + ttUpgradeOrnaments.setAnchorProbabilityInvested(null); + ttUpgradeOrnaments.setBlankProbabilityInvested(null); + ttUpgradeOrnaments.setBlankAmountInvested(null); + ttUpgradeOrnaments.setBlankEffectiveAmountInvested(null); + + // 根据最新价格和幸运区间同步重算本轮出货所需金额 + TtOrnament currentOrnament = new LambdaQueryChainWrapper<>(ornamentsMapper) + .eq(TtOrnament::getId, ttUpgradeOrnaments.getOrnamentsId()) + .one(); + BigDecimal price = resolveOrnamentPrice(currentOrnament, ttUpgradeOrnaments.getOrnamentPrice()); + if (price != null) { + ttUpgradeOrnaments.setAmountRequired(calculateAmountRequired(price, ttUpgradeOrnaments.getLuckSection())); + ttUpgradeOrnaments.setAnchorAmountRequired(calculateAmountRequired(price, ttUpgradeOrnaments.getAnchorLuckSection())); + } + + this.updateById(ttUpgradeOrnaments); + return ""; + } + + /** + * 计算本轮出货所需金额:price × 幸运区间最大值 × 0.01 + */ + private BigDecimal calculateAmountRequired(BigDecimal price, String luckSection) { + if (price == null || StringUtils.isEmpty(luckSection)) { + return BigDecimal.ZERO; + } + try { + List list = JSONUtil.toList(luckSection, Integer.class); + if (list == null || list.size() < 2) { + return BigDecimal.ZERO; + } + int max = list.get(1); + if (max <= 0) { + return BigDecimal.ZERO; + } + return price.multiply(new BigDecimal(max)).multiply(new BigDecimal("0.01")); + } catch (Exception e) { + return BigDecimal.ZERO; + } + } + + /** + * 取饰品最新价格:优先用 usePrice,为空则用 price,都取不到则 fallback 传入的默认价格 + */ + private BigDecimal resolveOrnamentPrice(TtOrnament ornament, BigDecimal fallbackPrice) { + if (ornament == null) { + return fallbackPrice; + } + if (ObjectUtil.isNotNull(ornament.getUsePrice())) { + return ornament.getUsePrice(); + } + if (ObjectUtil.isNotNull(ornament.getPrice())) { + return ornament.getPrice(); + } + return fallbackPrice; + } + + /** + * 校验幸运区间格式:必须为 [min, max] 且 min <= max,且 max > 0 + */ + private String validateLuckSection(String section, String label) { + if (StringUtils.isEmpty(section)) return ""; + try { + List list = JSONUtil.toList(section, Integer.class); + if (list == null || list.size() != 2) { + return label + "格式错误,应为 [最小值, 最大值]"; + } + int min = list.get(0); + int max = list.get(1); + if (min < 0 || max < 0) { + return label + "不能为负数"; + } + if (min > max) { + return label + "最小值不能大于最大值"; + } + if (max == 0) { + return label + "最大值不能为0"; + } + } catch (Exception e) { + return label + "格式解析错误"; + } + return ""; + } + + @Override + public Map getUpgradeProfitStatistics(Integer id) { + return baseMapper.getUpgradeProfitStatistics(id); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUpgradeRecordServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUpgradeRecordServiceImpl.java new file mode 100644 index 0000000..499d2b4 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUpgradeRecordServiceImpl.java @@ -0,0 +1,72 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.dto.upgrade.UpgradeCondition; +import com.ruoyi.domain.other.TtUpgradeRecord; +import com.ruoyi.admin.mapper.TtUpgradeRecordMapper; +import com.ruoyi.admin.service.TtUpgradeRecordService; +import com.ruoyi.domain.other.TtUpgradeRecordBody; +import com.ruoyi.domain.vo.upgrade.UpgradeRecordVO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TtUpgradeRecordServiceImpl extends ServiceImpl implements TtUpgradeRecordService { + + @Override + public List getUpgradeRecord(TtUpgradeRecordBody ttUpgradeRecordBody) { + return baseMapper.getUpgradeRecord(ttUpgradeRecordBody); + } + + @Override + public R> historyDetail(UpgradeCondition param) { + + Page pageInfo = new Page<>(param.getPage(), param.getSize()); + pageInfo.setOptimizeCountSql(false); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + wrapper.eq(ObjectUtil.isNotNull(param.getUpgradeRecordId()),TtUpgradeRecord::getId,param.getUpgradeRecordId()); + + wrapper.eq(ObjectUtil.isNotNull(param.getOrnamentId()),TtUpgradeRecord::getTargetOrnamentId,param.getOrnamentId()); + + wrapper.eq(ObjectUtil.isNotNull(param.getUserType()),TtUpgradeRecord::getUserType,param.getUserType()); + + wrapper.eq(ObjectUtil.isNotNull(param.getUserId()),TtUpgradeRecord::getUserId,param.getUserId()); + wrapper.orderByDesc(TtUpgradeRecord::getOpenTime); + + pageInfo = this.page(pageInfo, wrapper); + + return R.ok(pageInfo); + + } + + @Override + public R> adminGetLog(UpgradeCondition param) { + + param.setLimit((param.getPage()-1) * param.getSize()); + + List list = baseMapper.adminGetLog(param); + + Page pageInfo = new Page<>(param.getPage(), param.getSize()); + + if (list.isEmpty()){ + pageInfo.setTotal(0); + pageInfo.setRecords(list); + return R.ok(pageInfo); + } + + + pageInfo.setTotal(list.get(0).getTotal()); + pageInfo.setRecords(list); + + return R.ok(pageInfo); + + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserAmountRecordsServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserAmountRecordsServiceImpl.java new file mode 100644 index 0000000..e90ac88 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserAmountRecordsServiceImpl.java @@ -0,0 +1,239 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtUserAmountRecordsService; +import com.ruoyi.admin.service.TtUserBlendErcashService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.sys.MoneyType; +import com.ruoyi.domain.dto.userRecord.AmountRecordsDetailCondition; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.recorde.TtUserAmountRecords; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtUserAmountRecordsBody; +import com.ruoyi.domain.vo.TeamDetailVO; +import com.ruoyi.domain.vo.TtUserAccountRecordsRankVO; +import com.ruoyi.domain.vo.TtUserAmountRecords.PWelfareVO; +import com.ruoyi.domain.vo.TtUserAmountRecords.PersonBlendErcashVO; +import com.ruoyi.domain.vo.TtUserAmountRecords.UserAmountDetailVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +import static java.math.BigDecimal.ROUND_HALF_UP; + +@Slf4j +@Service +public class TtUserAmountRecordsServiceImpl extends ServiceImpl implements TtUserAmountRecordsService { + + + @Autowired + private TtUserCreditsRecordsMapper ttUserCreditsRecordsMapper; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private TtUserBlendErcashService ttUserBlendErcashService; + + @Autowired + private TtUserAmountRecordsMapper ttUserAmountRecordsMapper; + + @Autowired + private TtPromotionUpdateMapper ttPromotionUpdateMapper; + + @Autowired + private TtUserMapper userMapper; + + @Override + public R queryList(TtUserAmountRecordsBody param) { + + param.setLimit((param.getPage() - 1) * param.getSize()); + + Page pageInfo = new Page<>(param.getPage(), param.getSize()); + + List list = baseMapper.queryList(param); + Integer totalSize = baseMapper.totalSize(param); + + pageInfo.setRecords(list); + pageInfo.setTotal(totalSize); + + return R.ok(pageInfo); + } + + @Override + public Page rank(Timestamp begin, Timestamp end, Integer page, Integer size) { + + Page pageInfo = new Page<>(page, size); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String beginT = dateFormat.format(begin); + String endT = dateFormat.format(end); + + List rank = baseMapper.rank(beginT, endT, page - 1, size); + pageInfo.setRecords(rank); + pageInfo.setTotal(rank.size()); + return pageInfo; + + } + + // TODO 梳理充值网推广奖励明细 + @Override + public R pWelfareRecords(Integer uid, Integer page, Integer size) { + + // 测试时间段(今天00:00:00 - 现在)------------- + Calendar c = Calendar.getInstance(); + Timestamp end = new Timestamp(c.getTimeInMillis()); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + Timestamp begin = new Timestamp(c.getTimeInMillis()); + // 测试时间段------------------------------------ + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String beginT = dateFormat.format(begin); + String endT = dateFormat.format(end); + + if (StringUtils.isBlank(beginT) || StringUtils.isBlank(endT)) { + log.warn("推广福利统计失败!"); + return R.fail("时间 !!!"); + } + + Page pageInfo = new Page<>(page, size); + pageInfo.setOptimizeCountSql(false); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtUserBlendErcash::getUserId, uid) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.P_WELFARE.getCode()) + .orderByDesc(TtUserBlendErcash::getCreateTime); + + // 历史收益明细 + pageInfo = ttUserBlendErcashService.page(pageInfo, wrapper); + PWelfareVO pWelfareVO = PWelfareVO.builder() + .timeTotal(BigDecimal.ZERO) + .details(pageInfo) + .build(); + + // 今日预计收益 + BigDecimal pWelfareByTime = pWelfarePrizeToBoss(uid, beginT, endT); + pWelfareVO.setTodayPredict(pWelfareByTime); + + // BigDecimal pWelfareByTime = ttUserAmountRecordsMapper.pWelfareByTime(uid,beginT, endT); + // if (ObjectUtil.isEmpty(pWelfareByTime)) pWelfareByTime = BigDecimal.ZERO; + // pWelfareByTime = pWelfareByTime.abs().setScale(2,BigDecimal.ROUND_HALF_UP); + // pWelfareVO.setTodayPredict(pWelfareByTime); + + // 历史总收益 + BigDecimal pWHistoryTotal = baseMapper.pWHistoryTotal(uid); + pWelfareVO.setHistoryTotal(pWHistoryTotal); + + return R.ok(pWelfareVO); + } + + public BigDecimal pWelfarePrizeToBoss(Integer bossId,String beginTimeStr,String endTimeStr){ + + // 解析时间 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date beginTime = null; + try { + beginTime = dateFormat.parse(beginTimeStr); + } catch (ParseException e) { + log.warn("日期解析异常。"); + return BigDecimal.ZERO; + } + + // 查询所有下级id + List allEmployeesId = userMapper.allEmployeesByParents(Arrays.asList(bossId)); + + // 查询下级的最近一个绑定时间 + if (allEmployeesId.isEmpty()) return BigDecimal.ZERO; + List mit = ttPromotionUpdateMapper.latelyUpdate(allEmployeesId); + if (mit.size() < allEmployeesId.size()) log.warn("下级已绑定上级,但未写入更新日志,请及时检查!!!"); + // 如果最近绑定时间大于本次查询的起始时间,以最近绑定时间为准 + List empIds1 = new ArrayList<>(); + List empIds2 = new ArrayList<>(); + for (TeamDetailVO item : mit) { + Timestamp latelyTime = item.getBeginTime(); + if (latelyTime.compareTo(new Timestamp(beginTime.getTime())) < 0) { + empIds1.add(item.getEmployeeId()); + } else { + empIds2.add(item); + } + } + + List personBEList1 = new ArrayList<>(); + if (!empIds1.isEmpty()){ + // 当日没有 更换绑定的下级消费统计 + personBEList1 = ttUserBlendErcashMapper.personsTotalConsumeByTime( + empIds1, + beginTimeStr, + endTimeStr, + 0 + ); + } + + // 当日有 更换绑定的下级消费统计 + List personBEList2 = new ArrayList<>(); + if (!empIds2.isEmpty()){ + for (TeamDetailVO vo : empIds2){ + String beginT = dateFormat.format(vo.getBeginTime()); + PersonBlendErcashVO personBE = ttUserBlendErcashMapper.personTotalConsumeByTime( + vo.getEmployeeId(), + beginT, + endTimeStr, + 0 + ); + if (ObjectUtil.isEmpty(personBE)) continue; + personBEList2.add(personBE); + } + } + + personBEList1.addAll(personBEList2); + + // 发奖 + BigDecimal totalConsume = BigDecimal.ZERO; + for (PersonBlendErcashVO vo : personBEList1){ + totalConsume = totalConsume.add(vo.getTotal().abs()); + } + + TtUser ttUser = userMapper.selectTtUserById(bossId.longValue()); + if ("01".equals(ttUser.getUserType())) { + return totalConsume.multiply(new BigDecimal("0.045")).setScale(2,ROUND_HALF_UP); + } else { + return totalConsume.multiply(new BigDecimal("0.01")).setScale(2,ROUND_HALF_UP); + } + } + + @Override + public List userAccountDetail(AmountRecordsDetailCondition param) { + + param.setLimit((param.getPage() - 1) * param.getSize()); + + List res = ttUserBlendErcashMapper.userAccountDetail(param); + + if (param.getMoneyType().equals(MoneyType.GOLD.getCode())) { + + } else if (param.getMoneyType().equals(MoneyType.CREDITS.getCode())) { + + res.stream().forEach(item->{ + item.setAmount(item.getCredits()); + }); + + } + + return res; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserAvatarServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserAvatarServiceImpl.java new file mode 100644 index 0000000..7ec28c4 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserAvatarServiceImpl.java @@ -0,0 +1,20 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtUserAvatar; +import com.ruoyi.admin.mapper.TtUserAvatarMapper; +import com.ruoyi.admin.service.TtUserAvatarService; +import com.ruoyi.common.config.RuoYiConfig; +import org.springframework.stereotype.Service; + +@Service +public class TtUserAvatarServiceImpl extends ServiceImpl implements TtUserAvatarService { + + @Override + public String updateUserAvatarById(TtUserAvatar ttUserAvatar) { + String avatar = ttUserAvatar.getAvatar(); + ttUserAvatar.setAvatar(avatar); + this.updateById(ttUserAvatar); + return ""; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserBlendErcashServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserBlendErcashServiceImpl.java new file mode 100644 index 0000000..09395d4 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserBlendErcashServiceImpl.java @@ -0,0 +1,261 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtUserBlendErcashService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.dto.ChildDetailByBoss; +import com.ruoyi.domain.dto.userRecord.BlendErcashCondition; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.vo.ChildDetailByBossVO; +import com.ruoyi.domain.vo.TeamDetailVO; +import com.ruoyi.domain.vo.TtUserAmountRecords.TtUserBlendErcashVO; +import com.ruoyi.system.service.ISysConfigService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class TtUserBlendErcashServiceImpl extends ServiceImpl implements TtUserBlendErcashService { + + @Autowired + private ISysConfigService configService; + + @Autowired + private TtPromotionUpdateMapper promotionUpdateMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Override + public R byCondition(BlendErcashCondition condition) { + + condition.setLimit((condition.getPage() - 1) * condition.getSize()); + if (StringUtils.isBlank(condition.getUserName())) condition.setUserName(null); + + Page pageInfo = new Page<>(condition.getPage(), condition.getSize()); + List list = baseMapper.byCondition( + condition.getUserId(), + condition.getSource(), + condition.getType(), + condition.getUserName(), + condition.getMoneyType(), + condition.getPhoneNumber(), + condition.getLimit(), + condition.getSize() + ); + + Integer total = baseMapper.count( + condition.getUserId(), + condition.getSource(), + condition.getType(), + condition.getUserName(), + condition.getMoneyType(), + condition.getPhoneNumber(), + condition.getLimit(), + condition.getSize()); + pageInfo.setTotal(total); + pageInfo.setRecords(list); + + return R.ok(pageInfo); + } + + @Override + public R byConditionArchive(BlendErcashCondition condition) { + + condition.setLimit((condition.getPage() - 1) * condition.getSize()); + if (StringUtils.isBlank(condition.getUserName())) condition.setUserName(null); + + Page pageInfo = new Page<>(condition.getPage(), condition.getSize()); + List list = baseMapper.byConditionArchive( + condition.getUserId(), + condition.getSource(), + condition.getType(), + condition.getUserName(), + condition.getMoneyType(), + condition.getPhoneNumber(), + condition.getLimit(), + condition.getSize() + ); + + Integer total = baseMapper.countArchive( + condition.getUserId(), + condition.getSource(), + condition.getType(), + condition.getUserName(), + condition.getMoneyType(), + condition.getPhoneNumber(), + condition.getLimit(), + condition.getSize()); + pageInfo.setTotal(total); + pageInfo.setRecords(list); + + return R.ok(pageInfo); + } + + @Override + public Integer countArchive(BlendErcashCondition condition) { + if (StringUtils.isBlank(condition.getUserName())) condition.setUserName(null); + return baseMapper.countArchive( + condition.getUserId(), + condition.getSource(), + condition.getType(), + condition.getUserName(), + condition.getMoneyType(), + condition.getPhoneNumber(), + condition.getLimit(), + condition.getSize()); + } + + @Override + public R> childDetailByBoss(ChildDetailByBoss param) { + + // 解析开始时间 + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Timestamp beginTS = null; + if (!StringUtils.isBlank(param.getBeginTime())) { + Date parse; + try { + parse = dateFormat.parse(param.getBeginTime()); + if (!StringUtils.isBlank(param.getEndTime())) dateFormat.parse(param.getEndTime()); + } catch (ParseException e) { + return R.fail("解析时间异常。"); + } + beginTS = new Timestamp(parse.getTime()); + } + + // boss所有粉丝 + List cIds = userService.teamUIds(param.getBossId()); + if (cIds.isEmpty()) { + return R.ok(new Page()); + } + + // 查询最新的绑定更新记录 + List teamDetailVOS = promotionUpdateMapper.latelyUpdate(cIds.stream().map(Integer::valueOf).collect(Collectors.toList())); + if (cIds.size() > teamDetailVOS.size()) { + log.warn("用户{}的下级绑定记录不完整,请及时检查。", param.getBossId()); + } + + // 如果最新绑定记录时间大于查询起始时间,以最新绑定记录时间为准 + for (TeamDetailVO vo : teamDetailVOS) { + + if (ObjectUtil.isNotEmpty(beginTS) && vo.getBeginTime().compareTo(beginTS) <= 0) { + vo.setBeginTime(null); + } else { + String uid = String.valueOf(vo.getEmployeeId()); + cIds.remove(uid); + } + } + + // 消费途径 + Integer[] sources = new Integer[]{ + TtAccountRecordSource.GAME_TYPE_01.getCode(), + TtAccountRecordSource.GAME_TYPE_02.getCode(), + TtAccountRecordSource.GAME_TYPE_04.getCode() + }; + // 统计查询时间区间内每一个下级的消费 + List data = new ArrayList<>(); + if (!cIds.isEmpty()) { + data = baseMapper.collectByUIds( + cIds, + param.getBeginTime(), + param.getEndTime(), + TtAccountRecordType.OUTPUT.getCode(), + Arrays.asList(sources) + ); + } + + // 统计查询时间区间内有绑定更新的每一个下级的消费 + for (TeamDetailVO vo : teamDetailVOS) { + if (vo.getBeginTime() == null) { + continue; + } + List d = baseMapper.collectByUIds( + Arrays.asList(String.valueOf(vo.getEmployeeId())), + dateFormat.format(vo.getBeginTime()), + param.getEndTime(), + TtAccountRecordType.OUTPUT.getCode(), + Arrays.asList(sources) + ); + // if (d.isEmpty()) + // d.add(ChildDetailByBossVO.builder().nickName(vo.getEmployeeNickName()).total(BigDecimal.ZERO).build()); + data.addAll(d); + } + + // 补充没有消费记录的人员信息 + if (data.size() < teamDetailVOS.size()) { + for (TeamDetailVO tdVo : teamDetailVOS) { + boolean flag = false; + for (ChildDetailByBossVO vo : data) { + if (tdVo.getEmployeeId().equals(vo.getUserId())) { + flag = true; + break; + } + } + if (!flag) { + data.add(ChildDetailByBossVO.builder().nickName(tdVo.getEmployeeNickName()).total(BigDecimal.ZERO).build()); + } + } + } + + // 推广收益比例 + String pWelfarePrizeRatioStr = configService.selectConfigByKey("pWelfarePrize"); + if (StringUtils.isBlank(pWelfarePrizeRatioStr)) { + pWelfarePrizeRatioStr = "0.045"; + } + + // 补充收益信息 + for (ChildDetailByBossVO vo : data) { + vo.setEarnings(vo.getTotal().multiply(new BigDecimal(pWelfarePrizeRatioStr)) + .setScale(2, RoundingMode.HALF_UP).abs()); + } + + // 分页 + List collect = data.stream() + .sorted(new Comparator() { + @Override + public int compare(ChildDetailByBossVO o1, ChildDetailByBossVO o2) { + if (param.getOrderType().equals(1)) return o1.getTotal().compareTo(o2.getTotal()); + if (param.getOrderType().equals(2)) return o2.getTotal().compareTo(o1.getTotal()); + return o1.getTotal().compareTo(o2.getTotal()); + } + }) + .skip((long) (param.getPage() - 1) * param.getSize()) + .limit(param.getSize()) + .collect(Collectors.toList()); + + Page pageInfo = new Page<>(param.getPage(), param.getSize()); + pageInfo.setTotal(data.size()); + pageInfo.setRecords(collect); + + return R.ok(pageInfo); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserCreditsRecordsServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserCreditsRecordsServiceImpl.java new file mode 100644 index 0000000..78a86be --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserCreditsRecordsServiceImpl.java @@ -0,0 +1,88 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords; +import com.ruoyi.admin.mapper.TtUserCreditsRecordsMapper; +import com.ruoyi.admin.service.TtUserCreditsRecordsService; +import com.ruoyi.domain.other.TtUserCreditsRecordsBody; +import com.ruoyi.domain.vo.TtUserCreditsRecordsRankVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.List; + +@Service +public class TtUserCreditsRecordsServiceImpl extends ServiceImpl implements TtUserCreditsRecordsService { + + + @Autowired + private TtUserMapper userMapper; + + @Override + public List queryList(TtUserCreditsRecordsBody ttUserCreditsRecordsBody) { + return baseMapper.queryList(ttUserCreditsRecordsBody); + } + + /** + * + * @param page + * @param size + * @return + */ + @Override + public Page rank(Timestamp begin,Timestamp end,Integer page, Integer size) { + + Page pageInfo = new Page<>(page,size); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String beginT = dateFormat.format(begin); + String endT = dateFormat.format(end); + + List rank = baseMapper.rank(page - 1, size,beginT ,endT); + pageInfo.setRecords(rank); + pageInfo.setTotal(rank.size()); + return pageInfo; + } + + @Override + public Page pWelfareRecords(Integer uid, Integer type, Integer page, Integer size) { + + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + Timestamp today = new Timestamp(c.getTimeInMillis()); + + Page pageInfo = new Page<>(page, size); + pageInfo.setOptimizeCountSql(false); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtUserCreditsRecords::getUserId,uid) + .eq(TtUserCreditsRecords::getSource, TtAccountRecordSource.P_WELFARE.getCode()); + if (type.equals(1)){ + wrapper.ge(TtUserCreditsRecords::getCreateTime,today); + }else if (type.equals(2)){ + c.add(Calendar.DAY_OF_MONTH,-1); + Timestamp yesterday = new Timestamp(c.getTimeInMillis()); + wrapper + .ge(TtUserCreditsRecords::getCreateTime,yesterday) + .le(TtUserCreditsRecords::getCreateTime,today); + }else if (type.equals(3)){ + c.add(Calendar.DAY_OF_MONTH,-6); + Timestamp week = new Timestamp(c.getTimeInMillis()); + wrapper + .ge(TtUserCreditsRecords::getCreateTime,week); + } + + return page(pageInfo,wrapper); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserServiceImpl.java new file mode 100644 index 0000000..14048bb --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserServiceImpl.java @@ -0,0 +1,2398 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.RandomUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtDeliveryRecordMapper; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserAmountRecordsMapper; +import com.ruoyi.admin.mapper.TtUserAvatarMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserCreditsRecordsMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.model.UpdateUserAccountBo; +import com.ruoyi.admin.service.TtOrderService; +import com.ruoyi.admin.service.TtUserBlendErcashService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.service.TtVipLevelService; +import com.ruoyi.admin.util.PhoneNumberUtil; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig; +import com.ruoyi.framework.websocket.connector.SessionRegistry; +import com.ruoyi.admin.service.UserIdGenerator; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.common.utils.uuid.UUID; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.dto.sys.TeamUsersParam; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.recorde.TtUserAmountRecords; +import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords; +import com.ruoyi.domain.entity.sys.TtPromotionUpdate; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.*; +import com.ruoyi.domain.vo.TtUserPackSackDataVO; +import com.ruoyi.domain.vo.TtUserVipLevelVo; +import com.ruoyi.domain.vo.TtUserVipProgressVo; +import com.ruoyi.domain.vo.sys.SimpleTtUserVO; +import com.ruoyi.domain.vo.user.TtUseChildInfoVo; +import com.ruoyi.domain.vo.user.TtUserInfoVo; +import com.ruoyi.domain.vo.user.TtUserVo; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class TtUserServiceImpl extends ServiceImpl implements TtUserService { + + private final TtUserAmountRecordsMapper userAmountRecordsMapper; + private final TtUserCreditsRecordsMapper userCreditsRecordsMapper; + + public TtUserServiceImpl(TtUserAmountRecordsMapper userAmountRecordsMapper, + TtUserCreditsRecordsMapper userCreditsRecordsMapper) { + this.userAmountRecordsMapper = userAmountRecordsMapper; + this.userCreditsRecordsMapper = userCreditsRecordsMapper; + } + + @Autowired + private TtOrderService orderService; + + @Autowired + private SessionRegistry sessionRegistry; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtBoxRecordsMapper boxRecordsMapper; + + @Autowired + private TtPromotionUpdateMapper ttPromotionUpdateMapper; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private TtVipLevelService ttVipLevelService; + + @Autowired + @Lazy + private TtUserBlendErcashService ttUserBlendErcashService; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + Executor threadPoolTaskExecutor; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private TtUserAvatarMapper ttUserAvatarMapper; + + @Autowired + private UserIdGenerator userIdGenerator; + + @Autowired + private TtDeliveryRecordMapper ttDeliveryRecordMapper; + + @Autowired + private RedisCache redisCache; + + @Override + public Pair> queryList(TtUserBody ttUserBody) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StringUtils.isNotNull(ttUserBody.getUserId())) wrapper.eq(TtUser::getUserId, ttUserBody.getUserId()); + if (StringUtils.isNotNull(ttUserBody.getParentId())) wrapper.eq(TtUser::getParentId, ttUserBody.getParentId()); + if (StringUtils.isNotEmpty(ttUserBody.getUserName())) + wrapper.like(TtUser::getUserName, ttUserBody.getUserName()); + if (StringUtils.isNotNull(ttUserBody.getNickName())) + wrapper.like(TtUser::getNickName, ttUserBody.getNickName()); + if (StringUtils.isNotNull(ttUserBody.getUserType())) wrapper.eq(TtUser::getUserType, ttUserBody.getUserType()); + if (StringUtils.isNotEmpty(ttUserBody.getPhoneNumber())) + wrapper.likeRight(TtUser::getPhoneNumber, ttUserBody.getPhoneNumber()); + if (StringUtils.isNotEmpty(ttUserBody.getStatus())) wrapper.eq(TtUser::getStatus, ttUserBody.getStatus()); + if (StringUtils.isNotEmpty(ttUserBody.getInvitationCode())) + wrapper.eq(TtUser::getInvitationCode, ttUserBody.getInvitationCode()); + if (StringUtils.isNotNull(ttUserBody.getIsBlankAccount())) { + if (Integer.valueOf(1).equals(ttUserBody.getIsBlankAccount())) { + // 显式查询白号(白号管理页 isBlankAccount=1) + wrapper.eq(TtUser::getIsBlankAccount, 1); + } else { + // 显式排除白号(如添加白号弹窗 isBlankAccount=0,只列出可设为白号的普通用户) + wrapper.and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)); + } + } + // 未显式传 isBlankAccount 时不再排除白号:用户列表需能查到白号用户(含被设为白号的主播/招商等) + if (StringUtils.isNotEmpty(ttUserBody.getDelFlag())) + wrapper.eq(TtUser::getDelFlag, ttUserBody.getDelFlag()); + List users = this.list(wrapper); + if (CollectionUtils.isEmpty(users)) { + return Pair.of(0, new ArrayList<>()); + } + int total = (int) this.count(wrapper); + List vos = new ArrayList<>(); + List userIds = users.stream().filter(Objects::nonNull).map(TtUser::getUserId).collect(Collectors.toList()); + String startDate = StringUtils.isBlank(ttUserBody.getStartDate()) ? null : ttUserBody.getStartDate(); + String endDate = StringUtils.isBlank(ttUserBody.getEndDate()) ? null : ttUserBody.getEndDate(); + List records = ttRechargeRecordMapper.getTotalPriceByUserIds(userIds, startDate, endDate); + Map userIdToTotalPrice = records.stream().collect(Collectors.toMap(TtUidTotalRechargeRecord::getUserId, TtUidTotalRechargeRecord::getTotalPrice)); + List prices = boxRecordsMapper.boxUserIdPrice(userIds, startDate, endDate); + Map userIdToBoxPrice = prices.stream().collect(Collectors.toMap(TtBoxUserIdPrice::getUserId, TtBoxUserIdPrice::getTotalPrice)); + List deliveryRecords = ttDeliveryRecordMapper.getTotalDeliveryByUserIds(userIds); + Map userIdToTotalDelivery = deliveryRecords.stream().collect(Collectors.toMap(TtUidTotalRechargeRecord::getUserId, TtUidTotalRechargeRecord::getTotalPrice)); + for (TtUser user : users) { + TtUserVo vo = new TtUserVo(); + BeanUtils.copyProperties(user, vo); + // vip_level 存的是 tt_vip_level 主键 id(id=1→VIP0),列表/导出统一返回真实显示等级 + vo.setVipLevel(vo.getVipLevel() != null && vo.getVipLevel() > 0 ? vo.getVipLevel() - 1 : 0); + // 老数据默认启用赠送 + if (StringUtils.isBlank(vo.getGiftStatus())) { + vo.setGiftStatus("0"); + } + vo.setTotalPrice(userIdToTotalPrice.getOrDefault(user.getUserId(), BigDecimal.ZERO)); + vo.setTotalDelivery(userIdToTotalDelivery.getOrDefault(user.getUserId(), BigDecimal.ZERO)); + BigDecimal boxPrice = userIdToBoxPrice.getOrDefault(user.getUserId(), BigDecimal.ZERO); + BigDecimal totalLoss = boxPrice.subtract(vo.getTotalPrice()); + vo.setTotalLoss(totalLoss); + vos.add(vo); + } + return Pair.of(total, vos); + } + + public boolean checkParentRule(TtUser user) { + if (user == null) { + return false; + } + if (StringUtils.isEmpty(user.getUserType())) { + return true; + } + if (Objects.equals(user.getUserId(), user.getParentId())) { + return false; + } + + // 主播号必须有招商号上级 + if (UserType.ANCHOR.getCode().equals(user.getUserType())) { + if (user.getParentId() == null) { + return false; + } + TtUser parent = this.getById(user.getParentId()); + if (parent == null) { + return false; + } + return UserType.BUSINESS_USER.getCode().equals(parent.getUserType()); + } + if (UserType.COMMON_USER.getCode().equals(user.getUserType())) { + // 如果有上级,上级必须是主播号 + if (user.getParentId() != null) { + TtUser parent = this.getById(user.getParentId()); + if (parent == null) { + return false; + } + return UserType.ANCHOR.getCode().equals(parent.getUserType()); + } + } + // 招商号不能有上级 + if (UserType.BUSINESS_USER.getCode().equals(user.getUserType())) { + return user.getParentId() == null; + } + return true; + } + + /** + * 用户删除时的级联解绑:递归将该用户所有下级(含下级的下级)的 parent_id 置为 null。 + * 招商删除→主播解绑→主播下普通用户也解绑; + * 主播删除→普通用户解绑; + * 普通用户删除→无下级,无操作。 + */ + @Override + public void cascadeUnbindChildren(Integer userId) { + if (userId == null) return; + List children = this.list( + new LambdaQueryWrapper().eq(TtUser::getParentId, userId)); + if (children == null || children.isEmpty()) return; + // 本层所有子节点的 parent_id 置空 + userMapper.clearChildrenParentId(userId); + // 递归处理子节点的下级 + for (TtUser child : children) { + cascadeUnbindChildren(child.getUserId()); + } + } + + public boolean checkInvitationCode(TtUser user) { + if (user == null) { + return false; + } + if (StringUtils.isEmpty(user.getInvitationCode()) + && (Objects.equals(UserType.ANCHOR.getCode(), user.getUserType()) + || Objects.equals(UserType.BUSINESS_USER.getCode(), user.getUserType()))) { + return false; + } + if (StringUtils.isEmpty(user.getInvitationCode())) { + return true; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtUser::getInvitationCode, user.getInvitationCode()); + TtUser oldUser = userMapper.selectOne(wrapper); + return oldUser == null || Objects.equals(oldUser.getUserId(), user.getUserId()); + } + + public String userInfoCheck(TtUser ttUser) { + + String nickName = ttUser.getNickName(); + if (ObjectUtil.isNotEmpty(nickName)) { + if (nickName.length() < 3 || nickName.length() > 7) { + return "昵称长度限制3-7个字符。"; + } + } + return ""; + } + + @Override + @Transactional + public AjaxResult updateUserById(TtUser ttUser) { + TtUser oldUser = this.getById(ttUser.getUserId()); + + // 用户类型及白号身份是资金、返佣和游戏奖池口径的一部分,不允许通过编辑接口修改。 + if (oldUser != null && ttUser.getUserType() != null + && !Objects.equals(oldUser.getUserType(), ttUser.getUserType())) { + return AjaxResult.error("用户类型不可修改"); + } + if (oldUser != null && ttUser.getIsBlankAccount() != null + && !Objects.equals(oldUser.getIsBlankAccount(), ttUser.getIsBlankAccount())) { + return AjaxResult.error("白号身份不可修改"); + } + + // 仅当上级关系(上级ID或用户类型)发生变化时才校验上级规则, + // 避免历史数据不符合当前规则时,阻断账号状态、金额等其他字段的修改 + boolean parentRelationChanged = oldUser == null + || !Objects.equals(oldUser.getParentId(), ttUser.getParentId()) + || !Objects.equals(oldUser.getUserType(), ttUser.getUserType()); + if (parentRelationChanged && !checkParentRule(ttUser)) { + return AjaxResult.error("上级设置不符合要求"); + } + if (!checkInvitationCode(ttUser)) { + return AjaxResult.error("邀请码重复或者为空"); + } + + String check = userInfoCheck(ttUser); + if (!StringUtils.isBlank(check)) return AjaxResult.error(check); + + String password = ttUser.getPassword(); + // String avatar = ttUser.getAvatar(); + + // ttUser.setAvatar(RuoYiConfig.getDomainName() + avatar); + + if (StringUtils.isNotEmpty(password)) { + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + ttUser.setRemark("明文密码:" + password); + } else { + ttUser.setPassword(oldUser.getPassword()); + ttUser.setRemark(oldUser.getRemark()); + } + + // 安全密码(提货校验用):留空=不修改;填写则覆盖哈希与明文 + String safePassword = ttUser.getSafePasswordPlain(); + if (StringUtils.isNotEmpty(safePassword)) { + if (!NumberUtil.isNumber(safePassword) || safePassword.length() != 6) { + return AjaxResult.error("安全密码必须为6位数字"); + } + ttUser.setSafePassword(SecurityUtils.encryptPassword(safePassword)); + ttUser.setSafePasswordPlain(safePassword); + } else { + ttUser.setSafePassword(oldUser.getSafePassword()); + ttUser.setSafePasswordPlain(oldUser.getSafePasswordPlain()); + } + + // 白号身份一致性:user_type='06' ⟺ is_blank_account=1。 + // 游戏侧按 is_blank_account 判定白号(不是按 user_type 标签), + // 若此处不同步,仅改 user_type 标签会导致「改成白号却不生效」/「改回普通却仍被当白号」。 + if (ttUser.getUserType() != null) { + ttUser.setIsBlankAccount(UserType.BLANK_ACCOUNT.getCode().equals(ttUser.getUserType()) ? 1 : 0); + } + + this.updateById(ttUser); + + // 停用用户时,清理Redis Token并踢出WebSocket连接,防止已登录玩家继续操作 + if ("1".equals(ttUser.getStatus()) && !"1".equals(oldUser.getStatus())) { + kickOutLoginUsers(Collections.singletonList(ttUser.getUserId().longValue()), "您的账号已被停用"); + } + + if (!Objects.equals(oldUser.getParentId(), ttUser.getParentId())) { + if (ObjectUtil.isNotEmpty(oldUser.getParentId()) && ObjectUtil.isNotEmpty(ttUser.getParentId()) && oldUser.getParentId().equals(ttUser.getParentId())) { + return AjaxResult.success(); + } + // 上下级绑定关系表 + TtPromotionUpdate build = TtPromotionUpdate.builder() + .employeeId(ttUser.getUserId()) + .bossId(ttUser.getParentId()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttPromotionUpdateMapper.insert(build); + return AjaxResult.success(); + } + return AjaxResult.success(); + + } + + /** + * 逻辑删除用户后必须同步清理登录态: + * del_flag=2 后 getById 已查不到用户,但 Redis 里的 Token 仍然有效, + * 客户端轮询 /api/getAccountAmount 会一直空指针(Cannot invoke ... because "ttUser" is null)且永远无法登出。 + */ + @Override + public boolean removeByIds(Collection list) { + boolean ok = super.removeByIds(list); + if (ok && CollectionUtils.isNotEmpty(list)) { + List userIds = list.stream() + .filter(Objects::nonNull) + .filter(id -> id instanceof Number) + .map(id -> ((Number) id).longValue()) + .collect(Collectors.toList()); + kickOutLoginUsers(userIds, "您的账号已被删除"); + } + return ok; + } + + /** + * 清理指定客户端用户(user 恒为 null 的 api 端账号)的 Redis 登录态,并踢出 WebSocket 连接。 + * 后台/运营账号不处理,避免误踢。 + */ + @Override + public void kickOutLoginUsers(Collection userIds, String reason) { + if (CollectionUtils.isEmpty(userIds)) return; + Collection redisKeys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); + if (redisKeys == null || redisKeys.isEmpty()) return; + redisKeys.stream() + .map(redisCache::getCacheObject) + .filter(Objects::nonNull) + // api端用户 user 恒为 null(走 userData),以此区分,避免误踢后台/运营用户 + .filter(loginUser -> loginUser.getUser() == null) + .filter(loginUser -> userIds.contains(loginUser.getUserId())) + .forEach(loginUser -> { + sessionRegistry.kickOut(loginUser.getUserId(), reason); + redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + loginUser.getToken()); + }); + } + + @Override + public AjaxResult updateUserInvitationCodeById(TtUser ttUser) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(TtUser::getUserId, ttUser.getUserId()); + updateWrapper.set(TtUser::getInvitationCode, ttUser.getInvitationCode()); + updateWrapper.set(TtUser::getUpdateBy, ttUser.getUpdateBy()); + updateWrapper.set(TtUser::getUpdateTime, ttUser.getUpdateTime()); + update(updateWrapper); + return AjaxResult.success(); + } + + @Override + public void generateAccount(HttpServletResponse response, List accountList) { + String folderPath = RuoYiConfig.getDownloadPath() + "/"; + File folder = new File(folderPath); + if (!folder.exists()) { + folder.mkdirs(); + } + ServletOutputStream os = null; + BufferedWriter bw = null; + String uuid = IdUtils.fastSimpleUUID(); + try { + bw = new BufferedWriter(new FileWriter(folderPath + uuid + ".txt")); + for (int i = 0; i < accountList.size(); i++) { + bw.write("账号[" + (i + 1) + "]: " + accountList.get(i).getUserName() + " 密码: " + accountList.get(i).getRemark().substring(5)); + bw.newLine(); + bw.flush(); + } + File uploadFile = new File(folderPath + uuid + ".txt"); + os = response.getOutputStream(); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType("application/octet-stream"); + response.addHeader("Content-disposition", "attachment"); + os.write(FileUtil.readBytes(uploadFile)); + IoUtil.flush(os); + } catch (IOException ignored) { + } finally { + if (os != null) IoUtil.close(os); + if (bw != null) IoUtil.close(bw); + } + } + + @Override + public List getAccountList(Integer num) { + return getAccountList(num, null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List getAccountList(Integer num, List customPhoneNumbers) { + if (num == null || num <= 0 || num > 10) return null; + // 先对自定义手机号做唯一性与重复性校验,避免部分账号已创建后才失败 + validateCustomPhoneNumbers(customPhoneNumbers); + // 从后台「头像设置」库中 status='0'(启用)的头像随机选一张 + List avatarList = new LambdaQueryChainWrapper<>(ttUserAvatarMapper) + .eq(TtUserAvatar::getStatus, "0") + .list(); + List userList = new ArrayList<>(); + for (int i = 0; i < num; i++) { + // 优先使用第 i 个自定义手机号,为空则随机生成 + String phoneNumber = getCustomPhoneOrRandom(customPhoneNumbers, i); + String nickName = RandomUtils.getRandomName(new Random().nextInt(2)); + String password = generateMixedPassword(10); + String invitationCode = getInvitationCode(); + TtUser ttUser = TtUser.builder().build(); + ttUser.setUserName(phoneNumber); + ttUser.setNickName(nickName); + ttUser.setUserType(UserType.BUSINESS_USER.getCode()); + ttUser.setPhoneNumber(phoneNumber); + // 从启用的头像库随机选一张;库为空时兜底为空字符串 + String avatar = avatarList.isEmpty() ? "" : RandomUtil.randomEle(avatarList).getAvatar(); + ttUser.setAvatar(avatar); + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + ttUser.setInvitationCode(invitationCode.toLowerCase()); + ttUser.setIsRealCheck("1"); + ttUser.setRemark("明文密码:" + password); + ttUser.setCreateBy(SecurityUtils.getUsername()); + ttUser.setCreateTime(DateUtils.getNowDate()); + ttUser.setAccountAmount(BigDecimal.ZERO); + ttUser.setAccountCredits(BigDecimal.ZERO); + // 招商账号默认返点率:邀请新人充值0、新游戏消费0.5、经典玩法0.5 + ttUser.setCommissionRate(BigDecimal.valueOf(0)); + ttUser.setNewGameCommissionRate(new BigDecimal("0.5")); + ttUser.setClassicGameCommissionRate(new BigDecimal("0.5")); + ttUser.setUserId(userIdGenerator.generateUserId()); + this.save(ttUser); + userList.add(ttUser); + } + return userList; + } + + /** + * 取第 index 个自定义手机号;为空或越界时返回随机生成的手机号 + */ + private String getCustomPhoneOrRandom(List customPhoneNumbers, int index) { + if (customPhoneNumbers != null && index < customPhoneNumbers.size() + && StringUtils.isNotBlank(customPhoneNumbers.get(index))) { + return customPhoneNumbers.get(index).trim(); + } + return getRandomPhoneNumber(); + } + + /** + * 校验自定义手机号:批次内不可重复,且不可与库中已存在(用户名/手机号)冲突。 + * 忽略 del_flag,连软删除记录也拦截,避免直接 insert 触发唯一键冲突。 + */ + private void validateCustomPhoneNumbers(List customPhoneNumbers) { + if (customPhoneNumbers == null) return; + Set seen = new HashSet<>(); + for (String phone : customPhoneNumbers) { + if (StringUtils.isBlank(phone)) continue; + String p = phone.trim(); + if (!seen.add(p)) { + throw new ServiceException("自定义手机号存在重复:" + p); + } + long existCount = new LambdaQueryChainWrapper<>(baseMapper) + .and(w -> w.eq(TtUser::getUserName, p).or().eq(TtUser::getPhoneNumber, p)) + .count(); + if (existCount > 0) { + throw new ServiceException("手机号'" + p + "'已被注册!"); + } + } + } + + /** + * 生成英文字母 + 数字混合的指定长度密码 + */ + private String generateMixedPassword(int length) { + if (length <= 0) return ""; + String upperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + String lowerCase = "abcdefghijklmnopqrstuvwxyz"; + String digits = "0123456789"; + String allChars = upperCase + lowerCase + digits; + + StringBuilder sb = new StringBuilder(); + Random random = new Random(); + + // 确保至少包含一个字母和至少一个数字 + int lettersNeeded = random.nextInt(length - 1) + 1; // 1~length-1 个字母 + + for (int i = 0; i < lettersNeeded; i++) { + sb.append(allChars.charAt(random.nextInt(52))); // 前 52 位是字母 + } + + for (int i = lettersNeeded; i < length; i++) { + sb.append(digits.charAt(random.nextInt(10))); // 后 10 位是数字 + } + + // 打乱顺序 + char[] chars = sb.toString().toCharArray(); + for (int i = chars.length - 1; i > 0; i--) { + int j = random.nextInt(i + 1); + char temp = chars[i]; + chars[i] = chars[j]; + chars[j] = temp; + } + + return new String(chars); + } + + @Override + public TtUser getAccountList(String phoneNumber, String password) { + if (StringUtils.isBlank(phoneNumber) || StringUtils.isBlank(password)) return null; + + // 唯一索引 unique_user_name 仅作用于 user_name(不含 del_flag), + // 因此查重必须忽略 del_flag,连软删除记录也要拦截,否则直接 insert 会触发数据库唯一键冲突, + // 原始 SQLIntegrityConstraintViolationException 会直接暴露给前端。 + long existCount = new LambdaQueryChainWrapper<>(baseMapper) + .and(w -> w.eq(TtUser::getUserName, phoneNumber).or().eq(TtUser::getPhoneNumber, phoneNumber)) + .count(); + if (existCount > 0) { + throw new ServiceException("手机号'" + phoneNumber + "'已被注册!"); + } + + String nickName = RandomUtils.getRandomName(new Random().nextInt(2)); + + String invitationCode = getInvitationCode(); + TtUser ttUser = TtUser.builder().build(); + ttUser.setUserName(phoneNumber); + ttUser.setNickName(nickName); + ttUser.setUserType("01"); + ttUser.setPhoneNumber(phoneNumber); + ttUser.setAvatar(""); + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + ttUser.setInvitationCode(invitationCode.toLowerCase()); + ttUser.setIsRealCheck("1"); + ttUser.setRemark("明文密码:" + password); + ttUser.setCreateBy("admin"); + ttUser.setCreateTime(DateUtils.getNowDate()); + ttUser.setAccountAmount(BigDecimal.ZERO); + ttUser.setAccountCredits(BigDecimal.ZERO); + // 主播(01)默认返点率:邀请新人充值2、新游戏消费1、经典玩法4(缺失会导致实时佣金计算为0) + ttUser.setCommissionRate(new BigDecimal("2")); + ttUser.setNewGameCommissionRate(new BigDecimal("1")); + ttUser.setClassicGameCommissionRate(new BigDecimal("4")); + ttUser.setUserId(userIdGenerator.generateUserId()); + this.save(ttUser); + return ttUser; + } + + @Override + public List getRobotList(Integer num) { + if (num <= 0 || num > 10) return null; + // 机器人头像从后台「头像设置」库中 status='0' (启用)的头像随机选一张 + List avatarList = new LambdaQueryChainWrapper<>(ttUserAvatarMapper) + .eq(TtUserAvatar::getStatus, "0") + .list(); + List userList = new ArrayList<>(); + for (int i = 0; i < num; i++) { + String phoneNumber = getRandomPhoneNumber(); + String nickName = RandomUtils.getRandomName(new Random().nextInt(2)); + String password = generateMixedPassword(10); + String invitationCode = getInvitationCode(); + TtUser ttUser = TtUser.builder().build(); + ttUser.setUserName(phoneNumber); + ttUser.setNickName(nickName); + ttUser.setUserType("03"); + // 机器人VIP等级在创建时固定生成一个 1-10 之间的随机值并入库, + // 后续大厅/房间/观战/所有提示统一从 tt_user.vip_level 读取,保证显示一致 + ttUser.setVipLevel(RandomUtil.randomInt(1, 11)); + ttUser.setPhoneNumber(phoneNumber); + // 从启用的头像库随机选一张;库为空时兑底为空字符串 + String avatar = avatarList.isEmpty() ? "" : RandomUtil.randomEle(avatarList).getAvatar(); + ttUser.setAvatar(avatar); + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + ttUser.setAccountAmount(new BigDecimal(99999999)); + ttUser.setAccountCredits(BigDecimal.ZERO); + ttUser.setInvitationCode(invitationCode.toLowerCase()); + ttUser.setIsRealCheck("1"); + ttUser.setRemark("明文密码:" + password); + ttUser.setCreateBy(SecurityUtils.getUsername()); + ttUser.setCreateTime(DateUtils.getNowDate()); + ttUser.setUserId(userIdGenerator.generateUserId()); + this.save(ttUser); + userList.add(ttUser); + } + return userList; + } + + @Override + public List getBlankAccountList(Integer num) { + return getBlankAccountList(num, null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List getBlankAccountList(Integer num, List customPhoneNumbers) { + if (num == null || num <= 0 || num > 10) return null; + // 先对自定义手机号做唯一性与重复性校验,避免部分账号已创建后才失败 + validateCustomPhoneNumbers(customPhoneNumbers); + // 白号头像优先使用默认头像;没有默认头像则从启用头像库随机选一张 + List defaultAvatarList = new LambdaQueryChainWrapper<>(ttUserAvatarMapper) + .eq(TtUserAvatar::getIsDefault, "1") + .list(); + List avatarList = defaultAvatarList.isEmpty() + ? new LambdaQueryChainWrapper<>(ttUserAvatarMapper) + .eq(TtUserAvatar::getStatus, "0") + .list() + : defaultAvatarList; + List userList = new ArrayList<>(); + for (int i = 0; i < num; i++) { + // 优先使用第 i 个自定义手机号,为空则随机生成 + String phoneNumber = getCustomPhoneOrRandom(customPhoneNumbers, i); + String nickName = RandomUtils.getRandomName(new Random().nextInt(2)); + String password = generateMixedPassword(10); + String invitationCode = getInvitationCode(); + TtUser ttUser = TtUser.builder().build(); + ttUser.setUserName(phoneNumber); + ttUser.setNickName(nickName); + ttUser.setUserType(UserType.BLANK_ACCOUNT.getCode()); + ttUser.setIsBlankAccount(1); + ttUser.setVipLevel(0); + ttUser.setPhoneNumber(phoneNumber); + String avatar = avatarList.isEmpty() ? "" : RandomUtil.randomEle(avatarList).getAvatar(); + ttUser.setAvatar(avatar); + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + ttUser.setAccountAmount(BigDecimal.ZERO); + ttUser.setAccountCredits(BigDecimal.ZERO); + ttUser.setTotalRechargeAmount(BigDecimal.ZERO); + ttUser.setInvitationCode(invitationCode.toLowerCase()); + ttUser.setIsRealCheck("0"); + ttUser.setRemark("明文密码:" + password); + ttUser.setCreateBy(SecurityUtils.getUsername()); + ttUser.setCreateTime(DateUtils.getNowDate()); + ttUser.setUserId(userIdGenerator.generateUserId()); + this.save(ttUser); + userList.add(ttUser); + } + return userList; + } + + @Override + public List getCardMerchantList(Integer num) { + return getCardMerchantList(num, null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List getCardMerchantList(Integer num, List customPhoneNumbers) { + if (num == null || num <= 0 || num > 10) return null; + // 先对自定义手机号做唯一性与重复性校验,避免部分账号已创建后才失败 + validateCustomPhoneNumbers(customPhoneNumbers); + // 从后台「头像设置」库中 status='0'(启用)的头像随机选一张 + List avatarList = new LambdaQueryChainWrapper<>(ttUserAvatarMapper) + .eq(TtUserAvatar::getStatus, "0") + .list(); + List userList = new ArrayList<>(); + for (int i = 0; i < num; i++) { + // 优先使用第 i 个自定义手机号,为空则随机生成 + String phoneNumber = getCustomPhoneOrRandom(customPhoneNumbers, i); + String nickName = RandomUtils.getRandomName(new Random().nextInt(2)); + String password = generateMixedPassword(10); + String invitationCode = getInvitationCode(); + TtUser ttUser = TtUser.builder().build(); + ttUser.setUserName(phoneNumber); + ttUser.setNickName(nickName); + ttUser.setUserType(UserType.CARD_MERCHANT.getCode()); + ttUser.setPhoneNumber(phoneNumber); + // 从启用的头像库随机选一张;库为空时兜底为空字符串 + String avatar = avatarList.isEmpty() ? "" : RandomUtil.randomEle(avatarList).getAvatar(); + ttUser.setAvatar(avatar); + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + ttUser.setInvitationCode(invitationCode.toLowerCase()); + ttUser.setIsRealCheck("1"); + ttUser.setDelFlag("0"); + ttUser.setStatus("0"); + ttUser.setRemark("明文密码:" + password); + ttUser.setCreateBy(SecurityUtils.getUsername()); + ttUser.setCreateTime(DateUtils.getNowDate()); + ttUser.setAccountAmount(BigDecimal.ZERO); + ttUser.setAccountCredits(BigDecimal.ZERO); + ttUser.setMerchantBalance(BigDecimal.ZERO); + ttUser.setUserId(userIdGenerator.generateUserId()); + this.save(ttUser); + userList.add(ttUser); + } + return userList; + } + + @Override + public boolean checkUserNameUnique(TtUser user) { + int userId = StringUtils.isNull(user.getUserId()) ? -1 : user.getUserId(); + TtUser info = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtUser::getUserName, user.getUserName()) + .eq(TtUser::getDelFlag, "0") + .one(); + if (StringUtils.isNotNull(info) && info.getUserId() != userId) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + @Override + public boolean checkPhoneUnique(TtUser user) { + int userId = StringUtils.isNull(user.getUserId()) ? -1 : user.getUserId(); + TtUser info = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtUser::getPhoneNumber, user.getPhoneNumber()) + .eq(TtUser::getDelFlag, "0") + .one(); + if (StringUtils.isNotNull(info) && info.getUserId() != userId) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + @Override + public boolean checkIdNumUnique(TtUser user) { + int userId = StringUtils.isNull(user.getUserId()) ? -1 : user.getUserId(); + // 查询使用该身份证号已实名认证且未删除的用户数量(排除当前用户自身) + long count = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtUser::getIdNum, user.getIdNum()) + .eq(TtUser::getIsRealCheck, "1") + .eq(TtUser::getDelFlag, "0") + .ne(TtUser::getUserId, userId) + .count(); + // 同一身份证号最多允许3个账号绑定 + if (count >= 3) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + @Override + public boolean checkNickNameUnique(TtUser user) { + int userId = StringUtils.isNull(user.getUserId()) ? -1 : user.getUserId(); + long count = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtUser::getNickName, user.getNickName()) + .eq(TtUser::getDelFlag, "0") + .ne(TtUser::getUserId, userId) + .count(); + if (count > 0) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + @Override + public String getInvitationCode() { + while (true) { + try { + String randomInvitationCode = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(); + TtUser ttUser = new LambdaQueryChainWrapper<>(baseMapper) + .eq(TtUser::getInvitationCode, randomInvitationCode) + .eq(TtUser::getDelFlag, "0") + .one(); + if (StringUtils.isNull(ttUser)) { + return randomInvitationCode; + } + } catch (Exception e) { + return null; + } + } + } + + @Override + public TtUser selectTtUserById(Long id) { + return baseMapper.selectTtUserById(id); + } + + @Override + public ApiUserOnline loginUserToUserOnline(LoginUser user) { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserData())) { + return null; + } + ApiUserOnline userOnline = new ApiUserOnline(); + BeanUtils.copyBeanProp(userOnline, user); + userOnline.setNickName(user.getUserData().getNickName()); + userOnline.setUserName(user.getUsername()); + return userOnline; + } + + @Override + public ApiUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user) { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) { + return loginUserToUserOnline(user); + } + return null; + } + + @Override + public ApiUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user) { + if (StringUtils.equals(ipaddr, user.getIpaddr())) { + return loginUserToUserOnline(user); + } + return null; + } + + @Override + public ApiUserOnline selectOnlineByUserName(String userName, LoginUser user) { + if (StringUtils.equals(userName, user.getUsername())) { + return loginUserToUserOnline(user); + } + return null; + } + + @Override + public void insertUserAmountRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal amount, BigDecimal finalAmount) { + insertUserAmountRecords(userId, type, source, amount, finalAmount, null, null, null); + } + + @Override + public void insertUserAmountRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal amount, BigDecimal finalAmount, + Integer pwChildId, String pwChildName, BigDecimal childAccount) { + + TtUserAmountRecords userAmountRecords = TtUserAmountRecords.builder().build(); + userAmountRecords.setUserId(userId); + userAmountRecords.setType(type.getCode()); + userAmountRecords.setSource(source.getCode()); + userAmountRecords.setAmount(amount); + userAmountRecords.setFinalAmount(finalAmount); + userAmountRecords.setCreateTime(DateUtils.getNowDate()); + userAmountRecords.setRemark(source.getMsg()); + if (source.equals(TtAccountRecordSource.P_WELFARE)) { + userAmountRecords.setPwChildId(pwChildId); + userAmountRecords.setPwChildName(pwChildName); + userAmountRecords.setPwChildAccount(childAccount); + } + // switch (source) { + // case "0": + // userAmountRecords.setRemark("充值"); + // break; + // case "1": + // userAmountRecords.setRemark("开箱消费"); + // break; + // case "2": + // userAmountRecords.setRemark("创建对战消费"); + // break; + // case "3": + // userAmountRecords.setRemark("加入对战消费"); + // break; + // case "4": + // userAmountRecords.setRemark("对战结果收入"); + // break; + // case "5": + // userAmountRecords.setRemark("分解收入"); + // break; + // case "6": + // userAmountRecords.setRemark("注册红包收入"); + // break; + // case "7": + // userAmountRecords.setRemark("口令红包收入"); + // break; + // case "8": + // userAmountRecords.setRemark("商城积分转换收入"); + // break; + // case "9": + // userAmountRecords.setRemark("VIP等级返佣收入"); + // break; + // case "10": + // userAmountRecords.setRemark("后台操作余额变动"); + // break; + // case "11": + // userAmountRecords.setRemark("幸运升级消费"); + // break; + // case "12": + // userAmountRecords.setRemark("商城购物消费"); + // break; + // case "13": + // userAmountRecords.setRemark("推广福利收入"); + // userAmountRecords.setPwChildId(pwChildId); + // userAmountRecords.setPwChildName(pwChildName); + // userAmountRecords.setPwChildAccount(account); + // break; + // } + userAmountRecordsMapper.insert(userAmountRecords); + } + + @Override + public void insertUserAmountRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal amount, BigDecimal finalAmount, Integer taskId) { + + TtUserAmountRecords userAmountRecords = TtUserAmountRecords.builder().build(); + userAmountRecords.setUserId(userId); + userAmountRecords.setType(type.getCode()); + userAmountRecords.setSource(source.getCode()); + userAmountRecords.setAmount(amount); + userAmountRecords.setFinalAmount(finalAmount); + userAmountRecords.setCreateTime(DateUtils.getNowDate()); + userAmountRecords.setRemark(source.getMsg()); + if (source.equals(TtAccountRecordSource.TASK)) { + userAmountRecords.setTaskId(taskId); + } + userAmountRecordsMapper.insert(userAmountRecords); + } + + @Override + public void insertUserCreditsRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal credits, BigDecimal finalCredits) { + insertUserCreditsRecords(userId, type, source, credits, finalCredits, null, null, null); + } + + /** + * @param userId + * @param type + * @param source + * @param credits 变动金额 + * @param finalCredits 账户最终金额 + * @param pwChildId 推广福利 上级id + * @param pwChildName + * @param childAccount 推广福利 上级流水 + */ + @Override + public void insertUserCreditsRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal credits, BigDecimal finalCredits, + Integer pwChildId, String pwChildName, BigDecimal childAccount) { + TtUserCreditsRecords userCreditsRecords = TtUserCreditsRecords.builder().build(); + userCreditsRecords.setUserId(userId); + userCreditsRecords.setType(type.getCode()); + userCreditsRecords.setSource(source.getCode()); + userCreditsRecords.setCredits(credits); + userCreditsRecords.setFinalCredits(finalCredits); + userCreditsRecords.setCreateTime(DateUtils.getNowDate()); + userCreditsRecords.setRemark(source.getMsg()); + if (source.equals(TtAccountRecordSource.P_WELFARE)) { + userCreditsRecords.setPwChildId(pwChildId); + userCreditsRecords.setPwChildName(pwChildName); + userCreditsRecords.setPwChildAccount(childAccount); + } + // switch (source) { + // case "0": + // userCreditsRecords.setRemark("积分商城兑换消费"); + // break; + // case "1": + // userCreditsRecords.setRemark("开箱收入"); + // break; + // case "2": + // userCreditsRecords.setRemark("创建对战收入"); + // break; + // case "3": + // userCreditsRecords.setRemark("加入对战收入"); + // break; + // case "4": + // userCreditsRecords.setRemark("商城积分转换消费"); + // break; + // case "5": + // userCreditsRecords.setRemark("幸运升级收入"); + // break; + // case "10": + // userCreditsRecords.setRemark("后台操作积分变动"); + // break; + // case "11": + // // String remark = ""; + // // if (remarks.length>0){ + // // for (int i = 0;i < remarks.length;i++){ + // // remark = remark + remarks[i]; + // // } + // // } + // userCreditsRecords.setRemark("推广福利积分收入"); + // userCreditsRecords.setPwChildId(pwChildId); + // userCreditsRecords.setPwChildName(pwChildName); + // userCreditsRecords.setPwChildAccount(account); + // break; + // case "task_1": + // userCreditsRecords.setRemark("{首次下载奖励}任务奖励"); + // break; + // case "task_2": + // userCreditsRecords.setRemark("{每日流水奖励}任务奖励"); + // break; + // } + userCreditsRecordsMapper.insert(userCreditsRecords); + } + + @Override + public void insertUserCreditsRecords(Integer userId, TtAccountRecordType type, TtAccountRecordSource source, BigDecimal credits, BigDecimal finalCredits, Integer taskId) { + + TtUserCreditsRecords userCreditsRecords = TtUserCreditsRecords.builder().build(); + userCreditsRecords.setUserId(userId); + userCreditsRecords.setType(type.getCode()); + userCreditsRecords.setSource(source.getCode()); + userCreditsRecords.setCredits(credits); + userCreditsRecords.setFinalCredits(finalCredits); + userCreditsRecords.setCreateTime(DateUtils.getNowDate()); + userCreditsRecords.setRemark(source.getMsg()); + if (source.equals(TtAccountRecordSource.TASK)) { + userCreditsRecords.setTaskId(taskId); + } + + userCreditsRecordsMapper.insert(userCreditsRecords); + + } + + @Override + public List getPackSack(TtUserPackSackBody ttUserPackSackBody) { + return baseMapper.getPackSack(ttUserPackSackBody); + } + + @Override + public Map getUserProfitStatistics(Integer userId) { + return baseMapper.getUserProfitStatistics(userId); + } + + @Override + public List propRankOfDay(Timestamp beginTime, Timestamp endTime, Integer number) { + + // String[] sources = new String[]{"0","1","2","6"}; + Integer[] sources = new Integer[]{ + TtboxRecordSource.BLIND_BOX.getCode(), + TtboxRecordSource.FIGHT.getCode(), + // TtboxRecordSource.ROLL.getCode(), + TtboxRecordSource.UPGRADE.getCode() + }; + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String beginT = dateFormat.format(beginTime); + String endT = dateFormat.format(endTime); + + // System.out.println("出货排行榜时间区间:"+beginT+","+endT); + + List rank = boxRecordsMapper.propRankOfDay(beginT, endT, sources, number); + + return rank; + } + + @Override + public R> teamUsers(TeamUsersParam param) { + + Page pageInfo = new Page<>(param.getPage(), param.getSize()); + pageInfo.setOptimizeCountSql(false); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .like(ObjectUtil.isNotEmpty(param.getUserName()), TtUser::getNickName, param.getUserName()) + .or() + .like(ObjectUtil.isNotEmpty(param.getUserName()), TtUser::getUserName, param.getUserName()) + .in(ObjectUtil.isNotEmpty(param.getEmployeeIds()), TtUser::getParentId, param.getBossIds()) + .in(ObjectUtil.isNotEmpty(param.getEmployeeIds()), TtUser::getUserId, param.getEmployeeIds()); + + Page page = page(pageInfo, wrapper); + + List voList = page.getRecords().stream().map(item -> { + SimpleTtUserVO vo = new SimpleTtUserVO(); + BeanUtil.copyProperties(item, vo); + return vo; + }).collect(Collectors.toList()); + + Page p = new Page<>(); + BeanUtil.copyProperties(pageInfo, p, "records"); + p.setRecords(voList); + return R.ok(p); + } + + @Override + public List teamUIds(Integer bossId) { + return baseMapper.teamUIds(bossId); + } + + @Override + public void updateUserVIPLevel(Integer userId) { + // 查询用户累计总充值量(主表 + 归档表 UNION ALL,确保归档后数据不丢) + BigDecimal totalRechargeAmount = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource( + Collections.singletonList(userId), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + if (totalRechargeAmount == null) { + totalRechargeAmount = BigDecimal.ZERO; + } + + // 查询用户 + TtUser ttUser = getById(userId); + if (ttUser == null) { + return; + } + + // 按 id 升序遍历 VIP 等级,找到当前充值金额能达到的最高等级 + List ttVipLevels = ttVipLevelService.list( + new LambdaQueryWrapper().orderByAsc(TtVipLevel::getId)); + if (CollectionUtils.isEmpty(ttVipLevels)) { + return; + } + + Integer currentLevel = ttUser.getVipLevel() != null ? ttUser.getVipLevel() : 0; + Integer targetLevel = currentLevel; + for (TtVipLevel ttVipLevel : ttVipLevels) { + if (ttVipLevel.getRechargeThreshold() != null + && totalRechargeAmount.compareTo(ttVipLevel.getRechargeThreshold()) >= 0) { + targetLevel = ttVipLevel.getId(); + } else { + break; + } + } + + // 同步更新 VIP 等级和累计充值缓存字段 + boolean levelChanged = !targetLevel.equals(currentLevel); + boolean amountChanged = ttUser.getTotalRechargeAmount() == null + || totalRechargeAmount.compareTo(ttUser.getTotalRechargeAmount()) != 0; + if (levelChanged || amountChanged) { + ttUser.setVipLevel(targetLevel); + ttUser.setTotalRechargeAmount(totalRechargeAmount); + this.updateById(ttUser); + } + + // 注:转赠次数按提交时的 VIP 等级隔离记录,升级后自动获得新等级的完整额度, + // 无需在此处刷新 tt_user_transfer_daily。 + } + + private String getRandomPhoneNumber() { + while (true) { + try { + String phoneNumber = PhoneNumberUtil.createPhoneNumber(new Random().nextInt(3)); + TtUser ttUser = new LambdaQueryChainWrapper<>(baseMapper).eq(TtUser::getPhoneNumber, phoneNumber) + .eq(TtUser::getDelFlag, "0").one(); + if (StringUtils.isNull(ttUser)) { + return phoneNumber; + } + } catch (Exception e) { + return null; + } + } + } + + @Override + public TtUserVipLevelVo userVipLeverRate(Integer userId) { + // 先刷新VIP等级和累计充值缓存(确保归档后数据不丢) + updateUserVIPLevel(userId); + TtUser ttUser = getById(userId); + BigDecimal totalRechargeAmount = ttUser.getTotalRechargeAmount() != null + ? ttUser.getTotalRechargeAmount() : BigDecimal.ZERO; + List ttVipLevels = ttVipLevelService.list( + new LambdaQueryWrapper().orderByAsc(TtVipLevel::getId)); + Optional optional = ttVipLevels.stream().map(TtVipLevel::getRechargeThreshold).max(BigDecimal::compareTo); + BigDecimal maxRate = optional.orElse(BigDecimal.ZERO); + // tt_user.vip_level 存的是数据库 id(id=1 对应 VIP0),返回给前端时转成显示等级 + Integer displayVipLevel = ttUser.getVipLevel() != null && ttUser.getVipLevel() > 0 + ? ttUser.getVipLevel() - 1 + : 0; + return TtUserVipLevelVo.builder() + .userId(userId) + .vipLevel(displayVipLevel) + .rate(totalRechargeAmount.compareTo(maxRate) > 0 ? maxRate : totalRechargeAmount) + .maxRate(maxRate) + .build(); + } + + @Override + public TtUserVipProgressVo userVipProgress(Integer userId) { + // 刷新VIP等级和累计充值缓存(确保归档后数据不丢) + updateUserVIPLevel(userId); + TtUser currentUser = getById(userId); + BigDecimal rechargeTotal = currentUser.getTotalRechargeAmount() != null + ? currentUser.getTotalRechargeAmount() : BigDecimal.ZERO; + long currentExp = rechargeTotal.setScale(0, RoundingMode.CEILING).longValue(); + + // 2. 等级阈值列表(按 id 升序对应 VIP0..VIPn) + List levels = ttVipLevelService.list( + new LambdaQueryWrapper().orderByAsc(TtVipLevel::getId)); + if (CollectionUtils.isEmpty(levels)) { + return TtUserVipProgressVo.builder() + .userId(userId) + .vipLevel(0) + .currentExp(currentExp) + .nextLevelExp(0L) + .needExp(0L) + .maxLevel(true) + .progressText(currentExp + "/0") + .build(); + } + + // 3. 基于经验值实时定位等级:用户起始即 VIP0;每跨过一档阈值升一级 + // crossedIdx = 已跨过的最大阈值下标(-1 表示一档未跨) + int crossedIdx = -1; + for (int i = 0; i < levels.size(); i++) { + BigDecimal th = levels.get(i).getRechargeThreshold(); + long thLong = (th == null ? 0L : th.setScale(0, RoundingMode.CEILING).longValue()); + if (currentExp >= thLong) { + crossedIdx = i; + } else { + break; + } + } + + // 4. 展示等级 = 已跨过的最大阈值下标;未跨任何阈值时保底为 VIP0 + boolean maxLevel = (crossedIdx >= levels.size() - 1); + int displayLevel = maxLevel ? (levels.size() - 1) : Math.max(0, crossedIdx); + + // 5. 下一级阈值:未封顶取下一档阈值;封顶取最末档阈值 + BigDecimal nextThreshold = maxLevel + ? levels.get(levels.size() - 1).getRechargeThreshold() + : levels.get(crossedIdx + 1).getRechargeThreshold(); + if (nextThreshold == null) { + nextThreshold = BigDecimal.ZERO; + } + long nextExp = nextThreshold.setScale(0, RoundingMode.CEILING).longValue(); + long needExp = Math.max(0L, nextExp - currentExp); + + return TtUserVipProgressVo.builder() + .userId(userId) + .vipLevel(displayLevel) + .currentExp(currentExp) + .nextLevelExp(nextExp) + .needExp(needExp) + .maxLevel(maxLevel) + .progressText(currentExp + "/" + nextExp) + .build(); + } + + @Override + public TtUserInfoVo userInfo(Integer userId) { + TtUserInfoVo vo = new TtUserInfoVo(); + TtUser ttUser = getById(userId); + vo.setUserId(ttUser.getUserId()); + if (ttUser.getParentId() != null) { + TtUser parent = getById(ttUser.getParentId()); + if (parent != null) { + vo.setParentId(parent.getParentId()); + vo.setParentNickName(parent.getNickName()); + } + } + // 查询所有下级id + List allEmployeesId = userMapper.allEmployeesByParents(Collections.singletonList(ttUser.getUserId())); + if (CollectionUtils.isNotEmpty(allEmployeesId)) { + vo.setChildNum(allEmployeesId.size()); + // 查询所有下级累计总充值量(主表 + 归档表 UNION ALL) + BigDecimal totalRechargeAmount = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource( + allEmployeesId, + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + vo.setRechargePrice(totalRechargeAmount != null ? totalRechargeAmount : BigDecimal.ZERO); + } + vo.setCommissionsRate(new BigDecimal(3)); + vo.setTurnoverRate(new BigDecimal(3)); + return vo; + } + + @Override + public List userChildInfos(Integer userId) { + // 查询结果 + LambdaQueryWrapper ttUserQuery = new LambdaQueryWrapper<>(); + ttUserQuery.eq(TtUser::getParentId, userId); + List ttUsers = userMapper.selectList(ttUserQuery); + if (CollectionUtils.isEmpty(ttUsers)) { + return Collections.emptyList(); + } + List vos = new ArrayList<>(); + for (TtUser ttUser : ttUsers) { + TtUseChildInfoVo vo = new TtUseChildInfoVo(); + BeanUtils.copyProperties(ttUser, vo); + vos.add(vo); + } + return vos; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public AjaxResult userVipLevelRechargeThreshold(Integer userId, List vipLevels) { + // 领取前先根据累计充值刷新 VIP 等级,避免 tt_user.vip_level 滞后于实际充值进度 + updateUserVIPLevel(userId); + TtUser ttUser = getById(userId); + if (ttUser == null) { + return AjaxResult.error("当前用户不存在"); + } + if (ttUser.getVipLevel() == null) { + return AjaxResult.error("用户VIP等级未初始化"); + } + if (CollectionUtils.isEmpty(vipLevels)) { + return AjaxResult.error("VIP等级参数不能为空"); + } + // 去重 + 过滤 null;前端传的是显示等级 VIPn 中的 n,转成数据库主键 ID(VIP0 -> id=1) + List distinctLevels = vipLevels.stream() + .filter(Objects::nonNull) + .distinct() + .map(lv -> lv + 1) + .collect(Collectors.toList()); + if (distinctLevels.isEmpty()) { + return AjaxResult.error("VIP等级参数不能为空"); + } + // 校验:所有等级都必须 <= 当前用户VIP等级 + for (Integer lv : distinctLevels) { + if (ttUser.getVipLevel() < lv) { + return AjaxResult.error("VIP" + (lv - 1) + "等级未达成,无法领取"); + } + } + + // ===== 1次查:批量拉取所有请求等级的 VIP 配置 ===== + List levelList = ttVipLevelService.listByIds(distinctLevels); + Map levelMap = CollectionUtils.isEmpty(levelList) ? Collections.emptyMap() + : levelList.stream().collect(Collectors.toMap(TtVipLevel::getId, l -> l)); + + // ===== 1次查:批量查询这些等级的领取流水(以 remark 判重)===== + String msgPrefix = TtAccountRecordSource.VIP_LEVEL_REACH.getMsg(); + List targetRemarks = distinctLevels.stream() + .map(lv -> msgPrefix + lv) + .collect(Collectors.toList()); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(TtUserBlendErcash::getUserId, userId) + .eq(TtUserBlendErcash::getType, TtAccountRecordType.INPUT.getCode()) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.VIP_LEVEL_REACH.getCode()) + .in(TtUserBlendErcash::getRemark, targetRemarks) + .select(TtUserBlendErcash::getRemark); + Set claimedRemarks = ttUserBlendErcashMapper.selectList(query).stream() + .map(TtUserBlendErcash::getRemark) + .collect(Collectors.toSet()); + + // ===== 纯内存划分:待领取 / 已领取 / 未配置金额 ===== + List claimedNow = new ArrayList<>(); + List alreadyClaimed = new ArrayList<>(); + List emptyBonus = new ArrayList<>(); + List insertList = new ArrayList<>(); + BigDecimal totalAdded = BigDecimal.ZERO; + Timestamp now = new Timestamp(System.currentTimeMillis()); + + for (Integer lv : distinctLevels) { + TtVipLevel level = levelMap.get(lv); + if (level == null) { + continue; + } + BigDecimal addedBonus = level.getAddedBonus(); + // 方案 A:addedBonus 为 null 或负数才视为「未配置」退出;addedBonus=0 也允许领取(0 元入账 + 幂等流水)。 + if (addedBonus == null || addedBonus.compareTo(BigDecimal.ZERO) < 0) { + emptyBonus.add(lv); + continue; + } + String ramark = msgPrefix + level.getId(); + if (claimedRemarks.contains(ramark)) { + alreadyClaimed.add(lv); + continue; + } + totalAdded = totalAdded.add(addedBonus); + insertList.add(TtUserBlendErcash.builder() + .userId(userId) + // VIP等级达标赠送是加在「余额/金币」(account_amount)上,流水必须写入 amount/finalAmount 字段, + // 不能写 credits/finalCredits(那是「积分」字段,会让后台「积分变动」错误出现这条赠送)。 + .amount(addedBonus) + // 在原始账户余额上逐笔累加(批量领取多笔时保证 final_amount 递增) + .finalAmount(ttUser.getAccountAmount() == null ? totalAdded : ttUser.getAccountAmount().add(totalAdded)) + .total(addedBonus) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.VIP_LEVEL_REACH.getCode()) + .remark(ramark) + .createTime(now) + .updateTime(now) + .build()); + claimedNow.add(lv); + } + + if (claimedNow.isEmpty()) { + // 细化提示:区分「均已领取」 / 「后台未配置金额」 / 「等级不存在」 + if (!alreadyClaimed.isEmpty() && emptyBonus.isEmpty()) { + return AjaxResult.error("VIP红包均已领取:" + alreadyClaimed); + } + if (alreadyClaimed.isEmpty() && !emptyBonus.isEmpty()) { + return AjaxResult.error("以下VIP等级未配置奖励金额:" + emptyBonus); + } + if (!alreadyClaimed.isEmpty()) { + return AjaxResult.error("部分已领取" + alreadyClaimed + ",部分未配置金额" + emptyBonus); + } + return AjaxResult.error("没有可领取的VIP等级红包(请检查VIP等级配置是否存在)"); + } + + // ===== 1次写:仅当 totalAdded > 0 才更新账户余额(全部为 0 元领取时跳过 SQL,避免无意义的 +0 更新)===== + if (totalAdded.compareTo(BigDecimal.ZERO) > 0) { + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate.eq(TtUser::getUserId, userId) + .setSql("account_amount = account_amount + " + totalAdded); + this.update(userUpdate); + } + + // ===== 1次批量 insert 流水 ===== + ttUserBlendErcashService.saveBatch(insertList); + + Map data = new LinkedHashMap<>(); + data.put("claimed", claimedNow.stream().map(lv -> lv - 1).collect(Collectors.toList())); + data.put("alreadyClaimed", alreadyClaimed.stream().map(lv -> lv - 1).collect(Collectors.toList())); + data.put("emptyBonus", emptyBonus.stream().map(lv -> lv - 1).collect(Collectors.toList())); + data.put("totalBonus", totalAdded); + return AjaxResult.success("领取成功", data); + } + + @Override + public AjaxResult userHasVipLevelRechargeThreshold(Integer userId) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(TtUserBlendErcash::getUserId, userId) + .eq(TtUserBlendErcash::getType, TtAccountRecordType.INPUT.getCode()) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.VIP_LEVEL_REACH.getCode()) + ; + List dbBlendErcash = ttUserBlendErcashMapper.selectList(query); + if (CollectionUtils.isEmpty(dbBlendErcash)) { + return AjaxResult.success(new ArrayList<>()); + } + List vips = new ArrayList<>(); + for (TtUserBlendErcash ercash : dbBlendErcash) { + if (ercash == null || StringUtils.isBlank(ercash.getRemark())) { + continue; + } + String vip = ercash.getRemark().replace(TtAccountRecordSource.VIP_LEVEL_REACH.getMsg(), ""); + vips.add(String.valueOf(Integer.parseInt(vip) - 1)); + } + return AjaxResult.success(vips); + } + + @Override + public AjaxResult checkVipLevelClaimable(Integer userId) { + // 查询前先根据累计充值刷新 VIP 等级,避免 tt_user.vip_level 滞后于实际充值进度 + updateUserVIPLevel(userId); + TtUser ttUser = getById(userId); + if (ttUser == null || ttUser.getVipLevel() == null || ttUser.getVipLevel() <= 0) { + return AjaxResult.success(Collections.emptyList()); + } + // 查询所有VIP等级配置(id <= 用户当前等级 且 addedBonus >= 0) + List allLevels = ttVipLevelService.list(); + List eligibleLevels = allLevels.stream() + .filter(lv -> lv != null && lv.getId() != null + && lv.getId() <= ttUser.getVipLevel() + && lv.getAddedBonus() != null + && lv.getAddedBonus().compareTo(BigDecimal.ZERO) >= 0) + .sorted(java.util.Comparator.comparing(TtVipLevel::getId)) + .collect(Collectors.toList()); + if (eligibleLevels.isEmpty()) { + return AjaxResult.success(Collections.emptyList()); + } + // 查询已领取的等级 + String msgPrefix = TtAccountRecordSource.VIP_LEVEL_REACH.getMsg(); + List targetRemarks = eligibleLevels.stream() + .map(lv -> msgPrefix + lv.getId()) + .collect(Collectors.toList()); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(TtUserBlendErcash::getUserId, userId) + .eq(TtUserBlendErcash::getType, TtAccountRecordType.INPUT.getCode()) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.VIP_LEVEL_REACH.getCode()) + .in(TtUserBlendErcash::getRemark, targetRemarks) + .select(TtUserBlendErcash::getRemark); + Set claimedRemarks = ttUserBlendErcashMapper.selectList(query).stream() + .map(TtUserBlendErcash::getRemark) + .collect(Collectors.toSet()); + // 返回未领取的VIP等级ID列表 + List claimableList = eligibleLevels.stream() + .filter(lv -> !claimedRemarks.contains(msgPrefix + lv.getId())) + .map(lv -> String.valueOf(lv.getId() - 1)) + .collect(Collectors.toList()); + return AjaxResult.success(claimableList); + } + + @Override + public AjaxResult userRechargeBonus(Integer userId, Integer recharge, Double bonus) { + TtUser ttUser = getById(userId); + if (ttUser == null) { + return AjaxResult.error("当前用户不存在"); + } + if (recharge == null) { + return AjaxResult.error("充值金额门槛为空"); + } + if (bonus == null) { + return AjaxResult.error("奖励金额为空"); + } + // 创建日期格式化对象,指定日期格式 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + // 获取Calendar实例 + Calendar calendar = Calendar.getInstance(); + // 设置时间为今天凌晨 + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + // 获取今天凌晨时间 + Date todayMidnight = calendar.getTime(); + String formattedTodayMidnight = formatter.format(todayMidnight); + // 获取明天凌晨时间 + calendar.add(Calendar.DAY_OF_MONTH, 1); + Date tomorrowMidnight = calendar.getTime(); + String formattedTomorrowMidnight = formatter.format(tomorrowMidnight); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(TtRechargeRecord::getUserId, userId); + wrapper.between(TtRechargeRecord::getCreateTime, formattedTodayMidnight, formattedTomorrowMidnight); + List records = ttRechargeRecordMapper.selectList(wrapper); + if (CollectionUtils.isEmpty(records)) { + return AjaxResult.error("今天没有充值记录"); + } + BigDecimal total = records.stream().filter(Objects::nonNull).map(TtRechargeRecord::getAmountActuallyPaid).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal rechargeB = new BigDecimal(recharge); + if (rechargeB.compareTo(total) > 0) { + return AjaxResult.error("今天充值未达到门槛"); + } + String ramark = TtAccountRecordSource.RECHARGE_RESEARCH_BONUS.getMsg() + recharge; + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(TtUserBlendErcash::getUserId, userId) + .eq(TtUserBlendErcash::getType, TtAccountRecordType.INPUT.getCode()) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.RECHARGE_RESEARCH_BONUS.getCode()) + .eq(TtUserBlendErcash::getRemark, ramark) + .between(TtUserBlendErcash::getCreateTime, formattedTodayMidnight, formattedTomorrowMidnight) + ; + List dbBlendErcash = ttUserBlendErcashMapper.selectList(query); + if (CollectionUtils.isNotEmpty(dbBlendErcash)) { + return AjaxResult.error("今天充值门槛达标奖励已领取"); + } + // 加钱 + TtUser user = this.getById(userId); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + bonus); + this.update(userUpdate); + BigDecimal bonusB = new BigDecimal(bonus); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + .credits(bonusB.compareTo(BigDecimal.ZERO) > 0 ? bonusB : null) + .finalCredits(bonusB.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountCredits().add(bonusB) : null) + .total(bonusB) // 收支合计 + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE_RESEARCH_BONUS.getCode()) + .remark(ramark) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + ttUserBlendErcashMapper.insert(blendErcash); + + return AjaxResult.success(); + } + + @Override + public AjaxResult userHasRechargeBonus(Integer userId) { + // 创建日期格式化对象,指定日期格式 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + // 获取Calendar实例 + Calendar calendar = Calendar.getInstance(); + // 设置时间为今天凌晨 + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + // 获取今天凌晨时间 + Date todayMidnight = calendar.getTime(); + String formattedTodayMidnight = formatter.format(todayMidnight); + // 获取明天凌晨时间 + calendar.add(Calendar.DAY_OF_MONTH, 1); + Date tomorrowMidnight = calendar.getTime(); + String formattedTomorrowMidnight = formatter.format(tomorrowMidnight); + + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(TtUserBlendErcash::getUserId, userId) + .eq(TtUserBlendErcash::getType, TtAccountRecordType.INPUT.getCode()) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.RECHARGE_RESEARCH_BONUS.getCode()) + .between(TtUserBlendErcash::getCreateTime, formattedTodayMidnight, formattedTomorrowMidnight) + ; + List dbBlendErcash = ttUserBlendErcashMapper.selectList(query); + if (CollectionUtils.isEmpty(dbBlendErcash)) { + return AjaxResult.success(new ArrayList<>()); + } + List recharges = new ArrayList<>(); + for (TtUserBlendErcash ercash : dbBlendErcash) { + if (ercash == null || StringUtils.isBlank(ercash.getRemark())) { + continue; + } + String recharge = ercash.getRemark().replace(TtAccountRecordSource.RECHARGE_RESEARCH_BONUS.getMsg(), ""); + recharges.add(recharge); + } + return AjaxResult.success(recharges); + } + + @Override + public AjaxResult userCreditsToAmount(Integer userId, BigDecimal credits) { + TtUser ttUser = getById(userId); + if (ttUser == null) { + return AjaxResult.error("当前用户不存在"); + } + if (credits == null || credits.compareTo(BigDecimal.ZERO) <= 0) { + return AjaxResult.error("积分数据不合法"); + } + // 检查积分小数点是否超过两位 + if (credits.scale() > 2) { + return AjaxResult.error("积分小数点不能超过两位"); + } + if (credits.compareTo(ttUser.getAccountCredits()) > 0) { + return AjaxResult.error("兑换积分超过账户当前积分"); + } + int count = userMapper.creditsToAmount(Long.valueOf(ttUser.getUserId()), credits); + if (count <= 0) { + return AjaxResult.error("兑换失败,请稍后重试"); + } + return AjaxResult.success(); + } + + /** + * 日常金币变更,不是提货变更。 + * 根据金币变更规则,附带积分变更。 + * + * @param userId + * @param amount + * @param source + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R updateUserAccount(Integer userId, BigDecimal amount, TtAccountRecordSource source) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) { + return R.fail("金额为零不需要更新"); + } + + TtUser user = this.getById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + + if (amount.compareTo(BigDecimal.ZERO) > 0) { + int count = userMapper.addAccountAmount(Long.valueOf(userId), amount); + if (count <= 0) { + return R.fail("更新账户金额失败"); + } + + BigDecimal newAccountAmount = user.getAccountAmount().add(amount); + BigDecimal newCredits = user.getAccountCredits().add(amount); + + // 🔥 新增:在充值时也记录归属关系(用于追踪用户来源) + List referralIds = findAnchorAndBusinessAtTime(userId); + Integer anchorId = referralIds.get(0); + Integer businessId = referralIds.get(1); + + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + .amount(amount) + .finalAmount(newAccountAmount) + .credits(amount) + .finalCredits(newCredits) + .total(amount) // 收支合计 + .type(TtAccountRecordType.INPUT.getCode()) + .source(source.getCode()) + .remark(source.getMsg()) + + // 🔥 新增字段:直接记录归属关系 + .anchorId(anchorId) // 上级主播 + .businessId(businessId) // 招商 + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + ttUserBlendErcashMapper.insert(blendErcash); + return R.ok(new UpdateUserAccountBo(userId.longValue(), newAccountAmount, newCredits)); + } else { + int count = userMapper.subtractAccountAmount(Long.valueOf(userId), amount.abs()); + if (count <= 0) { + return R.fail("账户余额不足"); + } + BigDecimal newAccountAmount = user.getAccountAmount().subtract(amount.abs()); + + // 🔥 新增:在游戏消费前计算归属关系快照(用于防借流水) + List gameReferralIds = null; + boolean isGameConsume = TtAccountRecordSource.isGameConsumeForCommission(source.getCode()); + if (isGameConsume) { + gameReferralIds = findAnchorAndBusinessAtTime(userId); + } + + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + .amount(amount) + .finalAmount(newAccountAmount) + .total(amount) // 收支合计 + .type(TtAccountRecordType.OUTPUT.getCode()) + .source(source.getCode()) + .remark(source.getMsg()) + + // 🔥 新增字段:游戏消费时记录归属关系 + .anchorId(isGameConsume ? gameReferralIds.get(0) : null) + .businessId(isGameConsume ? gameReferralIds.get(1) : null) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + if (newAccountAmount.compareTo(user.getAccountCredits()) < 0) { + BigDecimal diff = user.getAccountCredits().subtract(newAccountAmount); + blendErcash.setCredits(diff.negate()); + blendErcash.setFinalCredits(newAccountAmount); + } + + ttUserBlendErcashMapper.insert(blendErcash); + // 精度安全:全程 BigDecimal,避免 doubleValue 转换丢精度 + BigDecimal newCredits = user.getAccountCredits().min(newAccountAmount); + // 游戏消费后异步分发佣金给上级 + tryDistributeGameCommission(userId, amount.abs(), source); + // VIP 流水返利已改为每日定时任务发放(见 VipFlowRebateTask.grantDailyVipRebate),此处不再实时发放 + // tryVipFlowRebate(userId, amount.abs(), source); + return R.ok(new UpdateUserAccountBo(userId.longValue(), newAccountAmount, newCredits)); + } + } + + /** + * 仅扣减金币,不动积分(避开 LEAST 副作用)。仅供扣款场景使用。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R updateUserAccountKeepCredits(Integer userId, BigDecimal amount, TtAccountRecordSource source) { + return updateUserAccountKeepCredits(userId, amount, source, BigDecimal.ZERO); + } + + /** + * 扣减金币 + 积分,并写入一条同时包含 amount 和 credits 的 blend_ercash 流水记录。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R updateUserAccountKeepCredits(Integer userId, BigDecimal amount, TtAccountRecordSource source, BigDecimal creditsCut) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) >= 0) { + return R.fail("仅支持扣款场景,amount 必须小于 0"); + } + if (creditsCut == null) { + creditsCut = BigDecimal.ZERO; + } + if (creditsCut.compareTo(BigDecimal.ZERO) < 0) { + return R.fail("积分扣减额 creditsCut 不能为负数"); + } + TtUser user = this.getById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + // 扣减金币 + int count = userMapper.subtractOnlyAccountAmount(Long.valueOf(userId), amount.abs()); + if (count <= 0) { + return R.fail("账户余额不足"); + } + // 扣减积分 + if (creditsCut.compareTo(BigDecimal.ZERO) > 0) { + int creditRows = userMapper.subtractOnlyAccountCredits(Long.valueOf(userId), creditsCut); + if (creditRows <= 0) { + throw new RuntimeException("积分扣减失败"); + } + } + BigDecimal newAccountAmount = user.getAccountAmount().subtract(amount.abs()); + BigDecimal newAccountCredits = user.getAccountCredits().subtract(creditsCut); + + // 🔥 新增:在游戏消费前计算归属关系快照(用于防借流水) + List gameReferralIds = null; + boolean isGameConsume = TtAccountRecordSource.isGameConsumeForCommission(source.getCode()); + if (isGameConsume) { + gameReferralIds = findAnchorAndBusinessAtTime(userId); + } + + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + .amount(amount) + .finalAmount(newAccountAmount) + .credits(creditsCut.negate()) + .finalCredits(newAccountCredits) + .total(amount) // 收支合计保持金币口径,避免影响既有 turnover 统计 + .type(TtAccountRecordType.OUTPUT.getCode()) + .source(source.getCode()) + .remark(source.getMsg()) + + // 🔥 新增字段:游戏消费时记录归属关系 + .anchorId(isGameConsume ? gameReferralIds.get(0) : null) + .businessId(isGameConsume ? gameReferralIds.get(1) : null) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttUserBlendErcashMapper.insert(blendErcash); + // 游戏消费后异步分发佣金给上级 + tryDistributeGameCommission(userId, amount.abs(), source); + // VIP 流水返利已改为每日定时任务发放(见 VipFlowRebateTask.grantDailyVipRebate),此处不再实时发放 + // tryVipFlowRebate(userId, amount.abs(), source); + return R.ok(new UpdateUserAccountBo(userId.longValue(), newAccountAmount, newAccountCredits)); + } + + /** + * 退款专用:加金币 + 加积分,与 updateUserAccountKeepCredits 对称。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R refundKeepCredits(Integer userId, BigDecimal goldAmount, TtAccountRecordSource source, BigDecimal creditsRefund) { + if (goldAmount == null || goldAmount.compareTo(BigDecimal.ZERO) <= 0) { + return R.fail("退款金额必须大于0"); + } + if (creditsRefund == null) { + creditsRefund = BigDecimal.ZERO; + } + TtUser user = this.getById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + // 加金币(仅金币,不动积分;与扣款 subtractOnlyAccountAmount 对称,避免积分多退本金) + int count = userMapper.addOnlyAccountAmount(Long.valueOf(userId), goldAmount); + if (count <= 0) { + return R.fail("更新账户金额失败"); + } + // 加积分 + if (creditsRefund.compareTo(BigDecimal.ZERO) > 0) { + int creditRows = userMapper.addOnlyAccountCredits(Long.valueOf(userId), creditsRefund); + if (creditRows <= 0) { + throw new RuntimeException("积分退还失败"); + } + } + BigDecimal newAccountAmount = user.getAccountAmount().add(goldAmount); + BigDecimal newCredits = user.getAccountCredits().add(creditsRefund); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + .amount(goldAmount) + .finalAmount(newAccountAmount) + .credits(creditsRefund) + .finalCredits(newCredits) + .total(goldAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(source.getCode()) + .remark(source.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttUserBlendErcashMapper.insert(blendErcash); + return R.ok(new UpdateUserAccountBo(userId.longValue(), newAccountAmount, newCredits)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R updateOnlyUserAccount(Integer userId, BigDecimal amount, TtAccountRecordSource source) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) { + return R.fail("金额为零不需要更新"); + } + + TtUser user = this.getById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + + if (amount.compareTo(BigDecimal.ZERO) > 0) { + int count = userMapper.addOnlyAccountAmount(Long.valueOf(userId), amount); + if (count <= 0) { + return R.fail("更新账户金额失败"); + } + + BigDecimal newAccountAmount = user.getAccountAmount().add(amount); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + + .amount(amount) + .finalAmount(newAccountAmount) + + .total(amount) // 收支合计 + + .type(TtAccountRecordType.INPUT.getCode()) + .source(source.getCode()) + .remark(source.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + ttUserBlendErcashMapper.insert(blendErcash); + return R.ok(new UpdateUserAccountBo(userId.longValue(), newAccountAmount, user.getAccountCredits())); + } else { + int count = userMapper.subtractAccountAmount(Long.valueOf(userId), amount.abs()); + if (count <= 0) { + return R.fail("账户余额不足"); + } + BigDecimal newAccountAmount = user.getAccountAmount().subtract(amount.abs()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + + .amount(amount) + .finalAmount(newAccountAmount) + + .total(amount) // 收支合计 + + .type(TtAccountRecordType.OUTPUT.getCode()) + .source(source.getCode()) + .remark(source.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + if (newAccountAmount.compareTo(user.getAccountCredits()) < 0) { + BigDecimal diff = user.getAccountCredits().subtract(newAccountAmount); + blendErcash.setCredits(diff.negate()); + blendErcash.setFinalCredits(newAccountAmount); + } + + ttUserBlendErcashMapper.insert(blendErcash); + // 精度安全:全程 BigDecimal,避免 doubleValue 转换丢精度 + BigDecimal newCredits = user.getAccountCredits().min(newAccountAmount); + return R.ok(new UpdateUserAccountBo(userId.longValue(), newAccountAmount, newCredits)); + } + } + + /** + * 日常金币变更,不是提货变更。 + * 根据金币变更规则,附带积分变更。 + * + * @param userId + * @param amount + * @param source + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R updateUserCredits(Integer userId, BigDecimal amount, TtAccountRecordSource source) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) { + return R.fail("金额为零不需要更新"); + } + + TtUser user = this.getById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + + if (amount.compareTo(BigDecimal.ZERO) > 0) { + int count = userMapper.addOnlyAccountCredits(Long.valueOf(userId), amount); + if (count <= 0) { + return R.fail("更新账户积分失败"); + } + + BigDecimal newCredits = user.getAccountCredits().add(amount); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + + .amount(BigDecimal.ZERO) + .finalAmount(user.getAccountAmount()) + + .credits(amount) + .finalCredits(newCredits) + + .total(amount) // 收支合计 + + .type(TtAccountRecordType.INPUT.getCode()) + .source(source.getCode()) + .remark(source.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + ttUserBlendErcashMapper.insert(blendErcash); + return R.ok(new UpdateUserAccountBo(userId.longValue(), user.getAccountAmount(), newCredits)); + } else { + int count = userMapper.subtractCreditsAmount(Long.valueOf(userId), amount.abs()); + if (count <= 0) { + return R.fail("账户积分不足"); + } + BigDecimal newAccountCredits = user.getAccountCredits().subtract(amount.abs()); + BigDecimal newAccountAmount = user.getAccountAmount().subtract(amount.abs()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + + .amount(amount) + .finalAmount(newAccountAmount) + + .credits(amount) + .finalCredits(newAccountCredits) + + .total(amount) // 收支合计 + + .type(TtAccountRecordType.OUTPUT.getCode()) + .source(source.getCode()) + .remark(source.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + ttUserBlendErcashMapper.insert(blendErcash); + // 精度安全:全程 BigDecimal,避免 doubleValue 转换丢精度 + BigDecimal newCredits = user.getAccountCredits().min(newAccountAmount); + return R.ok(new UpdateUserAccountBo(userId.longValue(), newAccountAmount, newCredits)); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R updateUserOnlyCredits(Integer userId, BigDecimal amount, TtAccountRecordSource source) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) >= 0) { + return R.fail("仅支持扣款(amount<0)"); + } + + TtUser user = this.getById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + + // 仅扣减积分,account_amount 保持不变 + int count = userMapper.subtractOnlyAccountCredits(Long.valueOf(userId), amount.abs()); + if (count <= 0) { + return R.fail("账户积分不足"); + } + BigDecimal newAccountCredits = user.getAccountCredits().subtract(amount.abs()); + BigDecimal originAccountAmount = user.getAccountAmount(); + + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + + // 金币未变动 + .amount(BigDecimal.ZERO) + .finalAmount(originAccountAmount) + + // 仅记录积分扣减 + .credits(amount) + .finalCredits(newAccountCredits) + + .total(amount) // 收支合计 + + .type(TtAccountRecordType.OUTPUT.getCode()) + .source(source.getCode()) + .remark(source.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + ttUserBlendErcashMapper.insert(blendErcash); + + return R.ok(new UpdateUserAccountBo(userId.longValue(), originAccountAmount, newAccountCredits)); + } + + /** + * 管理员通过「修改用户」调整余额/积分时,写入余额变动/积分变动流水(source=65/66)。 + * 只写流水、不动账户字段(账户字段由 updateUserById 统一更新)。 + * newUser 携带修改后的绝对余额/积分(为 null 表示该字段未修改)。 + * 操作人与登录 IP 写入流水 remark 以便审计。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void recordAdminBalanceCreditChange(TtUser newUser, String operatorName, String operatorIp) { + if (newUser == null || newUser.getUserId() == null) { + return; + } + TtUser old = this.getById(newUser.getUserId().longValue()); + if (old == null) { + return; + } + + String remark = buildAuditRemark(operatorName, operatorIp); + Timestamp now = new Timestamp(System.currentTimeMillis()); + + // 余额变动 + if (newUser.getAccountAmount() != null) { + BigDecimal balanceChange = newUser.getAccountAmount().subtract(old.getAccountAmount()); + if (balanceChange.compareTo(BigDecimal.ZERO) != 0) { + ttUserBlendErcashMapper.insert(TtUserBlendErcash.builder() + .userId(newUser.getUserId()) + .amount(balanceChange) + .finalAmount(newUser.getAccountAmount()) + .credits(BigDecimal.ZERO) + .finalCredits(old.getAccountCredits()) + .total(balanceChange) + .type(balanceChange.compareTo(BigDecimal.ZERO) > 0 ? TtAccountRecordType.INPUT.getCode() : TtAccountRecordType.OUTPUT.getCode()) + .source(TtAccountRecordSource.ADMIN_ADJUST_BALANCE.getCode()) + .remark(remark) + .createTime(now) + .updateTime(now) + .build()); + } + } + + // 积分变动 + if (newUser.getAccountCredits() != null) { + BigDecimal creditsChange = newUser.getAccountCredits().subtract(old.getAccountCredits()); + if (creditsChange.compareTo(BigDecimal.ZERO) != 0) { + ttUserBlendErcashMapper.insert(TtUserBlendErcash.builder() + .userId(newUser.getUserId()) + .amount(BigDecimal.ZERO) + .finalAmount(old.getAccountAmount()) + .credits(creditsChange) + .finalCredits(newUser.getAccountCredits()) + .total(creditsChange) + .type(creditsChange.compareTo(BigDecimal.ZERO) > 0 ? TtAccountRecordType.INPUT.getCode() : TtAccountRecordType.OUTPUT.getCode()) + .source(TtAccountRecordSource.ADMIN_ADJUST_CREDITS.getCode()) + .remark(remark) + .createTime(now) + .updateTime(now) + .build()); + } + } + } + + /** + * 组装审计备注:操作人 + 登录 IP。 + */ + private String buildAuditRemark(String operatorName, String operatorIp) { + StringBuilder audit = new StringBuilder(); + if (StringUtils.isNotEmpty(operatorName)) { + audit.append("操作人:").append(operatorName); + } + if (StringUtils.isNotEmpty(operatorIp)) { + if (audit.length() > 0) { + audit.append(","); + } + audit.append("登录IP:").append(operatorIp); + } + return audit.toString(); + } + + /** + * 通过 MQ 异步分发游戏消费佣金给上级。 + * 当下级玩游戏消费时,根据上级的 newGameCommissionRate 或 classicGameCommissionRate 计算佣金, + * 实时存入上级的金币余额。递归向上分发给所有祖先。 + *

+ * 发消息放在事务提交后(afterCommit),避免下注事务回滚但佣金消息已发出的资金隐患; + * 消费失败由 MQ 消费者 nack 重投,不再依赖本地线程 + 3 次重试。 + */ + private void tryDistributeGameCommission(Integer userId, BigDecimal consumeAmount, TtAccountRecordSource source) { + if (consumeAmount == null || consumeAmount.compareTo(BigDecimal.ZERO) <= 0) return; + if (!TtAccountRecordSource.isGameConsumeForCommission(source.getCode())) return; + + // 白号游戏消费不触发上级佣金 + TtUser user = this.getById(userId); + if (user != null && Integer.valueOf(1).equals(user.getIsBlankAccount())) return; + + Map msg = new HashMap<>(); + msg.put("userId", userId); + msg.put("consumeAmount", consumeAmount.toString()); + msg.put("sourceCode", source.getCode()); + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + rabbitTemplate.convertAndSend(DelayedQueueConfig.WHEEL_COMMISSION_QUEUE, msg); + } + }); + } else { + rabbitTemplate.convertAndSend(DelayedQueueConfig.WHEEL_COMMISSION_QUEUE, msg); + } + } + + /** + * 供 MQ 消费者调用:递归向上逐级分发游戏消费佣金。 + * 消费失败时抛异常,由消费者 nack 重投;直到 DB 恢复成功消费为止,佣金不会丢。 + */ + @Override + public void distributeGameCommission(Integer userId, BigDecimal consumeAmount, TtAccountRecordSource source) { + distributeGameCommissionRecursive(userId, consumeAmount, source, 0); + } + + /** + * 实时发放 VIP 流水返利。 + * 用户每玩一把游戏扣费时立即按当前 VIP 等级的 flowRebatePercent 计算返利并入账。 + */ + private void tryVipFlowRebate(Integer userId, BigDecimal consumeAmount, TtAccountRecordSource source) { + if (consumeAmount == null || consumeAmount.compareTo(BigDecimal.ZERO) <= 0) return; + if (!TtAccountRecordSource.getGameBetCodes().contains(source.getCode())) return; + + CompletableFuture.runAsync(() -> { + try { + TtUser user = this.getById(userId); + if (user == null) return; + // 白号不触发 VIP 流水返利 + if (Integer.valueOf(1).equals(user.getIsBlankAccount())) return; + if (user.getVipLevel() == null || user.getVipLevel() <= 0) return; + + TtVipLevel vipLevel = ttVipLevelService.getById(user.getVipLevel()); + if (vipLevel == null) return; + + // 优先使用 flowRebatePercent;若为 null 则回退到 commissions + BigDecimal percent = vipLevel.getFlowRebatePercent() != null + ? vipLevel.getFlowRebatePercent() + : (vipLevel.getCommissions() != null ? vipLevel.getCommissions() : BigDecimal.ZERO); + if (percent.compareTo(BigDecimal.ZERO) <= 0) return; + + BigDecimal rebate = consumeAmount + .multiply(percent) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + if (rebate.compareTo(BigDecimal.ZERO) <= 0) return; + + // 直接加金币(不走 updateUserAccount 避免递归触发佣金/返利) + int count = userMapper.addOnlyAccountAmount(Long.valueOf(userId), rebate); + if (count > 0) { + // 等待游戏扣费的 blend_ercash 异步写入完成,避免读到扣费前的旧余额 + Thread.sleep(100); + // 重新读取最新余额 + TtUser freshUser = this.getById(userId); + BigDecimal actualBalance = (freshUser != null) ? freshUser.getAccountAmount() : user.getAccountAmount().add(rebate); + + // 🔥 新增:为 VIP 返利流水添加快照字段(可选,用于未来统计需求) + List vipReferralIds = findAnchorAndBusinessAtTime(userId); + + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + .amount(rebate) + .finalAmount(actualBalance) + .total(rebate) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.VIP_CONSUME_AWARD.getCode()) + .remark(TtAccountRecordSource.VIP_CONSUME_AWARD.getMsg()) + + // 🔥 新增字段:记录归属关系(统一规范) + .anchorId(vipReferralIds.get(0)) + .businessId(vipReferralIds.get(1)) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttUserBlendErcashMapper.insert(blendErcash); + } + } catch (Exception e) { + log.error("[VIP流水返利异常] userId={}, amount={}, source={}", userId, consumeAmount, source, e); + } + }, threadPoolTaskExecutor); + } + + /** + * 递归向上分发佣金,最多向上5层防止无限递归 + */ + private void distributeGameCommissionRecursive(Integer userId, BigDecimal consumeAmount, TtAccountRecordSource source, int depth) { + distributeGameCommissionRecursive(userId, consumeAmount, source, depth, userId); + } + + private void distributeGameCommissionRecursive(Integer userId, BigDecimal consumeAmount, TtAccountRecordSource source, int depth, Integer originalSubUserId) { + if (depth >= 5) return; + TtUser user = this.getById(userId); + if (user == null || user.getParentId() == null) return; + + TtUser parent = this.getById(user.getParentId()); + if (parent == null) return; + + // 招商账号改为周结,此处跳过实时到账,继续向上递归 + if (UserType.BUSINESS_USER.getCode().equals(parent.getUserType())) { + distributeGameCommissionRecursive(parent.getUserId(), consumeAmount, source, depth + 1, originalSubUserId); + return; + } + + // 根据游戏类型确定佣金比例和来源码 + BigDecimal rate = BigDecimal.ZERO; + TtAccountRecordSource commissionSource = TtAccountRecordSource.GAME_CONSUME_COMMISSION; + if (TtAccountRecordSource.isNewGameConsumeForCommission(source.getCode())) { + rate = parent.getNewGameCommissionRate() != null ? parent.getNewGameCommissionRate() : BigDecimal.ZERO; + commissionSource = TtAccountRecordSource.GAME_CONSUME_COMMISSION; + } else if (TtAccountRecordSource.isClassicGameConsumeForCommission(source.getCode())) { + rate = parent.getClassicGameCommissionRate() != null ? parent.getClassicGameCommissionRate() : BigDecimal.ZERO; + commissionSource = TtAccountRecordSource.CLASSIC_GAME_CONSUME_COMMISSION; + } + + if (rate.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal commission = consumeAmount.multiply(rate) + .divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP); + if (commission.compareTo(BigDecimal.ZERO) > 0) { + final Integer parentUserId = parent.getUserId(); + final BigDecimal rateForRemark = rate; + final TtAccountRecordSource srcForRemark = commissionSource; + + // 🔥 新增:计算当前用户(主播)的归属关系快照 + List commissionReferralIds = findAnchorAndBusinessAtTime(parentUserId); + + // 加金币积分与写流水在同一事务内,避免"钱已加、流水未记"的账目不一致 + // (本方法在异步线程池执行,无法继承调用方事务,需显式开启) + new TransactionTemplate(transactionManager).executeWithoutResult(status -> { + // 同时加金币和积分(与充值返佣 updateUserAccount → addAccountAmount 口径一致) + int count = userMapper.addAccountAmount(Long.valueOf(parentUserId), commission); + if (count > 0) { + // P2#15: 重新查询获取最新余额,避免使用旧快照 + TtUser latest = this.getById(parentUserId); + BigDecimal newAmount = latest.getAccountAmount(); + BigDecimal newCredits = latest.getAccountCredits(); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(parentUserId) + .amount(commission) + .finalAmount(newAmount) + .credits(commission) + .finalCredits(newCredits) + .total(commission) + .type(TtAccountRecordType.INPUT.getCode()) + .source(srcForRemark.getCode()) + .remark("subId:" + originalSubUserId + ",rate:" + rateForRemark + ",amount:" + consumeAmount + ",src:" + source.getCode()) + + // 🔥 新增字段:记录归属关系 + .anchorId(commissionReferralIds.get(0)) + .businessId(commissionReferralIds.get(1)) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttUserBlendErcashMapper.insert(blendErcash); + } + }); + } + } + + // 继续向上递归分发,保留原始消费者 ID + distributeGameCommissionRecursive(parent.getUserId(), consumeAmount, source, depth + 1, originalSubUserId); + } + + /** + * 🔥 新增:在流水产生时递归查找上级主播和招商 + * 用于防借流水风控 - 记录消费时的归属关系快照 + * @param userId 当前用户 ID(消费者) + * @return List: [主播 ID, 招商 ID] + */ + private List findAnchorAndBusinessAtTime(Integer userId) { + Integer currentId = userId; + Integer anchorId = null; + Integer businessId = null; + + // 从消费点开始向上递归,最多遍历 5 级 + for (int i = 0; i < 5 && currentId != null; ) { + if (currentId == null) break; + TtUser user = this.getById(currentId); + if (user == null || user.getParentId() == null) break; + + TtUser parent = this.getById(user.getParentId()); + if (parent == null) break; + + // 如果是主播,记录为主播 ID + if (UserType.ANCHOR.equals(parent.getUserType())) { + anchorId = parent.getUserId(); + // 继续向上找招商 + currentId = parent.getUserId(); + i++; + continue; + } + + // 如果是招商,记录为招商 ID + if (UserType.BUSINESS_USER.equals(parent.getUserType())) { + businessId = parent.getUserId(); + break; // 找到招商就停止 + } + + // 其他类型用户,继续向上 + currentId = parent.getUserId(); + i++; + } + + return new ArrayList<>(Arrays.asList(anchorId, businessId)); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserTransferDailyServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserTransferDailyServiceImpl.java new file mode 100644 index 0000000..c361f93 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtUserTransferDailyServiceImpl.java @@ -0,0 +1,231 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtUserTransferDailyMapper; +import com.ruoyi.admin.service.TtUserTransferDailyService; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.entity.transfer.TtUserTransferDaily; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; + +@Slf4j +@Service +public class TtUserTransferDailyServiceImpl extends ServiceImpl implements TtUserTransferDailyService { + + @Override + public TtUserTransferDaily getOrCreateToday(Integer userId, Integer vipLevel, Integer totalCount) { + LocalDate today = LocalDate.now(); + TtUserTransferDaily record = lambdaQuery() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, today) + .one(); + if (record != null) { + // VIP 升级时:如果配置的上限比当前记录高,则刷新转赠次数上限 + if (totalCount != null && totalCount > 0 + && (record.getTotalCount() == null || record.getTotalCount() < totalCount)) { + lambdaUpdate() + .eq(TtUserTransferDaily::getId, record.getId()) + .set(TtUserTransferDaily::getTotalCount, totalCount) + .update(); + record.setTotalCount(totalCount); + } + return record; + } + + record = TtUserTransferDaily.builder() + .userId(userId) + .vipLevel(vipLevel) + .statDate(today) + .totalCount(totalCount != null ? totalCount : 0) + .usedCount(0) + .pendingCount(0) + .deliveryTotalCount(0) + .deliveryUsedCount(0) + .deliveryPendingCount(0) + .createTime(DateUtils.getNowDate()) + .build(); + try { + save(record); + } catch (DuplicateKeyException e) { + // 并发场景下其他线程已创建,重新查询并尝试刷新 total_count + record = lambdaQuery() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, today) + .one(); + if (record != null && totalCount != null && totalCount > 0 + && (record.getTotalCount() == null || record.getTotalCount() < totalCount)) { + lambdaUpdate() + .eq(TtUserTransferDaily::getId, record.getId()) + .set(TtUserTransferDaily::getTotalCount, totalCount) + .update(); + record.setTotalCount(totalCount); + } + } + return record; + } + + @Override + public TtUserTransferDaily getOrCreateTodayWithDelivery(Integer userId, Integer vipLevel, Integer deliveryTotalCount) { + LocalDate today = LocalDate.now(); + TtUserTransferDaily record = lambdaQuery() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, today) + .one(); + if (record != null) { + // VIP 升级时:如果配置的上限比当前记录高,则刷新提货次数上限 + if (deliveryTotalCount != null && deliveryTotalCount > 0 + && (record.getDeliveryTotalCount() == null || record.getDeliveryTotalCount() < deliveryTotalCount)) { + lambdaUpdate() + .eq(TtUserTransferDaily::getId, record.getId()) + .set(TtUserTransferDaily::getDeliveryTotalCount, deliveryTotalCount) + .update(); + record.setDeliveryTotalCount(deliveryTotalCount); + } + return record; + } + + record = TtUserTransferDaily.builder() + .userId(userId) + .vipLevel(vipLevel) + .statDate(today) + .totalCount(0) + .usedCount(0) + .pendingCount(0) + .deliveryTotalCount(deliveryTotalCount != null ? deliveryTotalCount : 0) + .deliveryUsedCount(0) + .deliveryPendingCount(0) + .createTime(DateUtils.getNowDate()) + .build(); + try { + save(record); + } catch (DuplicateKeyException e) { + // 并发场景下其他线程已创建,重新查询并尝试刷新 delivery_total_count + record = lambdaQuery() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, today) + .one(); + if (record != null && deliveryTotalCount != null && deliveryTotalCount > 0 + && (record.getDeliveryTotalCount() == null || record.getDeliveryTotalCount() < deliveryTotalCount)) { + lambdaUpdate() + .eq(TtUserTransferDaily::getId, record.getId()) + .set(TtUserTransferDaily::getDeliveryTotalCount, deliveryTotalCount) + .update(); + record.setDeliveryTotalCount(deliveryTotalCount); + } + } + return record; + } + + @Override + public boolean preDeduct(Integer userId, Integer vipLevel, Integer count) { + if (count == null || count <= 0) { + return true; + } + LocalDate today = LocalDate.now(); + return lambdaUpdate() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, today) + .apply("total_count - used_count - pending_count >= {0}", count) + .setSql("pending_count = pending_count + " + count) + .update(); + } + + @Override + public boolean confirmUsed(Integer userId, Integer vipLevel, LocalDate statDate) { + LocalDate date = statDate != null ? statDate : LocalDate.now(); + return lambdaUpdate() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, date) + .setSql("pending_count = pending_count - 1") + .setSql("used_count = used_count + 1") + .update(); + } + + @Override + public boolean releasePending(Integer userId, Integer vipLevel, LocalDate statDate) { + LocalDate date = statDate != null ? statDate : LocalDate.now(); + return lambdaUpdate() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, date) + .setSql("pending_count = pending_count - 1") + .update(); + } + + @Override + public boolean consumeDirectly(Integer userId, Integer vipLevel, Integer count) { + if (count == null || count <= 0) { + return true; + } + LocalDate today = LocalDate.now(); + return lambdaUpdate() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, today) + .apply("total_count - used_count - pending_count >= {0}", count) + .setSql("used_count = used_count + " + count) + .update(); + } + + @Override + public boolean preDeductDelivery(Integer userId, Integer vipLevel, Integer count) { + if (count == null || count <= 0) { + return true; + } + LocalDate today = LocalDate.now(); + return lambdaUpdate() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, today) + .apply("delivery_total_count - delivery_used_count - delivery_pending_count >= {0}", count) + .setSql("delivery_pending_count = delivery_pending_count + " + count) + .update(); + } + + @Override + public boolean confirmUsedDelivery(Integer userId, Integer vipLevel, LocalDate statDate) { + LocalDate date = statDate != null ? statDate : LocalDate.now(); + return lambdaUpdate() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, date) + .setSql("delivery_pending_count = delivery_pending_count - 1") + .setSql("delivery_used_count = delivery_used_count + 1") + .update(); + } + + @Override + public boolean releasePendingDelivery(Integer userId, Integer vipLevel, LocalDate statDate) { + LocalDate date = statDate != null ? statDate : LocalDate.now(); + return lambdaUpdate() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, date) + .setSql("delivery_pending_count = delivery_pending_count - 1") + .update(); + } + + @Override + public boolean consumeDirectlyDelivery(Integer userId, Integer vipLevel, Integer count) { + if (count == null || count <= 0) { + return true; + } + LocalDate today = LocalDate.now(); + return lambdaUpdate() + .eq(TtUserTransferDaily::getUserId, userId) + .eq(TtUserTransferDaily::getVipLevel, vipLevel) + .eq(TtUserTransferDaily::getStatDate, today) + .apply("delivery_total_count - delivery_used_count - delivery_pending_count >= {0}", count) + .setSql("delivery_used_count = delivery_used_count + " + count) + .update(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtVipLevelServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtVipLevelServiceImpl.java new file mode 100644 index 0000000..27a8b0d --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtVipLevelServiceImpl.java @@ -0,0 +1,62 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtVipLevel; +import com.ruoyi.admin.mapper.TtVipLevelMapper; +import com.ruoyi.admin.mapper.WebsiteSetupMapper; +import com.ruoyi.admin.service.TtVipLevelService; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.utils.DateUtils; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class TtVipLevelServiceImpl extends ServiceImpl implements TtVipLevelService { + + private final WebsiteSetupMapper websiteSetupMapper; + + public TtVipLevelServiceImpl(WebsiteSetupMapper websiteSetupMapper) { + this.websiteSetupMapper = websiteSetupMapper; + } + + @Override + public String generateVipLevel(Integer num) { + if (num == null || num < 0) { + return "最高VIP等级不能为负数"; + } + // num 表示「本站最高VIP等级」,从 VIP0 到 VIPnum,共 num+1 条 + long currentCount = this.count(); + long target = num + 1L; + if (currentCount >= target) { + return "当前已存在 " + currentCount + " 个VIP等级,不低于设置值,无需生成;如需重新生成请先点击「重置」。"; + } + // i 从 currentCount 开始,到 num 为止,所以第一次生成会从 VIP0 开始 + for (int i = (int) currentCount; i <= num; i++) { + TtVipLevel ttVipLevel = TtVipLevel.builder().build(); + ttVipLevel.setName("VIP" + i); + ttVipLevel.setIcon(""); + ttVipLevel.setRechargeThreshold(BigDecimal.ZERO); + ttVipLevel.setCommissions(BigDecimal.ZERO); + ttVipLevel.setAddedBonus(BigDecimal.ZERO); + ttVipLevel.setFlowRebatePercent(BigDecimal.ZERO); + ttVipLevel.setTransferCount(0); + ttVipLevel.setDeliveryCount(0); + ttVipLevel.setCreateTime(DateUtils.getNowDate()); + this.save(ttVipLevel); + } + return ""; + } + + @Override + public String updateVipLevelById(TtVipLevel ttVipLevel) { + ttVipLevel.setIcon(RuoYiConfig.getDomainName() + ttVipLevel.getIcon()); + this.updateById(ttVipLevel); + return ""; + } + + @Override + public void truncateVipLevel() { + websiteSetupMapper.truncateTtVipLevel(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtWelfareMonthlyRechargesServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtWelfareMonthlyRechargesServiceImpl.java new file mode 100644 index 0000000..ed3c2dd --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtWelfareMonthlyRechargesServiceImpl.java @@ -0,0 +1,93 @@ +package com.ruoyi.admin.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.admin.mapper.TtWelfareMonthlyRechargesMapper; +import com.ruoyi.domain.other.TtWelfareMonthlyRecharges; +import com.ruoyi.admin.service.ITtWelfareMonthlyRechargesService; + +/** + * 每月充值福利Service业务层处理 + * + * @author ruoyi + * @date 2024-06-24 + */ +@Service +public class TtWelfareMonthlyRechargesServiceImpl implements ITtWelfareMonthlyRechargesService +{ + @Autowired + private TtWelfareMonthlyRechargesMapper ttWelfareMonthlyRechargesMapper; + + /** + * 查询每月充值福利 + * + * @param id 每月充值福利主键 + * @return 每月充值福利 + */ + @Override + public TtWelfareMonthlyRecharges selectTtWelfareMonthlyRechargesById(Integer id) + { + return ttWelfareMonthlyRechargesMapper.selectTtWelfareMonthlyRechargesById(id); + } + + /** + * 查询每月充值福利列表 + * + * @param ttWelfareMonthlyRecharges 每月充值福利 + * @return 每月充值福利 + */ + @Override + public List selectTtWelfareMonthlyRechargesList(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges) + { + return ttWelfareMonthlyRechargesMapper.selectTtWelfareMonthlyRechargesList(ttWelfareMonthlyRecharges); + } + + /** + * 新增每月充值福利 + * + * @param ttWelfareMonthlyRecharges 每月充值福利 + * @return 结果 + */ + @Override + public int insertTtWelfareMonthlyRecharges(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges) + { + return ttWelfareMonthlyRechargesMapper.insertTtWelfareMonthlyRecharges(ttWelfareMonthlyRecharges); + } + + /** + * 修改每月充值福利 + * + * @param ttWelfareMonthlyRecharges 每月充值福利 + * @return 结果 + */ + @Override + public int updateTtWelfareMonthlyRecharges(TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges) + { + return ttWelfareMonthlyRechargesMapper.updateTtWelfareMonthlyRecharges(ttWelfareMonthlyRecharges); + } + + /** + * 批量删除每月充值福利 + * + * @param ids 需要删除的每月充值福利主键 + * @return 结果 + */ + @Override + public int deleteTtWelfareMonthlyRechargesByIds(Integer[] ids) + { + return ttWelfareMonthlyRechargesMapper.deleteTtWelfareMonthlyRechargesByIds(ids); + } + + /** + * 删除每月充值福利信息 + * + * @param id 每月充值福利主键 + * @return 结果 + */ + @Override + public int deleteTtWelfareMonthlyRechargesById(Integer id) + { + return ttWelfareMonthlyRechargesMapper.deleteTtWelfareMonthlyRechargesById(id); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtWelfareServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtWelfareServiceImpl.java new file mode 100644 index 0000000..fc61571 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtWelfareServiceImpl.java @@ -0,0 +1,96 @@ +package com.ruoyi.admin.service.impl; + +import java.util.List; +import com.ruoyi.common.utils.DateUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.admin.mapper.TtWelfareMapper; +import com.ruoyi.domain.other.TtWelfare; +import com.ruoyi.admin.service.TtWelfareService; + +/** + * 福利列表Service业务层处理 + * + * @author ruoyi + * @date 2024-05-11 + */ +@Service +public class TtWelfareServiceImpl implements TtWelfareService +{ + @Autowired + private TtWelfareMapper ttWelfareMapper; + + /** + * 查询福利列表 + * + * @param welfareId 福利列表主键 + * @return 福利列表 + */ + @Override + public TtWelfare selectTtWelfareByWelfareId(Integer welfareId) + { + return ttWelfareMapper.selectTtWelfareByWelfareId(welfareId); + } + + /** + * 查询福利列表列表 + * + * @param ttWelfare 福利列表 + * @return 福利列表 + */ + @Override + public List selectTtWelfareList(TtWelfare ttWelfare) + { + return ttWelfareMapper.selectTtWelfareList(ttWelfare); + } + + /** + * 新增福利列表 + * + * @param ttWelfare 福利列表 + * @return 结果 + */ + @Override + public int insertTtWelfare(TtWelfare ttWelfare) + { + ttWelfare.setCreateTime(DateUtils.getNowDate()); + return ttWelfareMapper.insertTtWelfare(ttWelfare); + } + + /** + * 修改福利列表 + * + * @param ttWelfare 福利列表 + * @return 结果 + */ + @Override + public int updateTtWelfare(TtWelfare ttWelfare) + { + ttWelfare.setUpdateTime(DateUtils.getNowDate()); + return ttWelfareMapper.updateTtWelfare(ttWelfare); + } + + /** + * 批量删除福利列表 + * + * @param welfareIds 需要删除的福利列表主键 + * @return 结果 + */ + @Override + public int deleteTtWelfareByWelfareIds(Integer[] welfareIds) + { + return ttWelfareMapper.deleteTtWelfareByWelfareIds(welfareIds); + } + + /** + * 删除福利列表信息 + * + * @param welfareId 福利列表主键 + * @return 结果 + */ + @Override + public int deleteTtWelfareByWelfareId(Integer welfareId) + { + return ttWelfareMapper.deleteTtWelfareByWelfareId(welfareId); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtYyOrnamentsServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtYyOrnamentsServiceImpl.java new file mode 100644 index 0000000..37299cd --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TtYyOrnamentsServiceImpl.java @@ -0,0 +1,11 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtYYOrnamentsMapper; +import com.ruoyi.admin.service.TtYyOrnamentsService; +import com.ruoyi.domain.other.TtYYOrnaments; +import org.springframework.stereotype.Service; + +@Service +public class TtYyOrnamentsServiceImpl extends ServiceImpl implements TtYyOrnamentsService { +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TurnoverLotteryServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TurnoverLotteryServiceImpl.java new file mode 100644 index 0000000..f37c536 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TurnoverLotteryServiceImpl.java @@ -0,0 +1,581 @@ +package com.ruoyi.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.mapper.TurnoverLotteryErcashMapper; +import com.ruoyi.admin.mapper.TurnoverLotteryMapper; +import com.ruoyi.admin.mapper.TurnoverLotteryPrizeMapper; +import com.ruoyi.admin.mapper.TurnoverLotteryRecordMapper; +import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TurnoverLotteryService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.message.service.MessageService; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.lottery.TtTurnoverLottery; +import com.ruoyi.domain.entity.lottery.TtTurnoverLotteryPrize; +import com.ruoyi.domain.entity.lottery.TtTurnoverLotteryRecord; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import com.ruoyi.domain.vo.lottery.TurnoverLotteryWinVO; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +/** + * 流水抽奖服务实现 + */ +@Slf4j +@Service +public class TurnoverLotteryServiceImpl implements TurnoverLotteryService { + + @Autowired + private TurnoverLotteryMapper lotteryMapper; + + @Autowired + private TurnoverLotteryPrizeMapper prizeMapper; + + @Autowired + private TurnoverLotteryRecordMapper recordMapper; + + @Autowired + private TurnoverLotteryErcashMapper ercashMapper; + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private TtOrnamentsLevelMapper ornamentsLevelMapper; + + @Autowired + private MessageService messageService; + + @Override + @Transactional + public R savePrizes(Long lotteryId, List> prizeList) { + try { + // 0. 先校验参数(在删除前校验,避免"先删后插"中途失败清空奖品) + if (prizeList != null && !prizeList.isEmpty()) { + for (Map prizeMap : prizeList) { + int initialReal = prizeMap.get("initialRealCount") != null ? + ((Number) prizeMap.get("initialRealCount")).intValue() : + (prizeMap.get("realCount") != null ? ((Number) prizeMap.get("realCount")).intValue() : 10); + if (initialReal <= 0) { + return R.fail("奖品真实数量必须大于0"); + } + } + } + + // 1. 删除原有奖品 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtTurnoverLotteryPrize::getLotteryId, lotteryId); + prizeMapper.delete(wrapper); + + // 2. 插入新奖品 + if (prizeList != null && !prizeList.isEmpty()) { + for (Map prizeMap : prizeList) { + TtTurnoverLotteryPrize prize = new TtTurnoverLotteryPrize(); + prize.setLotteryId(lotteryId); + prize.setOrnamentId(prizeMap.get("ornamentId") != null ? + ((Number) prizeMap.get("ornamentId")).longValue() : null); + prize.setOrnamentName(prizeMap.get("ornamentName") != null ? + prizeMap.get("ornamentName").toString() : null); + prize.setImageUrl(prizeMap.get("imageUrl") != null ? + prizeMap.get("imageUrl").toString() : null); + prize.setPrice(prizeMap.get("price") != null ? + new BigDecimal(prizeMap.get("price").toString()) : BigDecimal.ZERO); + prize.setOrnamentLevel(prizeMap.get("ornamentLevel") != null ? + ((Number) prizeMap.get("ornamentLevel")).intValue() : null); + prize.setExteriorName(prizeMap.get("exteriorName") != null ? + prizeMap.get("exteriorName").toString() : null); + int initialReal = prizeMap.get("initialRealCount") != null ? + ((Number) prizeMap.get("initialRealCount")).intValue() : + (prizeMap.get("realCount") != null ? ((Number) prizeMap.get("realCount")).intValue() : 10); + prize.setInitialRealCount(initialReal); + prize.setRealCount(initialReal); + prize.setDisplayCount(prizeMap.get("displayCount") != null ? + ((Number) prizeMap.get("displayCount")).intValue() : 10); + prize.setCreateTime(new java.util.Date()); + + prizeMapper.insert(prize); + } + } + + return R.ok("保存成功"); + } catch (Exception e) { + log.error("保存奖品失败", e); + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return R.fail("保存失败: " + e.getMessage()); + } + } + + @Override + @Transactional + public R toggleStatus(Long lotteryId, Integer status) { + try { + // 开启时先关闭其他活动,保证只有一个开启 + if (status != null && status == 1) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.ne(TtTurnoverLottery::getId, lotteryId) + .eq(TtTurnoverLottery::getStatus, 1); + List activeList = lotteryMapper.selectList(queryWrapper); + Date now = new java.util.Date(); + for (TtTurnoverLottery other : activeList) { + TtTurnoverLottery update = new TtTurnoverLottery(); + update.setId(other.getId()); + update.setStatus(0); + update.setUpdateTime(now); + lotteryMapper.updateById(update); + } + } + + TtTurnoverLottery lottery = new TtTurnoverLottery(); + lottery.setId(lotteryId); + lottery.setStatus(status); + lottery.setUpdateTime(new java.util.Date()); + + lotteryMapper.updateById(lottery); + return R.ok("状态更新成功"); + } catch (Exception e) { + log.error("更新状态失败", e); + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return R.fail("更新失败: " + e.getMessage()); + } + } + + @Override + public BigDecimal getAvailableTurnover(Integer userId) { + try { + Date todayStart = getTodayStart(); + Date todayEnd = new Date(); + + BigDecimal totalTurnover = getTodayNetTurnover(userId, todayStart, todayEnd); + + // 已使用流水从抽奖记录表统计(抽奖不扣余额,只扣流水额度) + BigDecimal usedTurnover = recordMapper.sumTodayUsedTurnover(userId, todayStart, todayEnd); + + BigDecimal availableTurnover = totalTurnover.subtract(usedTurnover); + return availableTurnover.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : availableTurnover; + } catch (Exception e) { + log.error("获取用户可用流水失败", e); + return BigDecimal.ZERO; + } + } + + /** + * 自然日边界按北京时间(Asia/Shanghai)计算,避免 JVM 默认时区(如 UTC)导致"今日流水"窗口偏移。 + */ + private Date getTodayStart() { + ZoneId zone = ZoneId.of("Asia/Shanghai"); + return Date.from(LocalDate.now(zone).atStartOfDay(zone).toInstant()); + } + + /** + * 计算用户当日有效净流水 = 各游戏消费毛额 − 疯狂转盘注单退款。 + * 疯狂转盘存在兜底退款(WHEEL_BET_REFUND,source=64,INPUT),退款不冲减已计入的 OUTPUT 消费流水, + * 若不扣除会导致"下注→退款→再下注"重复累计流水资格,故此处显式扣减,结果不为负。 + */ + private BigDecimal getTodayNetTurnover(Integer userId, Date todayStart, Date todayEnd) { + Set validSources = TtAccountRecordSource.getTurnoverLotteryCodes(); + BigDecimal grossTurnover = ercashMapper.sumTodayTurnover( + userId, new ArrayList<>(validSources), + TtAccountRecordType.OUTPUT.getCode(), todayStart, todayEnd); + BigDecimal wheelRefund = ercashMapper.sumTodayWheelRefund( + userId, TtAccountRecordSource.WHEEL_BET_REFUND.getCode(), + TtAccountRecordType.INPUT.getCode(), todayStart, todayEnd); + BigDecimal netTurnover = grossTurnover.subtract(wheelRefund); + return netTurnover.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : netTurnover; + } + + @Override + public R> getUserStats(Integer userId) { + try { + // 获取今日起止时间 + Date todayStart = getTodayStart(); + Date todayEnd = new Date(); // 当前时间作为结束 + + // 查询今日符合条件的流水总额 (消费) - 已扣除疯狂转盘注单退款 + BigDecimal totalTurnover = getTodayNetTurnover(userId, todayStart, todayEnd); + + // 查询今日已使用的流水 (抽奖消耗) - 从抽奖记录表统计,不写入余额/积分流水 + BigDecimal usedTurnover = recordMapper.sumTodayUsedTurnover(userId, todayStart, todayEnd); + + // 可用流水 = 总流水 - 已使用流水 + BigDecimal availableTurnover = totalTurnover.subtract(usedTurnover); + if (availableTurnover.compareTo(BigDecimal.ZERO) < 0) { + availableTurnover = BigDecimal.ZERO; + } + + // 获取所有开启的抽奖活动 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtTurnoverLottery::getStatus, 1); + wrapper.orderByAsc(TtTurnoverLottery::getSort, TtTurnoverLottery::getId); + List lotteryList = lotteryMapper.selectList(wrapper); + + // 批量获取等级图片 + List levelList = ornamentsLevelMapper.selectList(null); + Map levelImgMap = levelList.stream() + .collect(Collectors.toMap(TtOrnamentsLevel::getId, TtOrnamentsLevel::getLevelImg, (a, b) -> a)); + + // 构建用户端抽奖宝箱列表(只保留必要字段 + 奖品信息) + List> boxList = new ArrayList<>(); + for (TtTurnoverLottery lottery : lotteryList) { + Map box = new HashMap<>(); + box.put("id", lottery.getId()); + box.put("lotteryName", lottery.getLotteryName()); + box.put("status", lottery.getStatus()); + box.put("turnoverThreshold", lottery.getTurnoverThreshold()); + box.put("sort", lottery.getSort()); + + // 该宝箱的可抽奖次数 = 可用流水 / 该宝箱流水门槛 + int boxThreshold = lottery.getTurnoverThreshold() != null ? lottery.getTurnoverThreshold() : 3000; + if (boxThreshold <= 0) { + // 门槛配置异常,跳过该宝箱,避免除零 + continue; + } + int lotteryChances = availableTurnover.divideToIntegralValue(new BigDecimal(boxThreshold)).intValue(); + box.put("lotteryChances", lotteryChances); + + // 查询并计算奖品爆率 + LambdaQueryWrapper prizeWrapper = new LambdaQueryWrapper<>(); + prizeWrapper.eq(TtTurnoverLotteryPrize::getLotteryId, lottery.getId()); + prizeWrapper.orderByAsc(TtTurnoverLotteryPrize::getSort, TtTurnoverLotteryPrize::getId); + List prizeList = prizeMapper.selectList(prizeWrapper); + calculateRates(prizeList); + + // 计算奖品总价值(基于初始真实数量) + BigDecimal totalValue = prizeList.stream() + .map(p -> { + BigDecimal price = p.getPrice() != null ? p.getPrice() : BigDecimal.ZERO; + int count = p.getInitialRealCount() != null ? p.getInitialRealCount() + : (p.getRealCount() != null ? p.getRealCount() : 0); + return price.multiply(new BigDecimal(count)); + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + box.put("totalValue", totalValue); + + List> prizes = new ArrayList<>(); + for (TtTurnoverLotteryPrize prize : prizeList) { + Map p = new HashMap<>(); + p.put("id", prize.getId()); + p.put("ornamentName", prize.getOrnamentName()); + p.put("price", prize.getPrice()); + p.put("imageUrl", prize.getImageUrl()); + p.put("ornamentLevel", prize.getOrnamentLevel()); + p.put("exteriorName", prize.getExteriorName()); + p.put("levelImg", levelImgMap.get(prize.getOrnamentLevel())); + p.put("displayPercent", prize.getDisplayPercent()); + p.put("realPercent", prize.getRealPercent()); + p.put("displayCount", prize.getDisplayCount()); + p.put("realCount", prize.getRealCount()); + p.put("sort", prize.getSort()); + prizes.add(p); + } + box.put("prizes", prizes); + boxList.add(box); + } + + Map result = new HashMap<>(); + result.put("totalTurnover", totalTurnover); + result.put("usedTurnover", usedTurnover); + result.put("availableTurnover", availableTurnover); + result.put("lotteryList", boxList); + + return R.ok(result); + } catch (Exception e) { + log.error("获取用户统计失败", e); + return R.fail("获取统计失败: " + e.getMessage()); + } + } + + @Override + @Transactional + public R> draw(Integer userId, Long lotteryId) { + try { + // 1. 获取抽奖活动 + TtTurnoverLottery lottery = lotteryMapper.selectById(lotteryId); + if (lottery == null || lottery.getStatus() != 1) { + return R.fail("抽奖活动不存在或未开启"); + } + + // 2. 检查用户是否有抽奖资格(仅查询可用流水,不重复计算活动/奖品) + BigDecimal availableTurnover = getAvailableTurnover(userId); + int requiredTurnover = lottery.getTurnoverThreshold() != null ? lottery.getTurnoverThreshold() : 3000; + if (requiredTurnover <= 0) { + return R.fail("抽奖流水门槛配置错误,请联系管理员"); + } + int lotteryChances = availableTurnover.divideToIntegralValue(new BigDecimal(requiredTurnover)).intValue(); + if (lotteryChances <= 0) { + return R.fail("抽奖次数不足"); + } + + // 3. 获取奖品列表 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtTurnoverLotteryPrize::getLotteryId, lotteryId); + List prizeList = prizeMapper.selectList(wrapper); + + if (prizeList == null || prizeList.isEmpty()) { + return R.fail("奖品配置为空"); + } + + // 4. 校验并预重置奖池:如果当前剩余全部耗尽,按初始真实数量重置 + boolean allEmpty = prizeList.stream() + .allMatch(p -> (p.getRealCount() != null ? p.getRealCount() : 0) <= 0); + if (allEmpty) { + for (TtTurnoverLotteryPrize prize : prizeList) { + int refill = prize.getInitialRealCount() != null ? prize.getInitialRealCount() + : (prize.getDisplayCount() != null ? prize.getDisplayCount() : 0); + prize.setRealCount(refill); + prize.setUpdateTime(new java.util.Date()); + prizeMapper.updateById(prize); + } + } + + // 5. 再次校验总库存,防止所有奖品 initialRealCount=0 时空发 + int totalRealCount = prizeList.stream() + .mapToInt(p -> p.getRealCount() != null ? p.getRealCount() : 0) + .sum(); + if (totalRealCount <= 0) { + return R.fail("奖品库存不足"); + } + + // 6. 根据当前剩余权重随机抽取奖品 + TtTurnoverLotteryPrize winningPrize = drawPrize(prizeList); + if (winningPrize == null || winningPrize.getRealCount() == null || winningPrize.getRealCount() <= 0) { + return R.fail("奖品库存不足"); + } + + // 7. 原子扣减当前剩余库存(数据库层兜底:即使 Redis 锁失效也不会超发) + int updatedRows = prizeMapper.decrementStock(winningPrize.getId()); + if (updatedRows <= 0) { + // 库存已耗尽或并发竞争失败:未扣流水、未发奖 + return R.fail("奖品库存不足"); + } + + // 8. 记录抽奖记录 + TtTurnoverLotteryRecord record = new TtTurnoverLotteryRecord(); + record.setUserId(userId); + record.setLotteryId(lotteryId); + record.setPrizeId(winningPrize.getId()); + record.setOrnamentId(winningPrize.getOrnamentId()); + record.setOrnamentName(winningPrize.getOrnamentName()); + record.setImageUrl(winningPrize.getImageUrl()); + record.setOrnamentsPrice(winningPrize.getPrice()); + record.setCostTurnover(requiredTurnover); + record.setCreateTime(new java.util.Date()); + + recordMapper.insert(record); + + // 9. 发放饰品到背包 + TtBoxRecords boxRecord = TtBoxRecords.builder() + .userId(userId) + .boxId(lotteryId.intValue()) + .boxName(lottery.getLotteryName()) + .boxPrice(new BigDecimal(requiredTurnover)) + .ornamentId(winningPrize.getOrnamentId()) + .ornamentName(winningPrize.getOrnamentName()) + .imageUrl(winningPrize.getImageUrl()) + .exteriorName(winningPrize.getExteriorName()) + .ornamentsPrice(winningPrize.getPrice()) + .holderUserId(userId) + .source(TtboxRecordSource.TURNOVER_LOTTERY.getCode()) + .status(TtboxRecordStatus.IN_PACKSACK_ON.getCode()) + .createTime(new java.util.Date()) + .updateTime(new java.util.Date()) + .build(); + + boxRecordsService.save(boxRecord); + + // 10. 发送活动抽奖邮件通知(事务提交后发送,失败不回滚抽奖结果) + sendDrawNoticeAfterCommit(userId, lotteryId, lottery, winningPrize); + + // 11. 返回抽奖结果,前端用于播放动画 + Map result = new HashMap<>(); + result.put("prizeId", winningPrize.getId()); + // 背包记录 id:前端调用背包分解接口 /api/userPackSack/decompose 时作为 packSackIds 传入 + result.put("boxRecordId", boxRecord.getId()); + result.put("ornamentName", winningPrize.getOrnamentName()); + result.put("price", winningPrize.getPrice()); + result.put("imageUrl", winningPrize.getImageUrl()); + result.put("ornamentLevel", winningPrize.getOrnamentLevel()); + result.put("exteriorName", winningPrize.getExteriorName()); + result.put("lotteryName", lottery.getLotteryName()); + result.put("costTurnover", requiredTurnover); + result.put("msg", "抽奖成功!获得: " + winningPrize.getOrnamentName()); + + return R.ok(result); + } catch (Exception e) { + log.error("抽奖失败", e); + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return R.fail("系统繁忙,请稍后重试"); + } + } + + @Override + public R> getUserRecords(Integer userId, Long lotteryId, Integer pageNum, Integer pageSize) { + try { + int current = (pageNum == null || pageNum < 1) ? 1 : pageNum; + int size = (pageSize == null || pageSize < 1) ? 10 : Math.min(pageSize, 100); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtTurnoverLotteryRecord::getUserId, userId); + if (lotteryId != null) { + wrapper.eq(TtTurnoverLotteryRecord::getLotteryId, lotteryId); + } + wrapper.orderByDesc(TtTurnoverLotteryRecord::getCreateTime); + + IPage page = recordMapper.selectPage(new Page<>(current, size), wrapper); + + Map result = new HashMap<>(); + result.put("total", page.getTotal()); + result.put("pageNum", current); + result.put("pageSize", size); + result.put("rows", page.getRecords()); + return R.ok(result); + } catch (Exception e) { + log.error("查询用户抽奖记录失败", e); + return R.fail("查询失败,请稍后重试"); + } + } + + @Override + public R> getGlobalRecords(Long lotteryId, BigDecimal minPrice, Integer pageNum, Integer pageSize) { + try { + int current = (pageNum == null || pageNum < 1) ? 1 : pageNum; + int size = (pageSize == null || pageSize < 1) ? 20 : Math.min(pageSize, 100); + int offset = (current - 1) * size; + + List rows = + recordMapper.selectGlobalWinPage(lotteryId, minPrice, offset, size); + Long total = recordMapper.countGlobalWin(lotteryId, minPrice); + + Map result = new HashMap<>(); + result.put("total", total != null ? total : 0L); + result.put("pageNum", current); + result.put("pageSize", size); + result.put("rows", rows != null ? rows : new ArrayList<>()); + return R.ok(result); + } catch (Exception e) { + log.error("查询全服中奖记录失败", e); + return R.fail("查询失败,请稍后重试"); + } + } + + /** + * 根据真实数量权重随机抽取奖品 + */ + private TtTurnoverLotteryPrize drawPrize(List prizeList) { + // 使用 realCount 作为实际权重,null 按 0 处理 + int totalRealCount = prizeList.stream() + .mapToInt(p -> p.getRealCount() != null ? p.getRealCount() : 0) + .sum(); + + if (totalRealCount <= 0) { + return null; + } + + int random = ThreadLocalRandom.current().nextInt(totalRealCount); + int currentCount = 0; + + for (TtTurnoverLotteryPrize prize : prizeList) { + int realCount = prize.getRealCount() != null ? prize.getRealCount() : 0; + currentCount += realCount; + if (random < currentCount) { + return prize; + } + } + + return null; + } + + /** + * 计算两列爆率 (显示/真实) + */ + private void calculateRates(List prizeList) { + if (prizeList == null || prizeList.isEmpty()) { + return; + } + + // 1. 计算显示爆率 (基于展示数量 displayCount) + int totalDisplayCount = prizeList.stream() + .mapToInt(p -> p.getDisplayCount() != null ? p.getDisplayCount() : 0) + .sum(); + + if (totalDisplayCount > 0) { + for (TtTurnoverLotteryPrize prize : prizeList) { + int dc = prize.getDisplayCount() != null ? prize.getDisplayCount() : 0; + double displayRate = (double) dc / totalDisplayCount * 100; + prize.setTheoreticalRate(Math.round(displayRate * 100.0) / 100.0); + prize.setDisplayPercent(String.format("%.2f%%", displayRate)); + } + } + + // 2. 计算真实爆率 (基于真实数量 realCount) + int totalRealCount = prizeList.stream() + .mapToInt(p -> p.getRealCount() != null ? p.getRealCount() : 0) + .sum(); + + if (totalRealCount > 0) { + for (TtTurnoverLotteryPrize prize : prizeList) { + int rc = prize.getRealCount() != null ? prize.getRealCount() : 0; + double realRate = (double) rc / totalRealCount * 100; + prize.setRealRate(Math.round(realRate * 100.0) / 100.0); + prize.setRealPercent(String.format("%.2f%%", realRate)); + } + } + } + + /** + * 事务提交后发送抽奖中奖通知。 + * 通知属于旁路触达,失败仅记日志,绝不回滚抽奖结果(扣库存/写记录/入背包)。 + */ + private void sendDrawNoticeAfterCommit(Integer userId, Long lotteryId, + TtTurnoverLottery lottery, TtTurnoverLotteryPrize winningPrize) { + final String prizePriceStr = winningPrize.getPrice() != null + ? winningPrize.getPrice().stripTrailingZeros().toPlainString() + : "0"; + final String content = "恭喜您在“" + lottery.getLotteryName() + "”活动中抽中【" + + winningPrize.getOrnamentName() + "】(价值 ¥" + prizePriceStr + ")。"; + final Long noticeUserId = Long.valueOf(userId); + final String title = MessageCategoryEnum.SYS_EVENT_LOTTERY.getTitle(); + final String category = MessageCategoryEnum.SYS_EVENT_LOTTERY.getCode(); + + Runnable send = () -> { + try { + messageService.sendNotice(noticeUserId, title, content, category); + } catch (Exception ex) { + log.error("抽奖通知发送失败 userId={} lotteryId={}", userId, lotteryId, ex); + } + }; + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + send.run(); + } + }); + } else { + send.run(); + } + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/WebsitePropertyServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/WebsitePropertyServiceImpl.java new file mode 100644 index 0000000..1422393 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/WebsitePropertyServiceImpl.java @@ -0,0 +1,76 @@ +package com.ruoyi.admin.service.impl; + +import cn.hutool.core.util.IdUtil; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.admin.service.WebsitePropertyService; +import com.ruoyi.domain.vo.WebsitePropertyDataVO; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +public class WebsitePropertyServiceImpl implements WebsitePropertyService { + + private final TtOrnamentMapper ornamentsMapper; + + public WebsitePropertyServiceImpl(TtOrnamentMapper ornamentsMapper) { + this.ornamentsMapper = ornamentsMapper; + } + + @Override + public List list() { + return ornamentsMapper.list(); + } + + @Override + public WebsitePropertyDataVO getById(Integer id) { + TtOrnament ornaments = new LambdaQueryChainWrapper<>(ornamentsMapper) + .eq(TtOrnament::getId, id) + // .eq(TtOrnament::getIsProprietaryProperty, "0") + .one(); + WebsitePropertyDataVO websitePropertyDataVO = WebsitePropertyDataVO.builder().build(); + BeanUtils.copyBeanProp(websitePropertyDataVO, ornaments); + return websitePropertyDataVO; + } + + @Override + public String save(WebsitePropertyDataVO websitePropertyDataVO) { + if (StringUtils.isEmpty(websitePropertyDataVO.getImageUrl())) websitePropertyDataVO.setImageUrl(""); + else websitePropertyDataVO.setImageUrl(RuoYiConfig.getDomainName() + websitePropertyDataVO.getImageUrl()); + websitePropertyDataVO.setCreateTime(DateUtils.getNowDate()); + websitePropertyDataVO.setIsPutaway("1"); + websitePropertyDataVO.setIsProprietaryProperty("0"); + TtOrnament ttOrnament = TtOrnament.builder().build(); + BeanUtils.copyBeanProp(ttOrnament, websitePropertyDataVO); + ttOrnament.setMarketHashName(UUID.randomUUID().toString()); + ttOrnament.setId(IdUtil.getSnowflake().nextId() % 1000000000L); + ttOrnament.setShortName(websitePropertyDataVO.getName()); + ornamentsMapper.insert(ttOrnament); + return ""; + } + + @Override + public String updateWebsitePropertyById(WebsitePropertyDataVO websitePropertyDataVO) { + websitePropertyDataVO.setUpdateTime(DateUtils.getNowDate()); + String imageUrl = websitePropertyDataVO.getImageUrl(); + websitePropertyDataVO.setImageUrl(RuoYiConfig.getDomainName() + imageUrl); + // TtOrnament ttOrnament = TtOrnament.builder().build(); + // BeanUtils.copyBeanProp(ttOrnament, websitePropertyDataVO); + // ornamentsMapper.updateById(ttOrnament); + ornamentsMapper.updateWebsiteProperty(websitePropertyDataVO); + return ""; + } + + @Override + public int deleteWebsitePropertyByIds(Integer[] ids) { + return ornamentsMapper.deleteWebsitePropertyByIds(ids); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/WebsiteSetupServiceImpl.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/WebsiteSetupServiceImpl.java new file mode 100644 index 0000000..8b1dca9 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/WebsiteSetupServiceImpl.java @@ -0,0 +1,274 @@ +package com.ruoyi.admin.service.impl; + +import com.ruoyi.domain.other.ConfigData; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.WebsiteSetupMapper; +import com.ruoyi.admin.service.ConfigService; +import com.ruoyi.admin.service.WebsiteSetupService; +import com.ruoyi.domain.other.OperationalStatistics; +import com.ruoyi.domain.other.ParameterSettingBody; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.admin.event.MaintenanceEnabledEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class WebsiteSetupServiceImpl implements WebsiteSetupService { + + private final ConfigService configService; + private final ISysConfigService iSysConfigService; + private final WebsiteSetupMapper websiteSetupMapper; + private final TtOrnamentMapper ttOrnamentMapper; + private final ApplicationEventPublisher eventPublisher; + + public WebsiteSetupServiceImpl(ConfigService configService, + ISysConfigService iSysConfigService, + WebsiteSetupMapper websiteSetupMapper, + TtOrnamentMapper ttOrnamentMapper, + ApplicationEventPublisher eventPublisher) { + this.configService = configService; + this.iSysConfigService = iSysConfigService; + this.websiteSetupMapper = websiteSetupMapper; + this.ttOrnamentMapper = ttOrnamentMapper; + this.eventPublisher = eventPublisher; + } + + @Override + public List getOperationalStatistics() { + return websiteSetupMapper.getOperationalStatistics(); + } + + @Override + public ParameterSettingBody getParameterSetting() { + List parameterSettingList = websiteSetupMapper.selectParameterSettingList(); + Map> parameterSettingMap = parameterSettingList.stream() + .collect(Collectors.groupingBy(ConfigData::getConfigKey)); + ParameterSettingBody parameterSetting = ParameterSettingBody.builder().build(); + parameterSetting.setUsePricePremiumRate(getConfigValue(parameterSettingMap, "usePricePremiumRate")); + parameterSetting.setExchangePriceRatio(getConfigValue(parameterSettingMap, "exchangePriceRatio")); + parameterSetting.setRegisterRedPacket(getConfigValue(parameterSettingMap, "registerRedPacket")); + parameterSetting.setMaxCompoundNum(getConfigValue(parameterSettingMap, "maxCompoundNum")); + parameterSetting.setCompoundMinPrice(getConfigValue(parameterSettingMap, "compoundMinPrice")); + parameterSetting.setCompoundMinPremiumRate(getConfigValue(parameterSettingMap, "compoundMinPremiumRate")); + parameterSetting.setCompoundMaxPremiumRate(getConfigValue(parameterSettingMap, "compoundMaxPremiumRate")); + parameterSetting.setZBTParities(getConfigValue(parameterSettingMap, "ZBTParities")); + parameterSetting.setAutoDeliveryMinPrice(getConfigValue(parameterSettingMap, "autoDeliveryMinPrice")); + parameterSetting.setBuyPricePremiumRate(getConfigValue(parameterSettingMap, "buyPricePremiumRate")); + parameterSetting.setWebsiteMaintenance(getConfigValue(parameterSettingMap, "websiteMaintenance")); + parameterSetting.setShoppingMaintenance(getConfigValue(parameterSettingMap, "shoppingMaintenance")); + parameterSetting.setFightMaintenance(getConfigValue(parameterSettingMap, "fightMaintenance")); + parameterSetting.setBindBoxMaintenance(getConfigValue(parameterSettingMap, "bindBoxMaintenance")); + parameterSetting.setRollMaintenance(getConfigValue(parameterSettingMap, "rollMaintenance")); + parameterSetting.setCompoundMaintenance(getConfigValue(parameterSettingMap, "compoundMaintenance")); + parameterSetting.setWesternDuelMaintenance(getConfigValue(parameterSettingMap, "westernDuelMaintenance")); + parameterSetting.setLuckyUpgradeMaintenance(getConfigValue(parameterSettingMap, "luckyUpgradeMaintenance")); + parameterSetting.setWheelMaintenance(getConfigValue(parameterSettingMap, "wheelMaintenance")); + // 玫瑰女郎维护配置(对应键 olympusMaintenance,可能不存在,安全读取,兜底 "0") + List olympusMaintenanceList = parameterSettingMap.get("olympusMaintenance"); + if (olympusMaintenanceList != null && !olympusMaintenanceList.isEmpty()) { + parameterSetting.setOlympusMaintenance(olympusMaintenanceList.get(0).getConfigValue()); + } else { + parameterSetting.setOlympusMaintenance("0"); + } + // 充值渠道维护配置(可能不存在,安全读取,兜底 "0") + List rechargeMaintenanceList = parameterSettingMap.get("rechargeMaintenance"); + if (rechargeMaintenanceList != null && !rechargeMaintenanceList.isEmpty()) { + parameterSetting.setRechargeMaintenance(rechargeMaintenanceList.get(0).getConfigValue()); + } else { + parameterSetting.setRechargeMaintenance("0"); + } + // 卡密充值维护配置(可能不存在,安全读取,兜底 "0") + List cardRechargeMaintenanceList = parameterSettingMap.get("cardRechargeMaintenance"); + if (cardRechargeMaintenanceList != null && !cardRechargeMaintenanceList.isEmpty()) { + parameterSetting.setCardRechargeMaintenance(cardRechargeMaintenanceList.get(0).getConfigValue()); + } else { + parameterSetting.setCardRechargeMaintenance("0"); + } + // 推广后台维护配置(可能不存在,安全读取,兜底 "0") + List promoMaintenanceList = parameterSettingMap.get("promoMaintenance"); + if (promoMaintenanceList != null && !promoMaintenanceList.isEmpty()) { + parameterSetting.setPromoMaintenance(promoMaintenanceList.get(0).getConfigValue()); + } else { + parameterSetting.setPromoMaintenance("0"); + } + // 支付公司配置(可能不存在,安全读取) + List payCompanyList = parameterSettingMap.get("payCompany"); + if (payCompanyList != null && !payCompanyList.isEmpty()) { + parameterSetting.setPayCompany(payCompanyList.get(0).getConfigValue()); + } else { + parameterSetting.setPayCompany("chengduhipay"); + } + // 支付公司权重配置(可能不存在,安全读取) + List payCompanyWeightsList = parameterSettingMap.get("payCompanyWeights"); + if (payCompanyWeightsList != null && !payCompanyWeightsList.isEmpty()) { + parameterSetting.setPayCompanyWeights(payCompanyWeightsList.get(0).getConfigValue()); + } else { + parameterSetting.setPayCompanyWeights("[{\"name\":\"chengduhipay\",\"weight\":10}]"); + } + // 流水归档保留天数配置(可能不存在,安全读取,兜底 "7") + List flowArchiveRetainDaysList = parameterSettingMap.get("flowArchiveRetainDays"); + if (flowArchiveRetainDaysList != null && !flowArchiveRetainDaysList.isEmpty()) { + parameterSetting.setFlowArchiveRetainDays(flowArchiveRetainDaysList.get(0).getConfigValue()); + } else { + parameterSetting.setFlowArchiveRetainDays("7"); + } + // SnapPay 开关配置(可能不存在,安全读取,兜底 "1" 启用) + List snappayEnabledList = parameterSettingMap.get("snappayEnabled"); + if (snappayEnabledList != null && !snappayEnabledList.isEmpty()) { + parameterSetting.setSnappayEnabled(snappayEnabledList.get(0).getConfigValue()); + } else { + parameterSetting.setSnappayEnabled("1"); + } + return parameterSetting; + } + + @Override + public String updateParameterSetting(ParameterSettingBody parameterSettingBody) { + String usePricePremiumRate = parameterSettingBody.getUsePricePremiumRate(), + exchangePriceRatio = parameterSettingBody.getExchangePriceRatio(), + registerRedPacket = parameterSettingBody.getRegisterRedPacket(), + maxCompoundNum = parameterSettingBody.getMaxCompoundNum(), + compoundMinPrice = parameterSettingBody.getCompoundMinPrice(), + compoundMinPremiumRate = parameterSettingBody.getCompoundMinPremiumRate(), + compoundMaxPremiumRate = parameterSettingBody.getCompoundMaxPremiumRate(), + zbtParities = parameterSettingBody.getZBTParities(), + autoDeliveryMinPrice = parameterSettingBody.getAutoDeliveryMinPrice(), + buyPricePremiumRate = parameterSettingBody.getBuyPricePremiumRate(), + websiteMaintenance = parameterSettingBody.getWebsiteMaintenance(), + shoppingMaintenance = parameterSettingBody.getShoppingMaintenance(), + fightMaintenance = parameterSettingBody.getFightMaintenance(), + bindBoxMaintenance = parameterSettingBody.getBindBoxMaintenance(), + rollMaintenance = parameterSettingBody.getRollMaintenance(), + compoundMaintenance = parameterSettingBody.getCompoundMaintenance(), + westernDuelMaintenance = parameterSettingBody.getWesternDuelMaintenance(), + luckyUpgradeMaintenance = parameterSettingBody.getLuckyUpgradeMaintenance(), + wheelMaintenance = parameterSettingBody.getWheelMaintenance(), + olympusMaintenance = parameterSettingBody.getOlympusMaintenance(); + List parameterSettingList = websiteSetupMapper.selectParameterSettingList(); + Map> parameterSettingMap = parameterSettingList.stream().collect(Collectors.groupingBy(ConfigData::getConfigKey)); + List updateList = new ArrayList<>(); + setConfigValue(parameterSettingMap, "usePricePremiumRate", usePricePremiumRate, updateList); + setConfigValue(parameterSettingMap, "exchangePriceRatio", exchangePriceRatio, updateList); + setConfigValue(parameterSettingMap, "registerRedPacket", registerRedPacket, updateList); + setConfigValue(parameterSettingMap, "maxCompoundNum", maxCompoundNum, updateList); + setConfigValue(parameterSettingMap, "compoundMinPrice", compoundMinPrice, updateList); + setConfigValue(parameterSettingMap, "compoundMinPremiumRate", compoundMinPremiumRate, updateList); + setConfigValue(parameterSettingMap, "compoundMaxPremiumRate", compoundMaxPremiumRate, updateList); + setConfigValue(parameterSettingMap, "ZBTParities", zbtParities, updateList); + setConfigValue(parameterSettingMap, "buyPricePremiumRate", buyPricePremiumRate, updateList); + setConfigValue(parameterSettingMap, "autoDeliveryMinPrice", autoDeliveryMinPrice, updateList); + setConfigValue(parameterSettingMap, "websiteMaintenance", websiteMaintenance, updateList); + setConfigValue(parameterSettingMap, "shoppingMaintenance", shoppingMaintenance, updateList); + setConfigValue(parameterSettingMap, "fightMaintenance", fightMaintenance, updateList); + setConfigValue(parameterSettingMap, "bindBoxMaintenance", bindBoxMaintenance, updateList); + setConfigValue(parameterSettingMap, "rollMaintenance", rollMaintenance, updateList); + setConfigValue(parameterSettingMap, "compoundMaintenance", compoundMaintenance, updateList); + setConfigValue(parameterSettingMap, "westernDuelMaintenance", westernDuelMaintenance, updateList); + setConfigValue(parameterSettingMap, "luckyUpgradeMaintenance", luckyUpgradeMaintenance, updateList); + setConfigValue(parameterSettingMap, "wheelMaintenance", wheelMaintenance, updateList); + setConfigValue(parameterSettingMap, "olympusMaintenance", olympusMaintenance, updateList); + // 充值渠道维护配置(安全读写,不存在时跳过,避免未执行 SQL 时报错) + String rechargeMaintenance = parameterSettingBody.getRechargeMaintenance(); + List rechargeMaintenanceUpdateList = parameterSettingMap.get("rechargeMaintenance"); + if (rechargeMaintenanceUpdateList != null && !rechargeMaintenanceUpdateList.isEmpty()) { + ConfigData rechargeMaintenanceSysConfig = rechargeMaintenanceUpdateList.get(0); + rechargeMaintenanceSysConfig.setConfigValue(rechargeMaintenance != null ? rechargeMaintenance : "0"); + updateList.add(rechargeMaintenanceSysConfig); + } + // 卡密充值维护配置(安全读写,不存在时跳过,避免未执行 SQL 时报错) + String cardRechargeMaintenance = parameterSettingBody.getCardRechargeMaintenance(); + List cardRechargeMaintenanceUpdateList = parameterSettingMap.get("cardRechargeMaintenance"); + if (cardRechargeMaintenanceUpdateList != null && !cardRechargeMaintenanceUpdateList.isEmpty()) { + ConfigData cardRechargeMaintenanceSysConfig = cardRechargeMaintenanceUpdateList.get(0); + cardRechargeMaintenanceSysConfig.setConfigValue(cardRechargeMaintenance != null ? cardRechargeMaintenance : "0"); + updateList.add(cardRechargeMaintenanceSysConfig); + } + // 推广后台维护配置(安全读写,不存在时跳过,避免未执行 SQL 时报错) + String promoMaintenance = parameterSettingBody.getPromoMaintenance(); + List promoMaintenanceUpdateList = parameterSettingMap.get("promoMaintenance"); + if (promoMaintenanceUpdateList != null && !promoMaintenanceUpdateList.isEmpty()) { + ConfigData promoMaintenanceSysConfig = promoMaintenanceUpdateList.get(0); + promoMaintenanceSysConfig.setConfigValue(promoMaintenance != null ? promoMaintenance : "0"); + updateList.add(promoMaintenanceSysConfig); + } + // 支付公司配置(安全读写,不存在时跳过) + String payCompany = parameterSettingBody.getPayCompany(); + List payCompanyList = parameterSettingMap.get("payCompany"); + if (payCompanyList != null && !payCompanyList.isEmpty()) { + ConfigData payCompanySysConfig = payCompanyList.get(0); + payCompanySysConfig.setConfigValue(payCompany != null ? payCompany : "chengduhipay"); + updateList.add(payCompanySysConfig); + } + // 支付公司权重配置(安全读写,不存在时跳过) + String payCompanyWeights = parameterSettingBody.getPayCompanyWeights(); + List payCompanyWeightsList = parameterSettingMap.get("payCompanyWeights"); + if (payCompanyWeightsList != null && !payCompanyWeightsList.isEmpty()) { + ConfigData payCompanyWeightsSysConfig = payCompanyWeightsList.get(0); + payCompanyWeightsSysConfig.setConfigValue(payCompanyWeights != null ? payCompanyWeights : "[{\"name\":\"chengduhipay\",\"weight\":10}]"); + updateList.add(payCompanyWeightsSysConfig); + } + // 流水归档保留天数配置(安全读写,不存在时跳过) + String flowArchiveRetainDays = parameterSettingBody.getFlowArchiveRetainDays(); + List flowArchiveRetainDaysList = parameterSettingMap.get("flowArchiveRetainDays"); + if (flowArchiveRetainDaysList != null && !flowArchiveRetainDaysList.isEmpty()) { + ConfigData flowArchiveRetainDaysSysConfig = flowArchiveRetainDaysList.get(0); + flowArchiveRetainDaysSysConfig.setConfigValue(flowArchiveRetainDays != null ? flowArchiveRetainDays : "7"); + updateList.add(flowArchiveRetainDaysSysConfig); + } + // SnapPay 开关配置(安全读写,不存在时跳过,避免未执行 SQL 时报错) + String snappayEnabled = parameterSettingBody.getSnappayEnabled(); + List snappayEnabledList = parameterSettingMap.get("snappayEnabled"); + if (snappayEnabledList != null && !snappayEnabledList.isEmpty()) { + ConfigData snappayEnabledSysConfig = snappayEnabledList.get(0); + snappayEnabledSysConfig.setConfigValue(snappayEnabled != null ? snappayEnabled : "1"); + updateList.add(snappayEnabledSysConfig); + } + for (ConfigData configData : updateList) { + configService.updateById(configData); + } + iSysConfigService.resetConfigCache(); + + // 维护开关开启时,发布事件通知各游戏服务强制结算活跃游戏 + if ("1".equals(websiteMaintenance) || "1".equals(wheelMaintenance)) { + String sourceType = "1".equals(websiteMaintenance) ? "website" : "wheel"; + eventPublisher.publishEvent(new MaintenanceEnabledEvent(this, sourceType)); + log.info("MaintenanceEnabledEvent published, sourceType={}", sourceType); + } + + // 当价格倍率或汇率变更时,批量更新所有饰品的 usePrice + BigDecimal newRate = new BigDecimal(usePricePremiumRate); + BigDecimal newParities = new BigDecimal(zbtParities); + int updatedRows = ttOrnamentMapper.batchUpdateUsePriceByRate(newParities, newRate); + log.info("批量更新饰品价格完成,更新条数:{},ZBTParities={},usePricePremiumRate={}", updatedRows, newParities, newRate); + + return ""; + } + + /** + * 安全读取配置值:key 不存在时返回 null(不抛 NPE)。 + */ + private String getConfigValue(Map> map, String key) { + List list = map.get(key); + return (list != null && !list.isEmpty()) ? list.get(0).getConfigValue() : null; + } + + /** + * 安全更新配置值:key 不存在时跳过(不抛 NPE)。 + */ + private void setConfigValue(Map> map, String key, String value, List updateList) { + List list = map.get(key); + if (list != null && !list.isEmpty()) { + ConfigData config = list.get(0); + config.setConfigValue(value); + updateList.add(config); + } + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/task/TimeRollTask.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/task/TimeRollTask.java new file mode 100644 index 0000000..79276d3 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/task/TimeRollTask.java @@ -0,0 +1,185 @@ +package com.ruoyi.admin.task; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtRollJackpotOrnamentsMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtTimeRollService; +import com.ruoyi.admin.service.TtTimeRollUserService; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.roll.TtRollJackpotOrnaments; +import com.ruoyi.domain.entity.roll.TtTimeRoll; +import com.ruoyi.domain.entity.roll.TtTimeRollUser; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordSource.ROLL; + +@Slf4j +@Component("TimeRollTask") +public class TimeRollTask { + + private final TtOrnamentMapper ornamentsMapper; + private final TtBoxRecordsService boxRecordsService; + private final TtTimeRollService timeRollService; + private final TtTimeRollUserService timeRollUserService; + private final TtRechargeRecordMapper rechargeRecordMapper; + private final TtRollJackpotOrnamentsMapper rollJackpotOrnamentsMapper; + + public TimeRollTask(TtOrnamentMapper ornamentsMapper, + TtBoxRecordsService boxRecordsService, + TtTimeRollService timeRollService, + TtTimeRollUserService timeRollUserService, + TtRechargeRecordMapper rechargeRecordMapper, + TtRollJackpotOrnamentsMapper rollJackpotOrnamentsMapper) { + this.ornamentsMapper = ornamentsMapper; + this.boxRecordsService = boxRecordsService; + this.timeRollService = timeRollService; + this.timeRollUserService = timeRollUserService; + this.rechargeRecordMapper = rechargeRecordMapper; + this.rollJackpotOrnamentsMapper = rollJackpotOrnamentsMapper; + } + + @Transactional(rollbackFor = Exception.class) + public void timeRollSettle(Integer id){ + TtTimeRoll timeRoll = timeRollService.getById(id); + String rechargeCondition = timeRoll.getRechargeCondition(); + BigDecimal minRecharge = timeRoll.getMinRecharge(); + List userIds = null; + if ("0".equals(rechargeCondition)) { + userIds = rechargeRecordMapper.getLastHourRechargeUserIds(minRecharge); + } else if ("1".equals(rechargeCondition)) { + userIds = rechargeRecordMapper.getLastDayRechargeUserIds(minRecharge); + } else if ("2".equals(rechargeCondition)) { + userIds = rechargeRecordMapper.getLastWeekRechargeUserIds(minRecharge); + } else if ("3".equals(rechargeCondition)) { + userIds = rechargeRecordMapper.getLastMonthRechargeUserIds(minRecharge); + } + if (StringUtils.isNull(userIds) || userIds.isEmpty()) return; + List allocatedTimeRollUsers = new LambdaQueryChainWrapper<>(timeRollUserService.getBaseMapper()) + .eq(TtTimeRollUser::getTimeRollId, id).eq(TtTimeRollUser::getStatus, "2").eq(TtTimeRollUser::getEndStatus, "0").list(); + // 幂等:排除该定时Roll房内所有已存在成员(任意状态),避免重复结算导致同一用户重复入库、重复发奖 + List existingUserIds = new LambdaQueryChainWrapper<>(timeRollUserService.getBaseMapper()) + .eq(TtTimeRollUser::getTimeRollId, id).list() + .stream().map(TtTimeRollUser::getUserId).collect(Collectors.toList()); + userIds.removeAll(existingUserIds); + if (userIds.isEmpty()) return; + List timeRollUserList = new ArrayList<>(); + for (Integer userId : userIds) { + TtTimeRollUser timeRollUser = TtTimeRollUser.builder().build(); + timeRollUser.setTimeRollId(id); + timeRollUser.setUserId(userId); + timeRollUser.setJoinTime(DateUtils.getNowDate()); + timeRollUserList.add(timeRollUser); + } + if (timeRollUserService.saveBatch(timeRollUserList, 1)) { + List surplusOrnamentsList = getSurplusOrnamentsList(id, timeRoll.getJackpotId(), allocatedTimeRollUsers); + if (!surplusOrnamentsList.isEmpty()) { + List timeRollUsers = new LambdaQueryChainWrapper<>(timeRollUserService.getBaseMapper()) + .eq(TtTimeRollUser::getTimeRollId, id).eq(TtTimeRollUser::getStatus, "0").eq(TtTimeRollUser::getEndStatus, "0").list(); + Collections.shuffle(timeRollUsers); + int index = 0; + Map map = new HashMap<>(); + if (surplusOrnamentsList.size() > timeRollUsers.size()) { + for (TtTimeRollUser ttTimeRollUser : timeRollUsers) { + Long ornamentsId = surplusOrnamentsList.get(index); + TtRollJackpotOrnaments rollJackpotOrnaments = getRollJackpotOrnaments(ornamentsId, map, timeRoll.getJackpotId()); + TtBoxRecords boxRecords = addBoxRecordsData(id, ornamentsId, ttTimeRollUser.getUserId(), rollJackpotOrnaments); + boxRecordsService.save(boxRecords); + ttTimeRollUser.setJackpotOrnamentsId(rollJackpotOrnaments.getId()); + ttTimeRollUser.setOrnamentsId(ornamentsId); + ttTimeRollUser.setBoxRecordId(boxRecords.getId()); + ttTimeRollUser.setStatus("1"); + ttTimeRollUser.setDesignatedBy("系统随机分配"); + ttTimeRollUser.setUpdateTime(DateUtils.getNowDate()); + ttTimeRollUser.setEndStatus("1"); + index++; + } + } else { + for (Long ornamentsId : surplusOrnamentsList) { + TtRollJackpotOrnaments rollJackpotOrnaments = getRollJackpotOrnaments(ornamentsId, map, timeRoll.getJackpotId()); + TtBoxRecords boxRecords = addBoxRecordsData(id, ornamentsId, timeRollUsers.get(index).getUserId(), rollJackpotOrnaments); + boxRecordsService.save(boxRecords); + timeRollUsers.get(index).setJackpotOrnamentsId(rollJackpotOrnaments.getId()); + timeRollUsers.get(index).setOrnamentsId(ornamentsId); + timeRollUsers.get(index).setBoxRecordId(boxRecords.getId()); + timeRollUsers.get(index).setStatus("1"); + timeRollUsers.get(index).setDesignatedBy("系统随机分配"); + timeRollUsers.get(index).setUpdateTime(DateUtils.getNowDate()); + timeRollUsers.get(index).setEndStatus("1"); + index++; + } + } + timeRollUserService.updateBatchById(timeRollUsers, 1); + } + List boxRecordsList = new LambdaQueryChainWrapper<>(boxRecordsService.getBaseMapper()) + .eq(TtBoxRecords::getRollId, id) + .eq(TtBoxRecords::getStatus, "7") + .eq(TtBoxRecords::getSource, "7") + .list(); + boxRecordsList = boxRecordsList.stream().peek(ttBoxRecords -> { + ttBoxRecords.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); + ttBoxRecords.setUpdateTime(DateUtils.getNowDate()); + }).collect(Collectors.toList()); + boxRecordsService.updateBatchById(boxRecordsList, 1); + } + } + + private TtBoxRecords addBoxRecordsData(Integer timeRollId, Long ornamentsId, Integer userId, TtRollJackpotOrnaments rollJackpotOrnaments) { + TtOrnament ttOrnament = new LambdaQueryChainWrapper<>(ornamentsMapper).eq(TtOrnament::getId, ornamentsId).one(); + TtBoxRecords boxRecords = TtBoxRecords.builder().build(); + boxRecords.setUserId(userId); + boxRecords.setOrnamentId(ornamentsId); + boxRecords.setOrnamentsPrice(ttOrnament.getUsePrice()); + boxRecords.setOrnamentsLevelId(rollJackpotOrnaments.getOrnamentLevelId()); + boxRecords.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); + boxRecords.setCreateTime(DateUtils.getNowDate()); + boxRecords.setSource(ROLL.getCode()); + boxRecords.setRollId(timeRollId); + boxRecords.setHolderUserId(userId); + return boxRecords; + } + + private TtRollJackpotOrnaments getRollJackpotOrnaments(Long ornamentsId, Map map, Integer jackpotId) { + TtRollJackpotOrnaments ttRollJackpotOrnaments = map.get(ornamentsId); + if (ttRollJackpotOrnaments == null) { + ttRollJackpotOrnaments = new LambdaQueryChainWrapper<>(rollJackpotOrnamentsMapper).eq(TtRollJackpotOrnaments::getJackpotId, jackpotId) + .eq(TtRollJackpotOrnaments::getOrnamentsId, ornamentsId).one(); + map.put(ornamentsId, ttRollJackpotOrnaments); + } + return ttRollJackpotOrnaments; + } + + private List getSurplusOrnamentsList(Integer id, Integer jackpotId, List allocatedTimeRollUsers) { + List ornamentsList = + new LambdaQueryChainWrapper<>(rollJackpotOrnamentsMapper).eq(TtRollJackpotOrnaments::getJackpotId, jackpotId).list(); + Map data = new HashMap<>(); + for (TtRollJackpotOrnaments ornaments : ornamentsList) { + data.put(ornaments.getOrnamentsId(), ornaments.getOrnamentsNum()); + } + List surplusOrnamentsList = RandomUtils.toList(data); + List allocated = allocatedTimeRollUsers.stream().map(TtTimeRollUser::getOrnamentsId).collect(Collectors.toList()); + for (Long i : allocated) { + Iterator iterator = surplusOrnamentsList.iterator(); + while (iterator.hasNext()) { + Long next = iterator.next(); + if (Objects.equals(next, i)) { + iterator.remove(); + break; + } + } + } + return surplusOrnamentsList; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/task/TtExponentGenerateTask.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/task/TtExponentGenerateTask.java new file mode 100644 index 0000000..5005e25 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/task/TtExponentGenerateTask.java @@ -0,0 +1,99 @@ +package com.ruoyi.admin.task; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtExponentMapper; +import com.ruoyi.domain.entity.exponent.TtExponent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Random; + +@Slf4j +@Component("TtExponentGenerateTask") +public class TtExponentGenerateTask { + + private final static int EXPONENT_BASE = 30050000; + + @Autowired + private TtExponentMapper ttExponentMapper; + +// @Scheduled(cron = "*/30 * * * * ?") + public void generateExponent() { + // 创建日期格式化对象,指定日期格式 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + // 获取Calendar实例 + Calendar calendar = Calendar.getInstance(); + // 设置时间为今天凌晨 + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + // 获取今天凌晨时间 + Date todayMidnight = calendar.getTime(); + String formattedTodayMidnight = formatter.format(todayMidnight); + // 获取明天凌晨时间 + calendar.add(Calendar.DAY_OF_MONTH, 1); + Date tomorrowMidnight = calendar.getTime(); + String formattedTomorrowMidnight = formatter.format(tomorrowMidnight); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.between(TtExponent::getCreateTime, formattedTodayMidnight, formattedTomorrowMidnight); + wrapper.orderByDesc(TtExponent::getId); + wrapper.last("limit 1"); + TtExponent db = ttExponentMapper.selectOne(wrapper); + TtExponent exponent = new TtExponent(); + if (db == null) { + int value = 0; + StringBuilder points = new StringBuilder(); + points.append(EXPONENT_BASE).append(","); + int exponentValue = 0; + for (int i = 0; i < 10; i++) { + value = generate(); + exponentValue = EXPONENT_BASE + value; + points.append(exponentValue).append(","); + } + exponent.setExponent(exponentValue); + exponent.setPoints(points.substring(0, points.length() - 1)); + exponent.setForward(value > 0 ? 1 : 2); + exponent.setNumber(1); + exponent.setCreateTime(new Date()); + } else { + StringBuilder points = new StringBuilder(); + points.append(db.getExponent()).append(","); + int value = 0; + int exponentValue = 0; + for (int i = 0; i < 10; i++) { + value = generate(); + exponentValue = db.getExponent() + value; + points.append(exponentValue).append(","); + } + exponent.setExponent(exponentValue); + exponent.setPoints(points.substring(0, points.length() - 1)); + exponent.setForward(value > 0 ? 1 : 2); + exponent.setNumber(db.getNumber() + 1); + exponent.setCreateTime(new Date()); + } + ttExponentMapper.insert(exponent); +// log.info("更新数据完成"); + } + + + /** + * 随机生成数据 + */ + private int generate() { + Random random = new Random(); + // 生成100到1000之间的随机数 + int randomNumber = 100 + random.nextInt(901); // nextInt(901)生成0到900之间的数,加上100后就是100到1000 + // 随机决定是正数还是负数 + if (random.nextBoolean()) { // 有50%的概率是true,50%的概率是false + randomNumber = -randomNumber; + } + return randomNumber; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/OperatorEnum.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/OperatorEnum.java new file mode 100644 index 0000000..007713e --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/OperatorEnum.java @@ -0,0 +1,27 @@ +package com.ruoyi.admin.util; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum OperatorEnum { + + CHINA_MOBILE(0, "中国移动"), + CHINA_UNICOM(1, "中国联通"), + CHINA_TELECOM(2, "中国电信"); + + private final Integer code; + + private final String name; + + public static String getOperateNameByCode(Integer code) { + for (OperatorEnum value : OperatorEnum.values()) { + if (value.code.equals(code)) { + return value.name; + } + } + return null; + } +} + diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/PhoneNumberUtil.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/PhoneNumberUtil.java new file mode 100644 index 0000000..2814a18 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/PhoneNumberUtil.java @@ -0,0 +1,43 @@ +package com.ruoyi.admin.util; + +import java.util.Random; + +public class PhoneNumberUtil { + + private static final String[] CHINA_MOBILE = { + "134", "135", "136", "137", "138", "139", "150", "151", "152", "157", "158", "159", + "182", "183", "184", "187", "188", "178", "147", "172", "198" + }; + + private static final String[] CHINA_UNICOM = {"130", "131", "132", "145", "155", "156", "166", "171", "175", "176", "185", "186", "166"}; + + private static final String[] CHINA_TELECOM = {"133", "149", "153", "173", "177", "180", "181", "189", "199"}; + + public static String createPhoneNumber(int operator) { + Random random = new Random(); + StringBuilder builder = new StringBuilder(); + String mobilePrefix = null; + switch (operator) { + case 0: + mobilePrefix = CHINA_MOBILE[random.nextInt(CHINA_MOBILE.length)]; + break; + case 1: + mobilePrefix = CHINA_UNICOM[random.nextInt(CHINA_UNICOM.length)]; + break; + case 2: + mobilePrefix = CHINA_TELECOM[random.nextInt(CHINA_TELECOM.length)]; + break; + default: + mobilePrefix = "运营商错误"; + break; + } + + builder.append(mobilePrefix); + int temp; + for (int i = 0; i < 8; i++) { + temp = random.nextInt(10); + builder.append(temp); + } + return builder.toString(); + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/RandomUtils.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/RandomUtils.java new file mode 100644 index 0000000..18dc166 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/RandomUtils.java @@ -0,0 +1,197 @@ +package com.ruoyi.admin.util; + +import cn.hutool.core.util.RandomUtil; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class RandomUtils { + + private static final Logger log = LoggerFactory.getLogger(RandomUtils.class); + + // ==================== 自然风光类(28 个前缀) ==================== + private static final String[] NATURE_PREFIX = { + "清晨", "午后", "黄昏", "深夜", "黎明", "正午", "傍晚", "子夜", + "春日", "夏日", "秋日", "冬日", "早春", "仲夏", "金秋", "隆冬", + "蓝天", "白云", "彩虹", "繁星", "皓月", "朝阳", "夕阳", "月亮", + "高山", "大海", "森林", "草原", "沙漠", "湖泊", "溪流", "瀑布" + }; + + private static final String[] NATURE_SUFFIX = { + "微风", "细雨", "流云", "飞霞", "白雪", "落花", "落叶", "枯叶", + "晨曦", "晚霞", "月光", "星光", "雾气", "露珠", "雪花", "冰晶", + "青山", "绿水", "碧空", "苍穹", "云霄", "天际", "远方", "彼岸", + "林间", "海边", "山顶", "谷底", "云端", "风中", "雨中", "雪中" + }; + + // ==================== 古风诗意类(35 个前缀) ==================== + private static final String[] CLASSICAL_PREFIX = { + "青衣", "红袖", "白玉", "黄金", "水墨", "丹青", "锦绣", "琉璃", + "青梅", "竹马", "知己", "故友", "佳人", "书生", "侠客", "将军", + "江山", "天下", "四海", "九州", "昆仑", "蓬莱", "瑶台", "琼楼", + "兰亭", "曲水", "琴音", "棋局", "书画", "诗韵", "词牌", "歌喉", + "东风", "南雁", "西窗", "北岭", "南山", "东湖", "西湖", "南湖", + "柳枝", "桃红", "荷香", "菊黄", "梅雪", "竹叶", "松涛", "桐荫" + }; + + private static final String[] CLASSICAL_SUFFIX = { + "烟雨", "烟雨", "烟雨", "烟雨", "烟雨", "一蓑", "半盏", "几许", + "如梦", "似幻", "成空", "无痕", "有痕", "有意", "无心", "有情", + "千回", "百转", "万重", "九重", "十里", "千秋", "百年", "千年", + "一笑", "一顾", "一盼", "一醉", "一梦", "一诺", "一生", "一世", + "谁家", "何处", "何时", "何故", "谁人", "何妨", "何苦", "何必", + "江畔", "楼头", "窗前", "陌上", "溪边", "树下", "花间", "月下" + }; + + // ==================== 现代文艺类(40 个前缀) ==================== + private static final String[] MODERN_PREFIX = { + "安静", "喧嚣", "浮躁", "平静", "淡然", "执着", "迷茫", "坚定", + "孤独", "热闹", "冷清", "繁华", "简单", "复杂", "纯粹", "深沉", + "温柔", "坚强", "脆弱", "勇敢", "胆怯", "乐观", "悲观", "积极", + "浪漫", "现实", "理想", "幻想", "梦境", "真实", "虚幻", "平凡", + "特别", "普通", "独特", "共性", "自我", "他人", "世界", "宇宙", + "天空", "大地", "海洋", "山川", "河流", "城市", "乡村", "旅途", + "时光", "岁月", "年华", "青春", "回忆", "往事", "现在", "未来" + }; + + private static final String[] MODERN_SUFFIX = { + "的猫", "的鱼", "的云", "的风", "的雨", "的雪", "的星", "的月", + "少年", "少女", "大叔", "阿姨", "小哥", "小妹", "朋友", "陌生人", + "旅行家", "观察者", "思考者", "创造者", "梦想家", "实践者", "追随者", "引领者", + "拾荒者", "摘星人", "捕风者", "追光者", "赏花人", "听雨客", "踏雪郎", "乘风子", + "过客", "归人", "行人", "路人", "友人", "仇人", "亲人", "爱人", + "诗人", "画家", "舞者", "歌者", "匠人", "农夫", "工匠", "学者", + "行者", "隐者", "智者", "勇者", "忍者", "强者", "弱者", "凡人" + }; + + // ==================== 网络趣味类(50 个前缀) ==================== + private static final String[] FUNNY_PREFIX = { + "快乐", "忧伤", "开心", "难过", "兴奋", "沮丧", "得意", "失落", + "满足", "遗憾", "惊喜", "惊吓", "期待", "失望", "希望", "绝望", + "热情", "冷漠", "友好", "敌对", "真诚", "虚伪", "诚实", "欺骗", + "聪明", "愚笨", "机智", "迟钝", "灵活", "僵硬", "敏捷", "迟缓", + "潇洒", "邋", "整洁", "凌乱", "优雅", "粗俗", "高贵", "低贱", + "可爱", "帅气", "漂亮", "丑陋", "美丽", "迷人", "性感", "性感", + "霸气", "温顺", "强势", "柔弱", "主动", "被动", "积极", "消极", + "幽默", "严肃", "活泼", "沉闷", "开朗", "阴郁", "阳光", "阴暗", + "自信", "自卑", "自豪", "羞愧", "骄傲", "谦逊", "傲慢", "谦卑", + "疯狂", "冷静", "冲动", "理智", "感性", "理性", "感性", "机械" + }; + + private static final String[] FUNNY_SUFFIX = { + "咸鱼", "卧龙凤雏", "锦鲤附体", "单身狗", "剁手党", "熬夜冠军", "美食家", + "吃货", "酒鬼", "赌鬼", "财迷", "工作狂", "游戏控", "追剧党", "阅读者", + "健身达人", "护肤小能手", "时尚达人", "潮流先锋", "复古爱好者", + "文艺青年", "霸道总裁", "软萌萝莉", "高冷男神", "暖男哥哥", "毒舌妹妹", + "沙雕网友", "键盘侠", "潜水员", "潜水艇", "潜水高手", "潜水专家", + "表情包大户", "段子手", "鸡汤大师", "负能量批发商", "正能量传播者", + "杠精", "喷子", "吹牛大王", "画饼大师", "挖坑专业户", + "套路深", "智商税", "韭菜苗", "被割菜", "待宰羔羊", "幸运儿", + "天选之子", "倒霉蛋", "扫把星", "福星", "吉祥物", "幸运星", + "潜力股", "黑马选手", "老司机", "新手村", "退役选手", "退休玩家", + "职业选手", "业余选手", "民间高手", "扫地僧", "隐士高人" + }; + + /** + * 生成随机 4 字网名(从五类中随机选一类,该类别内完全随机组合) + * + * @param sex 性别参数,当前已忽略,仅保留以兼容历史调用方 + * @return 4 字随机网名 + */ + public static String getRandomName(int sex) { + int style = randomInt(5); // 0=自然风光,1=古风诗意,2=现代文艺,3=网络趣味 + switch (style) { + case 0: // 自然风光(28×28=784 种) + return NATURE_PREFIX[randomInt(NATURE_PREFIX.length)] + + NATURE_SUFFIX[randomInt(NATURE_SUFFIX.length)]; + case 1: // 古风诗意(46×46=2116 种) + return CLASSICAL_PREFIX[randomInt(CLASSICAL_PREFIX.length)] + + CLASSICAL_SUFFIX[randomInt(CLASSICAL_SUFFIX.length)]; + case 2: // 现代文艺(49×43=2107 种) + return MODERN_PREFIX[randomInt(MODERN_PREFIX.length)] + + MODERN_SUFFIX[randomInt(MODERN_SUFFIX.length)]; + default: // 3-4: 网络趣味(50×56=2800 种) + return FUNNY_PREFIX[randomInt(FUNNY_PREFIX.length)] + + FUNNY_SUFFIX[randomInt(FUNNY_SUFFIX.length)]; + } + } + + public static int randomInt(int maxNum) { + Random random = new Random(); + return random.nextInt(maxNum); + } + + public static List toList(Map data) { + List resultList = new ArrayList<>(); + try { + for (Map.Entry entry : data.entrySet()) { + Long key = entry.getKey(); + Integer value = entry.getValue(); + for (int i = 0; i < value; i++) { + resultList.add(key); + } + } + Collections.shuffle(resultList); + } catch (Exception e) { + log.error("调用RandomUtils.toList()方法时出现异常,请检查代码!"); + } + return resultList; + } + + public static int getRandomIndex(int maxValue) { + Random random = new Random(); + return random.nextInt(maxValue); + } + + public static BigDecimal getRandomPrice(String priceSectionJSONStr) { + List priceSection = JSONObject.parseObject(priceSectionJSONStr, new TypeReference>() { + }); + if (priceSection.get(0).compareTo(priceSection.get(1)) == 0) { + return priceSection.get(0); + } else if (priceSection.get(0).compareTo(priceSection.get(1)) > 0) { + return RandomUtil.randomBigDecimal(priceSection.get(1), priceSection.get(0)); + } else { + return RandomUtil.randomBigDecimal(priceSection.get(0), priceSection.get(1)); + } + } + + /** + * 获取两个数之间的随机数 + * + * @param beginB + * @param endB + * @return + */ + public static BigDecimal getRandomaBigdecimal(BigDecimal beginB, BigDecimal endB) { + double begin = beginB.doubleValue(); + double end = endB.doubleValue(); + Double rtn = begin + (Double) (Math.random() * (end - begin)); + if (rtn.equals(begin) || rtn.equals(end)) { + return new BigDecimal(getRandomaDouble(begin, end)).setScale(2, BigDecimal.ROUND_HALF_DOWN); + } + return new BigDecimal(rtn).setScale(2, BigDecimal.ROUND_HALF_DOWN); + } + + /** + * 获取两个数之间的随机数 + * + * @param begin + * @param end + * @return + */ + public static double getRandomaDouble(Double begin, Double end) { + Double rtn = begin + (Double) (Math.random() * (end - begin)); + if (rtn.equals(begin) || rtn.equals(end)) { + return getRandomaDouble(begin, end); + } + return rtn; + } +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/core/fight/LotteryMachine.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/core/fight/LotteryMachine.java new file mode 100644 index 0000000..04aa0b0 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/core/fight/LotteryMachine.java @@ -0,0 +1,718 @@ +package com.ruoyi.admin.util.core.fight; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.cache.BoxMockCacheRepository; +import com.ruoyi.admin.enums.BoxPoolType; +import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper; +import com.ruoyi.admin.mapper.TtBoxThirdExplosiveUserMapper; +import com.ruoyi.admin.mapper.TtCompRecordMapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.model.BoxMockCache; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.RandomUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.box.TtBoxThirdExplosiveUser; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtBoxOrnaments; +import com.ruoyi.domain.other.TtCompRecord; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static com.ruoyi.admin.config.RedisConstants.BASE_POOL_KEY; + +// 抽奖机 +@Slf4j +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +public class LotteryMachine { + + + // 抽奖锁 + private static final String LOTTERY_LOCK = "lotteryLock_"; + + // 从mysql加载抽奖空间锁 + private static final String PRIZE_POOL_LOAD_LOCK = "prize_pool_load_lock"; + + /** + * 奖池信息预热key + */ + private static final String All_PRIZE_POOL_KEY = "all_prize_pool:"; + + private RedisLock redisLock; + + private RedisCache redisCache; + + private TtBoxOrnamentsMapper boxOrnamentsMapper; + + // 奖池(实时的) 空的时候加锁同步更新奖池 + // 奖池key:PrizePool + public Map prizePools; + + @Autowired + private TtCompRecordMapper ttCompRecordMapper; + + @Autowired + private TtBoxThirdExplosiveUserMapper ttBoxThirdExplosiveUserMapper; + + @Autowired + private BoxMockCacheRepository boxMockCacheRepository; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + // 预热抽奖机,初始化所有奖池 + // TODO: 2024/3/22 缓存预热的数据要在数据更新时自动更新 + public void preheat() { + + try { + LambdaQueryWrapper boxOrnamentsQuery = new LambdaQueryWrapper<>(); + boxOrnamentsQuery.orderByDesc(TtBoxOrnaments::getBoxId); + List all = boxOrnamentsMapper.selectList(boxOrnamentsQuery); + + Integer boxId = -1; + PrizePool anchorPrizePool = null; + PrizePool realPrizePool = null; + PrizePool robotPrizePool = null; // 新增机器人用户奖池 + PrizePool robotLuckyPrizePool = null; // 06 机器人用户奖池(欧皇) + PrizePool robotUnluckyPrizePool = null; // 07 机器人用户奖池(非酋) + PrizePool compensationPrizePool = null; // 新增补偿奖池 + PrizePool thirdExplosivePrizePool = null; // 新增三级爆率奖池 + for (TtBoxOrnaments item : all) { + + if (!item.getBoxId().equals(boxId)) { + + // 新箱子 + boxId = item.getBoxId(); + + // 把上个箱子的数据保存 + if (ObjectUtil.isNotEmpty(anchorPrizePool) + && ObjectUtil.isNotEmpty(realPrizePool) + && ObjectUtil.isNotEmpty(compensationPrizePool) + && ObjectUtil.isNotEmpty(thirdExplosivePrizePool) + && ObjectUtil.isNotEmpty(robotPrizePool) + && ObjectUtil.isNotEmpty(robotLuckyPrizePool) + && ObjectUtil.isNotEmpty(robotUnluckyPrizePool)) { + // 奖池写入内存 + String anchorKey = anchorPrizePool.getKey(); + String realKey = realPrizePool.getKey(); + String robotKey = robotPrizePool.getKey(); // 新增机器人用户奖池Key + String robotLuckyKey = robotLuckyPrizePool.getKey(); // 06 机器人欧皇奖池Key + String robotUnluckyKey = robotUnluckyPrizePool.getKey(); // 07 机器人非酋奖池Key + String compensationKey = compensationPrizePool.getKey(); // 新增补偿奖池Key + String thirdExplosiveKey = thirdExplosivePrizePool.getKey(); // 新增三级爆率 + this.prizePools.put(anchorKey, anchorPrizePool); + this.prizePools.put(robotKey, robotPrizePool); // 写入机器人用户奖池 + this.prizePools.put(robotLuckyKey, robotLuckyPrizePool); // 写入机器人欧皇奖池 + this.prizePools.put(robotUnluckyKey, robotUnluckyPrizePool); // 写入机器人非酋奖池 + this.prizePools.put(realKey, realPrizePool); + this.prizePools.put(compensationKey, compensationPrizePool); // 写入补偿奖池 + this.prizePools.put(thirdExplosiveKey, thirdExplosivePrizePool); + + // 奖池写入redis(先删再存) + redisCache.deleteObject(anchorKey); + redisCache.deleteObject(robotKey); // 删除机器人用户奖池Key + redisCache.deleteObject(robotLuckyKey); // 删除机器人欧皇奖池Key + redisCache.deleteObject(robotUnluckyKey); // 删除机器人非酋奖池Key + redisCache.deleteObject(realKey); + redisCache.deleteObject(compensationKey); // 删除补偿奖池Key + redisCache.deleteObject(thirdExplosiveKey); // 删除三级爆率Key + + redisCache.setCacheMap(anchorKey, anchorPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); + redisCache.setCacheMap(robotKey, robotPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); // 缓存机器人用户奖池 + redisCache.setCacheMap(robotLuckyKey, robotLuckyPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); // 缓存机器人欧皇奖池 + redisCache.setCacheMap(robotUnluckyKey, robotUnluckyPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); // 缓存机器人非酋奖池 + redisCache.setCacheMap(realKey, realPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); + redisCache.setCacheMap(compensationKey, compensationPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); // 缓存补偿奖池 + redisCache.setCacheMap(thirdExplosiveKey, thirdExplosivePrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); // 缓存三级爆率奖池 + } + + // 01 + HashMap anchorBoxSpace = new HashMap<>(); + anchorBoxSpace.put(String.valueOf(item.getOrnamentId()), item.getAnchorOdds()); + + anchorPrizePool = PrizePool.builder() + .key(BASE_POOL_KEY + item.getBoxId() + ":" + BoxPoolType.ANCHOR_PLAYER.getId()) + .boxId(String.valueOf(item.getBoxId())) + .poolType(BoxPoolType.ANCHOR_PLAYER.getId()) + .goodsNumber(item.getAnchorOdds()) + .boxSpace(anchorBoxSpace) + .build(); + + // 02 + HashMap realBoxSpace = new HashMap<>(); + realBoxSpace.put(String.valueOf(item.getOrnamentId()), item.getRealOdds()); + + realPrizePool = PrizePool.builder() + .key(BASE_POOL_KEY + item.getBoxId() + ":" + BoxPoolType.NORMAL_PLAYER.getId()) + .boxId(String.valueOf(item.getBoxId())) + .poolType(BoxPoolType.NORMAL_PLAYER.getId()) + .goodsNumber(item.getRealOdds()) + .boxSpace(realBoxSpace) + .build(); + + // 05 机器人用户奖池 + HashMap robotBoxSpace = new HashMap<>(); + robotBoxSpace.put(String.valueOf(item.getOrnamentId()), item.getRobotOdds()); + robotPrizePool = PrizePool.builder() + .key(BASE_POOL_KEY + item.getBoxId() + ":" + BoxPoolType.ROBOT_PLAYER.getId()) + .boxId(String.valueOf(item.getBoxId())) + .poolType(BoxPoolType.ROBOT_PLAYER.getId()) + .goodsNumber(item.getRobotOdds()) // 机器人用户奖池数量 + .goodsNumber(item.getRobotOdds()) + .boxSpace(robotBoxSpace) + .build(); + + // 06 机器人用户奖池(欧皇) + HashMap robotLuckyBoxSpace = new HashMap<>(); + robotLuckyBoxSpace.put(String.valueOf(item.getOrnamentId()), item.getRobotOddsLucky() == null ? 0 : item.getRobotOddsLucky()); + robotLuckyPrizePool = PrizePool.builder() + .key(BASE_POOL_KEY + item.getBoxId() + ":" + BoxPoolType.ROBOT_PLAYER_LUCKY.getId()) + .boxId(String.valueOf(item.getBoxId())) + .poolType(BoxPoolType.ROBOT_PLAYER_LUCKY.getId()) + .goodsNumber(item.getRobotOddsLucky() == null ? 0 : item.getRobotOddsLucky()) + .boxSpace(robotLuckyBoxSpace) + .build(); + + // 07 机器人用户奖池(非酋) + HashMap robotUnluckyBoxSpace = new HashMap<>(); + robotUnluckyBoxSpace.put(String.valueOf(item.getOrnamentId()), item.getRobotOddsUnlucky() == null ? 0 : item.getRobotOddsUnlucky()); + robotUnluckyPrizePool = PrizePool.builder() + .key(BASE_POOL_KEY + item.getBoxId() + ":" + BoxPoolType.ROBOT_PLAYER_UNLUCKY.getId()) + .boxId(String.valueOf(item.getBoxId())) + .poolType(BoxPoolType.ROBOT_PLAYER_UNLUCKY.getId()) + .goodsNumber(item.getRobotOddsUnlucky() == null ? 0 : item.getRobotOddsUnlucky()) + .boxSpace(robotUnluckyBoxSpace) + .build(); + + // 03 补偿奖池 + HashMap compensationBoxSpace = new HashMap<>(); + compensationBoxSpace.put(String.valueOf(item.getOrnamentId()), item.getRealCompOdds()); + compensationPrizePool = PrizePool.builder() + .key(BASE_POOL_KEY + item.getBoxId() + ":" + BoxPoolType.COMPENSATION.getId()) + .boxId(String.valueOf(item.getBoxId())) + .poolType(BoxPoolType.COMPENSATION.getId()) + .goodsNumber(item.getRealCompOdds()) + .boxSpace(compensationBoxSpace) + .build(); + + // 04 三级爆率奖池 + HashMap thirdExplosiveBoxSpace = new HashMap<>(); + thirdExplosiveBoxSpace.put(String.valueOf(item.getOrnamentId()), item.getRealCompOdds()); + thirdExplosivePrizePool = PrizePool.builder() + .key(BASE_POOL_KEY + item.getBoxId() + ":" + BoxPoolType.THIRD_EXPLOSIVE.getId()) + .boxId(String.valueOf(item.getBoxId())) + .poolType(BoxPoolType.THIRD_EXPLOSIVE.getId()) + .goodsNumber(item.getThirdExplosiveOdds()) + .boxSpace(thirdExplosiveBoxSpace) + .build(); + } else { + // 累加 + anchorPrizePool.setGoodsNumber(anchorPrizePool.getGoodsNumber() + item.getAnchorOdds()); + Integer i1 = anchorPrizePool.getBoxSpace().get(String.valueOf(item.getOrnamentId())); + i1 = ObjectUtil.isEmpty(i1) ? 0 : i1; + anchorPrizePool.getBoxSpace().put(String.valueOf(item.getOrnamentId()), i1 + item.getAnchorOdds()); + + realPrizePool.setGoodsNumber(realPrizePool.getGoodsNumber() + item.getRealOdds()); + Integer i2 = realPrizePool.getBoxSpace().get(String.valueOf(item.getOrnamentId())); + i2 = ObjectUtil.isEmpty(i2) ? 0 : i2; + realPrizePool.getBoxSpace().put(String.valueOf(item.getOrnamentId()), i2 + item.getRealOdds()); + + // 05 机器人用户奖池 + robotPrizePool.setGoodsNumber(robotPrizePool.getGoodsNumber() + item.getRobotOdds()); + Integer i5 = robotPrizePool.getBoxSpace().get(String.valueOf(item.getOrnamentId())); + i5 = ObjectUtil.isEmpty(i5) ? 0 : i5; + robotPrizePool.getBoxSpace().put(String.valueOf(item.getOrnamentId()), i5 + item.getRobotOdds()); + + // 06 机器人用户奖池(欧皇) + int luckyOdds = item.getRobotOddsLucky() == null ? 0 : item.getRobotOddsLucky(); + robotLuckyPrizePool.setGoodsNumber(robotLuckyPrizePool.getGoodsNumber() + luckyOdds); + Integer i6 = robotLuckyPrizePool.getBoxSpace().get(String.valueOf(item.getOrnamentId())); + i6 = ObjectUtil.isEmpty(i6) ? 0 : i6; + robotLuckyPrizePool.getBoxSpace().put(String.valueOf(item.getOrnamentId()), i6 + luckyOdds); + + // 07 机器人用户奖池(非酋) + int unluckyOdds = item.getRobotOddsUnlucky() == null ? 0 : item.getRobotOddsUnlucky(); + robotUnluckyPrizePool.setGoodsNumber(robotUnluckyPrizePool.getGoodsNumber() + unluckyOdds); + Integer i7 = robotUnluckyPrizePool.getBoxSpace().get(String.valueOf(item.getOrnamentId())); + i7 = ObjectUtil.isEmpty(i7) ? 0 : i7; + robotUnluckyPrizePool.getBoxSpace().put(String.valueOf(item.getOrnamentId()), i7 + unluckyOdds); + + compensationPrizePool.setGoodsNumber(compensationPrizePool.getGoodsNumber() + item.getRealCompOdds()); + Integer i3 = compensationPrizePool.getBoxSpace().get(String.valueOf(item.getOrnamentId())); + i3 = ObjectUtil.isEmpty(i3) ? 0 : i3; + compensationPrizePool.getBoxSpace().put(String.valueOf(item.getOrnamentId()), i3 + item.getRealCompOdds()); + + thirdExplosivePrizePool.setGoodsNumber(thirdExplosivePrizePool.getGoodsNumber() + item.getThirdExplosiveOdds()); + Integer i4 = thirdExplosivePrizePool.getBoxSpace().get(String.valueOf(item.getOrnamentId())); + i4 = ObjectUtil.isEmpty(i4) ? 0 : i4; + thirdExplosivePrizePool.getBoxSpace().put(String.valueOf(item.getOrnamentId()), i4 + item.getThirdExplosiveOdds()); + } + + } + + // 保存最后一个箱子的奖池 + // 奖池写入内存 + if (ObjectUtil.isEmpty(anchorPrizePool) || ObjectUtil.isEmpty(realPrizePool)) { + log.info("没有宝箱绑定饰品信息,奖池预热结束。"); + return; + } + ; + String anchorKey = anchorPrizePool.getKey(); + String realKey = realPrizePool.getKey(); + String robotKey = robotPrizePool.getKey(); // 新增机器人用户奖池Key + String robotLuckyKey = robotLuckyPrizePool.getKey(); // 06 机器人欧皇奖池Key + String robotUnluckyKey = robotUnluckyPrizePool.getKey(); // 07 机器人非酋奖池Key + String compensationKey = compensationPrizePool.getKey(); + String thirdExplosiveKey = thirdExplosivePrizePool.getKey(); // 新增三级爆率 + this.prizePools.put(anchorKey, anchorPrizePool); + this.prizePools.put(realKey, realPrizePool); + this.prizePools.put(robotKey, robotPrizePool); // 新增机器人用户奖池 + this.prizePools.put(robotLuckyKey, robotLuckyPrizePool); // 新增机器人欧皇奖池 + this.prizePools.put(robotUnluckyKey, robotUnluckyPrizePool); // 新增机器人非酋奖池 + this.prizePools.put(compensationKey, compensationPrizePool); + this.prizePools.put(thirdExplosiveKey, thirdExplosivePrizePool); + // 奖池写入redis(先删再存) + redisCache.deleteObject(anchorKey); + redisCache.deleteObject(realKey); + redisCache.deleteObject(robotKey); // 新增机器人用户奖池 + redisCache.deleteObject(robotLuckyKey); // 删除机器人欧皇奖池 + redisCache.deleteObject(robotUnluckyKey); // 删除机器人非酋奖池 + redisCache.deleteObject(compensationKey); + redisCache.deleteObject(thirdExplosiveKey); + + redisCache.setCacheMap(anchorKey, anchorPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); + redisCache.setCacheMap(realKey, realPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); + redisCache.setCacheMap(robotKey, robotPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); // 新增机器人用户奖池 + redisCache.setCacheMap(robotLuckyKey, robotLuckyPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); // 缓存机器人欧皇奖池 + redisCache.setCacheMap(robotUnluckyKey, robotUnluckyPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); // 缓存机器人非酋奖池 + redisCache.setCacheMap(compensationKey, compensationPrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); + redisCache.setCacheMap(thirdExplosiveKey, thirdExplosivePrizePool.getBoxSpace(), 600, TimeUnit.SECONDS); // 缓存三级爆率奖池 + + log.info("抽奖机预热成功"); + } catch (Exception e) { + log.error("抽奖机预热失败!", e); + } + } + + // 单次抽奖(要加锁) + public String singleLottery(TtUser player, TtBox box) { + return singleLottery(player, box, false, null); + } + + // 单次抽奖(要加锁),skipMock=true 时跳过临时爆率逻辑(如福利箱子) + public String singleLottery(TtUser player, TtBox box, boolean skipMock) { + return singleLottery(player, box, skipMock, null); + } + + // 单次抽奖(对战场景,传递对战模式) + public String singleLottery(TtUser player, TtBox box, String fightModel) { + return singleLottery(player, box, false, fightModel); + } + + // 单次抽奖(对战场景,带skipMock和对战模式) + public String singleLottery(TtUser player, TtBox box, boolean skipMock, String fightModel) { + + // 白号(is_blank_account=1):走自己的白号爆率池并消耗自己的白号池库存,与真实/主播/机器人池完全隔离 + final boolean isBlankAccount = Integer.valueOf(1).equals(player.getIsBlankAccount()); + + // 1 构建抽奖key + UserType userType = UserType.fromCode(player.getUserType()); + if (userType == null) { + return null; + } + + // 白号(is_blank_account=1)走自己的白号爆率池,其余按 user_type 分池 + String playerPoolType = isBlankAccount + ? BoxPoolType.WHITE_PLAYER.getId() + : switch (userType) { + // 仅主播(01)走主播爆率池 + case ANCHOR -> BoxPoolType.ANCHOR_PLAYER.getId(); + case ROBOT_USER -> { + if ("0".equals(fightModel)) { + yield BoxPoolType.ROBOT_PLAYER_LUCKY.getId(); + } else if ("1".equals(fightModel)) { + yield BoxPoolType.ROBOT_PLAYER_UNLUCKY.getId(); + } else { + yield BoxPoolType.ROBOT_PLAYER.getId(); + } + } + // 普通玩家/招商/卡商/超管等其余类型均走真实爆率池 + default -> BoxPoolType.NORMAL_PLAYER.getId(); + }; + + String prizePoolKey = BASE_POOL_KEY + box.getBoxId() + ":" + playerPoolType; + String compensationPrizePoolKey = BASE_POOL_KEY + box.getBoxId() + ":" + BoxPoolType.COMPENSATION.getId(); // 补偿奖池key + String thirdExplosivePrizePoolKey = BASE_POOL_KEY + box.getBoxId() + ":" + BoxPoolType.THIRD_EXPLOSIVE.getId(); // 三级爆率奖池key + + String lotteryLock = LOTTERY_LOCK + box.getBoxId() + "_" + player.getUserType(); + + Boolean lock = false; + int doTry = 0; + while (doTry < 3) { + lock = redisLock.tryLock(lotteryLock, 3L, 10L, TimeUnit.SECONDS); + if (!lock) { + doTry++; + continue; + } + break; + } + + if (!lock) return null; + + // 判断是否存在三级爆率的存储 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(TtBoxThirdExplosiveUser::getBoxId, box.getBoxId()); + query.eq(TtBoxThirdExplosiveUser::getUserId, player.getUserId()); + List users = ttBoxThirdExplosiveUserMapper.selectList(query); + boolean thirdExplosive = CollectionUtils.isNotEmpty(users); + + try { + + // 2 选中奖池 + PrizePool prizePool = this.prizePools.get(prizePoolKey); + PrizePool compensationPrizePool = this.prizePools.get(compensationPrizePoolKey); + PrizePool thirdExplosivePrizePool = this.prizePools.get(thirdExplosivePrizePoolKey); + + // 如果为空,则初始化 + if (ObjectUtil.isNull(prizePool)) { + prizePool = PrizePool.builder() + .key(BASE_POOL_KEY + box.getBoxId() + ":" + playerPoolType) + .boxId(String.valueOf(box.getBoxId())) + .poolType(playerPoolType) + .boxSpace(new HashMap()) + .goodsNumber(0) + .build(); + } + + // 如果为空,则初始化补偿奖池 + if (ObjectUtil.isNull(compensationPrizePool)) { + compensationPrizePool = PrizePool.builder() + .key(BASE_POOL_KEY + box.getBoxId() + ":" + BoxPoolType.COMPENSATION.getId()) + .boxId(String.valueOf(box.getBoxId())) + .poolType(BoxPoolType.COMPENSATION.getId()) + .boxSpace(new HashMap()) + .goodsNumber(0) + .build(); + } + + // 如果为空,则初始化三级爆率 + if (ObjectUtil.isNull(thirdExplosivePrizePool)) { + thirdExplosivePrizePool = PrizePool.builder() + .key(BASE_POOL_KEY + box.getBoxId() + ":" + BoxPoolType.THIRD_EXPLOSIVE.getId()) + .boxId(String.valueOf(box.getBoxId())) + .poolType(BoxPoolType.THIRD_EXPLOSIVE.getId()) + .boxSpace(new HashMap()) + .goodsNumber(0) + .build(); + } + + // 3 检查奖池 + PrizePool finalPrizePool = checkPool(thirdExplosive ? thirdExplosivePrizePool : prizePool); + if (ObjectUtil.isEmpty(finalPrizePool)) return null; + + // 如果满足补偿条件,使用补偿奖池 + TtCompRecord ttCompRecord = ttCompRecordMapper.selectTtCompRecord(player.getUserId(), box.getBoxId()); + if ("3".equals(box.getBoxType()) && Objects.isNull(ttCompRecord)) { + TtCompRecord insertTtCompRecord = new TtCompRecord(); + insertTtCompRecord.setUserId(player.getUserId()); + insertTtCompRecord.setBoxId(box.getBoxId()); + insertTtCompRecord.setCompAmount(0); + ttCompRecordMapper.insertTtCompRecord(insertTtCompRecord); + } + if (!Objects.isNull(ttCompRecord)) { + if ("3".equals(box.getBoxType()) && ttCompRecord.getCompAmount() >= box.getCompAmount()) { + finalPrizePool = checkPool(compensationPrizePool); + // 补偿值清零 + ttCompRecordMapper.deleteTtCompRecord(ttCompRecord.getUserId(), box.getBoxId()); + } + } + // ===== 临时爆率逻辑开始 ===== + if (!skipMock) { + //根据用户的id,从redis中取出用户的临时爆率 + BoxMockCache boxMockCache = boxMockCacheRepository.getMockResultCache(player.getUserId()); + //判空 + if (boxMockCache != null && boxMockCache.getItems() != null && !boxMockCache.getItems().isEmpty()) { + //顺序取第一条,获取最低和最高的价格 + BoxMockCache.MockItem mockItem = boxMockCache.getItems().get(0); + BigDecimal lowPrice = mockItem.getLowPrice(); + BigDecimal highPrice = mockItem.getHighPrice(); + + //批量查询当前奖池内所有库存>0的饰品价格 + List ornamentIdList = new ArrayList<>(); + for (Map.Entry entry : finalPrizePool.getBoxSpace().entrySet()) { + if (entry.getValue() > 0) { + try { + ornamentIdList.add(Long.parseLong(entry.getKey())); + } catch (NumberFormatException e) { + log.warn("临时爆率: ornamentId解析异常 ornamentId={}", entry.getKey()); + } + } + } + + // 从当前奖池中找出 usePrice in [lowPrice, highPrice] 且库存 > 0 的饰品 + List eligibleIds = new ArrayList<>(); + if (!ornamentIdList.isEmpty()) { + //根据id列表批量查询饰品 + List ornaments = ttOrnamentMapper.selectByOrnamentIds(ornamentIdList); + for (TtOrnament o : ornaments) { + if (o.getUsePrice() == null) continue; + if (o.getUsePrice().compareTo(lowPrice) >= 0 + && o.getUsePrice().compareTo(highPrice) <= 0) { + eligibleIds.add(String.valueOf(o.getId())); + } + } + } + + if (!eligibleIds.isEmpty()) { + // 箱子内存在满足价格区间的饰品 → 消费本条 + boxMockCache.getItems().remove(0); + boxMockCacheRepository.setMockResultCache(player.getUserId(), boxMockCache); + + // 从满足条件的饰品中随机返回一个 + String forcedId = eligibleIds.get((int) (Math.random() * eligibleIds.size())); + return forcedId; + + } else { + // 箱子内无满足条件的饰品 → 本条【不消费】,走正常抽奖,下次开箱继续检查 + log.info("用户{}临时爆率不生效: boxId={} 内无[{},{}]区间饰品,本条保留,走正常开奖", + player.getUserId(), box.getBoxId(), lowPrice, highPrice); + } + } + } // ===== 临时爆率逻辑结束 ===== + // 4 抽奖(白号也消耗自己的白号池[B方案],抽干后 checkPool 从 DB 兜底重置;真实/主播/机器人池照常扣减) + return doLottery(finalPrizePool, true); + + } catch (Exception e) { + e.printStackTrace(); + log.warn("用户{}抽奖异常", player.getUserId()); + return null; + } finally { + redisLock.unlock(lotteryLock); + } + + } + + // 抽奖算法。consumeStock=false 时(白号)只计算展示结果,不真实扣减共享奖池库存 + private String doLottery(PrizePool prizePool, boolean consumeStock) { + + Map boxSpace = prizePool.getBoxSpace(); + + log.info("抽奖空间:{}", boxSpace); + + // 2 计算随机数,开始抽奖(5次尝试) + String targetOrnamentId = null; + for (int c = 0; c < 5; c++) { + + int r = 0; + for (int i = 0; i < 5; i++) { + r = RandomUtil.rand(prizePool.getGoodsNumber()); + } + + log.info("抽奖随机数:{}", r); + + int count = 0; // 宝箱的第几个饰品 + boolean flag = false; + for (String ornamentId : boxSpace.keySet()) { + if (flag) break; + for (int i = 0; i < boxSpace.get(ornamentId); i++) { + if (count == r) { + targetOrnamentId = ornamentId; + flag = true; + break; + } + count++; + } + } + + if (!StringUtils.isBlank(targetOrnamentId)) { + break; + } + } + + // 多次尝试依然没抽到东西,按序给一个 + if (ObjectUtil.isEmpty(targetOrnamentId)) { + for (String ornamentId : boxSpace.keySet()) { + if (boxSpace.get(ornamentId) > 0) { + targetOrnamentId = ornamentId; + break; + } + } + } + + // 3 库存减一,更新奖池(白号跳过:不消耗真实库存,真实玩家奖池分布不受影响) + if (consumeStock) { + PrizePool newPool = prizePool.sub(targetOrnamentId); + this.prizePools.put(newPool.getKey(), newPool); + log.info("最终抽奖空间:{}", newPool.getBoxSpace()); + } + + log.info("抽奖奖品:{}", targetOrnamentId); + + return targetOrnamentId; + + } + + // 检查奖池,如果没有自动补充 + private PrizePool checkPool(PrizePool prizePool) { + + boolean isEmpty = true; + Map boxSpace = prizePool.getBoxSpace(); + if (ObjectUtil.isNotNull(boxSpace)) { + Set keySet = boxSpace.keySet(); + for (String ornamentId : keySet) { + if (prizePool.getBoxSpace().get(ornamentId) > 0) { + isEmpty = false; + break; + } + } + } + + // 有库存 + if (prizePool.getGoodsNumber() > 0 && !isEmpty) { + return prizePool; + } + + // 没库存,从redis补充 + Map prizeSpace = redisCache.getCacheMap(prizePool.getKey()); + if (ObjectUtil.isNotEmpty(prizeSpace)) { + int number = 0; + for (Integer p : prizeSpace.values()) { + number = number + p; + } + prizePool.setGoodsNumber(number); + prizePool.setBoxSpace(prizeSpace); + return prizePool; + } + + // redis也没有,从mysql加载 + Boolean tryLoad = false; + for (int i = 0; i < 2; i++) { + + tryLoad = redisLock.tryLock(PRIZE_POOL_LOAD_LOCK, 2L, 7L, TimeUnit.SECONDS); + + if (tryLoad) { + break; + } + } + + if (!tryLoad) return null; + + try { + List list = new LambdaQueryChainWrapper<>(boxOrnamentsMapper) + .eq(TtBoxOrnaments::getBoxId, prizePool.getBoxId()) + .list(); + + HashMap space = new HashMap<>(); + Integer goodsNumber = 0; + String poolType = prizePool.getPoolType(); + var pt = BoxPoolType.fromId(poolType); + if (pt == null) { + pt = BoxPoolType.ANCHOR_PLAYER; + } + for (TtBoxOrnaments item : list) { + switch (pt) { + case BoxPoolType.ANCHOR_PLAYER -> { + space.put(String.valueOf(item.getOrnamentId()), item.getAnchorOdds()); + goodsNumber += item.getAnchorOdds(); + } + case BoxPoolType.NORMAL_PLAYER -> { + space.put(String.valueOf(item.getOrnamentId()), item.getRealOdds()); + goodsNumber += item.getRealOdds(); + } + case BoxPoolType.WHITE_PLAYER -> { + // ponytail: 白号池未在 preheat 预热,靠 checkPool 首次从 DB 懒加载(单次查询);若并发白号量大再补 preheat + int wOdds = item.getWhiteOdds() == null ? 0 : item.getWhiteOdds(); + space.put(String.valueOf(item.getOrnamentId()), wOdds); + goodsNumber += wOdds; + } + case BoxPoolType.ROBOT_PLAYER -> { + space.put(String.valueOf(item.getOrnamentId()), item.getRobotOdds()); + goodsNumber += item.getRobotOdds(); + } + case BoxPoolType.ROBOT_PLAYER_LUCKY -> { + space.put(String.valueOf(item.getOrnamentId()), item.getRobotOddsLucky() == null ? 0 : item.getRobotOddsLucky()); + goodsNumber += item.getRobotOddsLucky() == null ? 0 : item.getRobotOddsLucky(); + } + case BoxPoolType.ROBOT_PLAYER_UNLUCKY -> { + space.put(String.valueOf(item.getOrnamentId()), item.getRobotOddsUnlucky() == null ? 0 : item.getRobotOddsUnlucky()); + goodsNumber += item.getRobotOddsUnlucky() == null ? 0 : item.getRobotOddsUnlucky(); + } + case BoxPoolType.COMPENSATION -> { + space.put(String.valueOf(item.getOrnamentId()), item.getRealCompOdds()); + goodsNumber += item.getRealCompOdds(); + } + case BoxPoolType.THIRD_EXPLOSIVE -> { + space.put(String.valueOf(item.getOrnamentId()), item.getThirdExplosiveOdds()); + goodsNumber += item.getThirdExplosiveOdds(); + } + } + } + + prizePool.setGoodsNumber(goodsNumber); + prizePool.setBoxSpace(space); + redisCache.setCacheMap(prizePool.getKey(), space, 600, TimeUnit.SECONDS); + return prizePool; + } catch (Exception e) { + log.warn("prize load warn"); + return null; + } finally { + redisLock.unlock(PRIZE_POOL_LOAD_LOCK); + } + + } + + // 删除内存奖池 + public boolean removeMemoryPrizePool(String key) { + this.prizePools.remove(key); + return true; + } + + // 删除redis奖池 + public boolean removeRedisPrizePool(String key) { + return redisCache.deleteObject(key); + } + + // 清空box奖池 + public boolean clearBoxPrizePool(Integer boxId) { + for (BoxPoolType poolType : BoxPoolType.allTypes()) { + String key = BASE_POOL_KEY + boxId + ":" + poolType.getId(); + this.removeMemoryPrizePool(key); + this.removeRedisPrizePool(key); + } + return true; + } + + public void resetBoxPrizePool(Integer boxId) { + for (BoxPoolType poolType : BoxPoolType.allTypes()) { + String key = BASE_POOL_KEY + boxId + ":" + poolType.getId(); + this.removeMemoryPrizePool(key); + var prizePool = PrizePool.builder() + .key(key) + .boxId(String.valueOf(boxId)) + .poolType(poolType.getId()) + .boxSpace(new HashMap<>()) + .goodsNumber(0) + .build(); + this.checkPool(prizePool); + } + + } + +} diff --git a/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/core/fight/PrizePool.java b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/core/fight/PrizePool.java new file mode 100644 index 0000000..6fce082 --- /dev/null +++ b/skins-service/service-admin/src/main/java/com/ruoyi/admin/util/core/fight/PrizePool.java @@ -0,0 +1,35 @@ +package com.ruoyi.admin.util.core.fight; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PrizePool { + + private String key; + + private String boxId; + + private String poolType; + + // 物品件数 + private Integer goodsNumber; + + // ornamentId : number + private Map boxSpace; + + // 减库存 + public PrizePool sub(String ornamentId) { + this.goodsNumber--; + this.boxSpace.put(ornamentId, this.boxSpace.get(ornamentId) - 1); + return this; + } + +} diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtAnnouncementMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtAnnouncementMapper.xml new file mode 100644 index 0000000..6138334 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtAnnouncementMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + insert into tt_announcement + + title, + content, + type, + target_user_id, + create_time, + + + #{title}, + #{content}, + #{type}, + #{targetUserId}, + #{createTime}, + + + + + + + update tt_announcement + + title = #{title}, + content = #{content}, + type = #{type}, + target_user_id = #{targetUserId}, + + where announcement_id = #{announcementId} + + + + delete from tt_announcement where announcement_id = #{announcementId} + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxMapper.xml new file mode 100644 index 0000000..b7dffbb --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxMapper.xml @@ -0,0 +1,98 @@ + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxOpenChanceMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxOpenChanceMapper.xml new file mode 100644 index 0000000..595f608 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxOpenChanceMapper.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + select ornament_id, box_id, chance_num from tt_box_open_chance + + + + + + + + insert into tt_box_open_chance + + ornament_id, + box_id, + chance_num, + + + #{ornamentId}, + #{boxId}, + #{chanceNum}, + + + + + update tt_box_open_chance + + box_id = #{boxId}, + chance_num = #{chanceNum}, + + where ornament_id = #{ornamentId} + + + + delete from tt_box_open_chance where ornament_id = #{ornamentId} + + + + delete from tt_box_open_chance where ornament_id in + + #{ornamentId} + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxOrnamentsMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxOrnamentsMapper.xml new file mode 100644 index 0000000..bce425c --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxOrnamentsMapper.xml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + UPDATE tt_box_ornaments + SET + + + anchor_odds = GREATEST(anchor_odds - 1, 0) + + + real_odds = GREATEST(real_odds - 1, 0) + + + real_comp_odds = GREATEST(real_comp_odds - 1, 0) + + + third_explosive_odds = GREATEST(third_explosive_odds - 1, 0) + + + robot_odds = GREATEST(robot_odds - 1, 0) + + + robot_odds_lucky = GREATEST(robot_odds_lucky - 1, 0) + + + robot_odds_unlucky = GREATEST(robot_odds_unlucky - 1, 0) + + + WHERE box_id = #{boxId} AND ornament_id = #{ornamentId} + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxRecordsMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxRecordsMapper.xml new file mode 100644 index 0000000..c19bac3 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxRecordsMapper.xml @@ -0,0 +1,577 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + ttu.nick_name as holder_user_nick_name, + ttu.avatar as avatar, + tbr.holder_user_id as holder_user_id, + + tbr.ornament_id, + tto.name as ornament_name, + tto.image_url, + tbr.ornaments_price, + + tol.level_img as ornament_level_img, + + tbr.`status`, + tbr.create_time, + tbr.update_time + from tt_box_records tbr + left join tt_user ttu on ttu.user_id = tbr.holder_user_id + left join tt_roll_jackpot_ornaments trjo on tbr.ornament_id = trjo.ornaments_id + left join tt_ornament tto on tto.id = tbr.ornament_id + left join tt_ornaments_level tol on tol.id = trjo.ornament_level_id + + AND tbr.roll_id = #{rollId} + + order by tbr.ornaments_price desc + limit #{limit},#{size} + + + + + + + + + update tt_box_records + set status = #{status} + where id IN + + #{id} + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxThirdExplosiveUserMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxThirdExplosiveUserMapper.xml new file mode 100644 index 0000000..d401354 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtBoxThirdExplosiveUserMapper.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtCompRecordMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtCompRecordMapper.xml new file mode 100644 index 0000000..4e17e4e --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtCompRecordMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + INSERT INTO tt_comp_record(user_id, box_id, comp_amount) + VALUES (#{userId}, #{boxId}, #{compAmount}) + + + + update tt_comp_record + + comp_amount = #{compAmount}, + + where user_id = #{userId} and box_id = #{boxId} + + + + delete from tt_comp_record where user_id = #{userId} and box_id = #{boxId} + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtDeliveryRecordMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtDeliveryRecordMapper.xml new file mode 100644 index 0000000..0a7416f --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtDeliveryRecordMapper.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtExponentMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtExponentMapper.xml new file mode 100644 index 0000000..b557113 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtExponentMapper.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFightMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFightMapper.xml new file mode 100644 index 0000000..4e6a923 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFightMapper.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFightRankingRewardMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFightRankingRewardMapper.xml new file mode 100644 index 0000000..d5b3323 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFightRankingRewardMapper.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + select id, name, reward from tt_fight_ranking_reward + + + + + + + + insert into tt_fight_ranking_reward + + id, + name, + reward, + + + #{id}, + #{name}, + #{reward}, + + + + + update tt_fight_ranking_reward + + name = #{name}, + reward = #{reward}, + + where id = #{id} + + + + delete from tt_fight_ranking_reward where id = #{id} + + + + delete from tt_fight_ranking_reward where id in + + #{id} + + + + + TRUNCATE TABLE tt_fight_ranking_reward; + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFightUserMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFightUserMapper.xml new file mode 100644 index 0000000..2b36687 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFightUserMapper.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFirstRechargeMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFirstRechargeMapper.xml new file mode 100644 index 0000000..0307b90 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtFirstRechargeMapper.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + select id, min_amount, ratio, description, create_by, create_time, update_by, update_time from tt_first_recharge + + + + + + + + insert into tt_first_recharge + + id, + min_amount, + ratio, + description, + create_by, + create_time, + update_by, + update_time, + + + #{id}, + #{minAmount}, + #{ratio}, + #{description}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + + + + + update tt_first_recharge + + min_amount = #{minAmount}, + ratio = #{ratio}, + description = #{description}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + + where id = #{id} + + + + delete from tt_first_recharge where id = #{id} + + + + delete from tt_first_recharge where id in + + #{id} + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtOrderMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtOrderMapper.xml new file mode 100644 index 0000000..6f8ce1e --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtOrderMapper.xml @@ -0,0 +1,222 @@ + + + + + + + + + + + + + select + user_id, + nick_name, + avatar, + user_type + from tt_user ttu + + + ttu.user_id in + + #{uid} + + + + + ttu.user_id = -1 + + + + + + + select + any_value(tto.user_id) as user_id, + sum(tto.total_amount) as recharge + from tt_order tto + left join tt_user ttu on tto.user_id = ttu.user_id + + + tto.user_id in + + #{uid} + + + + + and tto.create_time between #{beginTime} and #{endTime} + + and tto.status = 4 + + group by user_id + + + + select + any_value(tube.user_id) as user_id, + sum(tube.total) as total, + sum(tube.credits) as credits, + sum(tube.amount) as amount + from tt_user_blend_ercash tube + left join tt_user ttu on ttu.user_id = tube.user_id + + + tube.user_id in + + #{uid} + + + + and tube.create_time between #{beginTime} and #{endTime} + + and type = 0 + + group by user_id + + + + + + + select + t2.user_id as user_id, + t2.nick_name as nick_name, + t2.avatar as avatar, + t2.user_type as user_type, + t2.recharge as recharge + from ( + + select + any_value(t1.user_id) as user_id, + any_value(t1.nick_name) as nick_name, + any_value(t1.user_type) as user_type, + any_value(t1.avatar) as avatar, + sum(t1.total_amount) as recharge + from ( + select + tto.user_id as user_id, + ttu.nick_name as nick_name, + ttu.user_type as user_type, + ttu.avatar as avatar, + tto.total_amount as total_amount + from tt_order tto + left join tt_user ttu on tto.user_id = ttu.user_id + + + tto.user_id in + + #{uid} + + + + + and tto.create_time between #{beginTime} and #{endTime} + + and tto.status in (1,4) + + ) t1 + group by t1.user_id + + ) t2 + order by t2.recharge + asc + desc + limit #{limit},#{size} + + + + + + select + any_value(t1.user_id) as user_id, + any_value(t1.nick_name) as nick_name, + any_value(t1.avatar) as avatar, + any_value(t1.user_type) as user_type, + sum(t1.total) as be_consume, + sum(t1.credits) as credits_consume, + sum(t1.amount) as amount_consume + from ( + select + tube.user_id as user_id, + tube.total as total, + tube.credits as credits, + tube.amount as amount, + ttu.nick_name as nick_name, + ttu.avatar as avatar, + ttu.user_type as user_type + from tt_user_blend_ercash tube + left join tt_user ttu on ttu.user_id = tube.user_id + + + tube.user_id in + + #{uid} + + + + and tube.create_time between #{beginTime} and #{endTime} + + + ) t1 + group by t1.user_id + +-- select +-- user_id, +-- be_consume, +-- credits_consume, +-- amount_consume, +-- nick_name, +-- avatar, +-- user_type +-- from ( +-- +-- ) t2 + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtOrnamentMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtOrnamentMapper.xml new file mode 100644 index 0000000..dff9b08 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtOrnamentMapper.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + update tt_ornament + + name = #{name}, + short_name = #{name}, + use_price = #{usePrice}, + image_url = #{imageUrl}, + quantity = #{quantity}, + update_time = sysdate() + + where id = #{id} + + + + DELETE FROM tt_ornament WHERE id IN + + #{id} + + + + + UPDATE tt_ornament + SET store_price = price * #{zbtParities} * #{usePricePremiumRate}, + update_time = NOW() + WHERE price IS NOT NULL AND price > 0 + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtOrnamentsZBTMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtOrnamentsZBTMapper.xml new file mode 100644 index 0000000..aba1ca8 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtOrnamentsZBTMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtPromotionRecordMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtPromotionRecordMapper.xml new file mode 100644 index 0000000..367cd44 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtPromotionRecordMapper.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtPromotionUpdateMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtPromotionUpdateMapper.xml new file mode 100644 index 0000000..c661132 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtPromotionUpdateMapper.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRechargeRankingRewardMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRechargeRankingRewardMapper.xml new file mode 100644 index 0000000..ba919fe --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRechargeRankingRewardMapper.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + select id, name, reward from tt_recharge_ranking_reward + + + + + + + + insert into tt_recharge_ranking_reward + + id, + name, + reward, + + + #{id}, + #{name}, + #{reward}, + + + + + update tt_recharge_ranking_reward + + name = #{name}, + reward = #{reward}, + + where id = #{id} + + + + delete from tt_recharge_ranking_reward where id = #{id} + + + + delete from tt_recharge_ranking_reward where id in + + #{id} + + + + + TRUNCATE TABLE tt_recharge_ranking_reward; + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRechargeRecordMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRechargeRecordMapper.xml new file mode 100644 index 0000000..89bf008 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRechargeRecordMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRedPackMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRedPackMapper.xml new file mode 100644 index 0000000..96a3f15 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRedPackMapper.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + select id, title, pack_type, recharge_limit, pack_count, valid_count, value_min, value_max, begin_time, end_time, status, create_time, update_time from tt_red_pack + + + + + + + + insert into tt_red_pack + + title, + pack_type, + recharge_limit, + pack_count, + valid_count, + value_min, + value_max, + begin_time, + end_time, + status, + create_time, + update_time, + + + #{title}, + #{packType}, + #{rechargeLimit}, + #{packCount}, + #{validCount}, + #{valueMin}, + #{valueMax}, + #{beginTime}, + #{endTime}, + #{status}, + #{createTime}, + #{updateTime}, + + + + + update tt_red_pack + + title = #{title}, + pack_type = #{packType}, + recharge_limit = #{rechargeLimit}, + pack_count = #{packCount}, + valid_count = #{validCount}, + value_min = #{valueMin}, + value_max = #{valueMax}, + begin_time = #{beginTime}, + end_time = #{endTime}, + status = #{status}, + create_time = #{createTime}, + update_time = #{updateTime}, + + where id = #{id} + + + + delete from tt_red_pack where id = #{id} + + + + delete from tt_red_pack where id in + + #{id} + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRedPacketMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRedPacketMapper.xml new file mode 100644 index 0000000..85e26d9 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRedPacketMapper.xml @@ -0,0 +1,54 @@ + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRedPacketRecordMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRedPacketRecordMapper.xml new file mode 100644 index 0000000..9ce6255 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRedPacketRecordMapper.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtReplacementRecordMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtReplacementRecordMapper.xml new file mode 100644 index 0000000..2b312ba --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtReplacementRecordMapper.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + select id, uid, uname, oids, award_oid, award_oname, award_oprice,award_oimg, time, create_time, update_time from tt_replacement_record + + + + + + + + insert into tt_replacement_record + + uid, + uname, + oids, + award_oid, + award_oname, + award_oprice, + award_oimg, + time, + create_time, + update_time, + + + #{uid}, + #{uname}, + #{oids}, + #{awardOid}, + #{awardOname}, + #{awardOprice}, + #{awardOimg}, + #{time}, + #{createTime}, + #{updateTime}, + + + + + update tt_replacement_record + + uid = #{uid}, + uname = #{uname}, + oids = #{oids}, + award_oid = #{awardOid}, + award_oname = #{awardOname}, + award_oprice = #{awardOprice}, + time = #{time}, + create_time = #{createTime}, + update_time = #{updateTime}, + + where id = #{id} + + + + delete from tt_replacement_record where id = #{id} + + + + delete from tt_replacement_record where id in + + #{id} + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightGroupBoxMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightGroupBoxMapper.xml new file mode 100644 index 0000000..6cc949d --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightGroupBoxMapper.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + select b.group_id, b.box_id, b.box_num, bx.box_name + from tt_robot_fight_group_box b + left join tt_box bx on bx.box_id = b.box_id + + + + + + + + + + insert into tt_robot_fight_group_box + + group_id, + box_id, + box_num, + + + #{groupId}, + #{boxId}, + #{boxNum}, + + + + + update tt_robot_fight_group_box + + box_id = #{boxId}, + box_num = #{boxNum}, + + where group_id = #{groupId} and box_id = #{oldBoxId} + + + + delete from tt_robot_fight_group_box where group_id = #{groupId} and box_id = #{boxId} + + + + delete from tt_robot_fight_group_box where group_id = #{groupId} and box_id in + + #{boxId} + + + + + delete from tt_robot_fight_group_box where group_id = #{groupId} + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightGroupMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightGroupMapper.xml new file mode 100644 index 0000000..80aef81 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightGroupMapper.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + select group_id, group_name, seat_num, model, price_config_id from tt_robot_fight_group + + + + + + + + insert into tt_robot_fight_group + + group_id, + group_name, + seat_num, + model, + price_config_id, + + + #{groupId}, + #{groupName}, + #{seatNum}, + #{model}, + #{priceConfigId}, + + + + + update tt_robot_fight_group + + group_name = #{groupName}, + seat_num = #{seatNum}, + model = #{model}, + price_config_id = #{priceConfigId}, + + where group_id = #{groupId} + + + + delete from tt_robot_fight_group where group_id = #{groupId} + + + + delete from tt_robot_fight_group where group_id in + + #{groupId} + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightJoinConfigMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightJoinConfigMapper.xml new file mode 100644 index 0000000..83b8e2f --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightJoinConfigMapper.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + select id, min_amount, max_amount, join_seconds, random_seconds, status, remark, create_time, update_time from tt_robot_fight_join_config + + + + + + + + insert into tt_robot_fight_join_config + + min_amount, + max_amount, + join_seconds, + random_seconds, + status, + remark, + create_time, + + + #{minAmount}, + #{maxAmount}, + #{joinSeconds}, + #{randomSeconds}, + #{status}, + #{remark}, + now(), + + + + + update tt_robot_fight_join_config + + min_amount = #{minAmount}, + max_amount = #{maxAmount}, + join_seconds = #{joinSeconds}, + random_seconds = #{randomSeconds}, + status = #{status}, + remark = #{remark}, + update_time = now(), + + where id = #{id} + + + + delete from tt_robot_fight_join_config where id = #{id} + + + + delete from tt_robot_fight_join_config where id in + + #{id} + + + diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightPriceConfigMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightPriceConfigMapper.xml new file mode 100644 index 0000000..3478a9a --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRobotFightPriceConfigMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + select id, price_limit, target_count, sort_order, status, remark, create_time, update_time from tt_robot_fight_price_config + + + + + + + + insert into tt_robot_fight_price_config + + price_limit, + target_count, + sort_order, + status, + remark, + create_time, + + + #{priceLimit}, + #{targetCount}, + #{sortOrder}, + #{status}, + #{remark}, + now(), + + + + + update tt_robot_fight_price_config + + price_limit = #{priceLimit}, + target_count = #{targetCount}, + sort_order = #{sortOrder}, + status = #{status}, + remark = #{remark}, + update_time = now(), + + where id = #{id} + + + + delete from tt_robot_fight_price_config where id = #{id} + + + + delete from tt_robot_fight_price_config where id in + + #{id} + + + diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollJackpotOrnamentsMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollJackpotOrnamentsMapper.xml new file mode 100644 index 0000000..cd1d940 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollJackpotOrnamentsMapper.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + select + jackpot_id + from + tt_roll + where id = #{rollId} + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollMapper.xml new file mode 100644 index 0000000..be526ba --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollMapper.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollUserMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollUserMapper.xml new file mode 100644 index 0000000..79148e2 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollUserMapper.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollUserPrizeMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollUserPrizeMapper.xml new file mode 100644 index 0000000..2f599d0 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtRollUserPrizeMapper.xml @@ -0,0 +1,67 @@ + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtTaskCenterMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtTaskCenterMapper.xml new file mode 100644 index 0000000..76b4401 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtTaskCenterMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + select task_id, task_name, description, identify, status, create_by, create_time, update_by, update_time from tt_task_center + + + + + + + + + + insert into tt_task_center + + task_name, + description, + identify, + status, + create_by, + create_time, + update_by, + update_time, + + + #{taskName}, + #{description}, + #{identify}, + #{status}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + + + + + update tt_task_center + + task_name = #{taskName}, + description = #{description}, + identify = #{identify}, + status = #{status}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + + where task_id = #{taskId} + + + + delete from tt_task_center where task_id = #{taskId} + + + + delete from tt_task_center where task_id in + + #{taskId} + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtTimeRollMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtTimeRollMapper.xml new file mode 100644 index 0000000..9037997 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtTimeRollMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtTransferApplyMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtTransferApplyMapper.xml new file mode 100644 index 0000000..120c6b0 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtTransferApplyMapper.xml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND ta.batch_id LIKE CONCAT('%',#{batchId},'%') + AND ta.from_user_id = #{fromUserId} + AND tu_from.nick_name LIKE CONCAT('%',#{fromNickName},'%') + AND ta.to_user_id = #{toUserId} + AND tu_to.nick_name LIKE CONCAT('%',#{toNickName},'%') + AND ta.ornament_name LIKE CONCAT('%',#{ornamentName},'%') + AND ta.status = #{status} + + AND ta.status IN + #{s} + + AND (tu_from.phone_number = #{phoneNumber} OR tu_to.phone_number = #{phoneNumber}) + + = #{startTime} AND ta.create_time <= #{endTime}) ]]> + + + + + + + + + + + + + diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUpgradeFailOrnamentsMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUpgradeFailOrnamentsMapper.xml new file mode 100644 index 0000000..61c2f23 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUpgradeFailOrnamentsMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUpgradeOrnamentsMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUpgradeOrnamentsMapper.xml new file mode 100644 index 0000000..0649143 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUpgradeOrnamentsMapper.xml @@ -0,0 +1,49 @@ + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUpgradeRecordMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUpgradeRecordMapper.xml new file mode 100644 index 0000000..f0a0dcd --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUpgradeRecordMapper.xml @@ -0,0 +1,77 @@ + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserAmountRecordsMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserAmountRecordsMapper.xml new file mode 100644 index 0000000..60e08e4 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserAmountRecordsMapper.xml @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserBlendErcashMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserBlendErcashMapper.xml new file mode 100644 index 0000000..a97bdcb --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserBlendErcashMapper.xml @@ -0,0 +1,846 @@ + + + + + + + + + + + + + + and create_time between #{startTime} and #{endTime} + + + and user_id = #{userId} + + + and source = #{source} + + + and `type` = #{type} + + + and amount != 0 + and amount is not null + + + and credits != 0 + and credits is not null + + + + + + + + + + + + + + + + + + + select + user_id, + nick_name, + avatar, + user_type + from tt_user ttu + + + ttu.user_id in + + #{uid} + + + + + ttu.user_id = -1 + + + + + + + + select + any_value(tube.user_id) as user_id, + sum(tube.total) as total, + sum(tube.credits) as credits, + sum(tube.amount) as amount + from ( + SELECT user_id, amount, credits, total + FROM tt_user_blend_ercash + WHERE type = 0 + + AND user_id in + + #{uid} + + + + and create_time between #{beginTime} and #{endTime} + + UNION ALL + SELECT user_id, amount, credits, total + FROM tt_user_blend_ercash_archive + WHERE type = 0 + + AND user_id in + + #{uid} + + + + and create_time between #{beginTime} and #{endTime} + + ) tube + group by user_id + + + + + select + any_value(tto.user_id) as user_id, + sum(tto.total_amount) as recharge + from tt_order tto + left join tt_user ttu on tto.user_id = ttu.user_id + + + tto.user_id in + + #{uid} + + + + + and tto.create_time between #{beginTime} and #{endTime} + + and tto.status = 4 + + group by user_id + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserCreditsRecordsMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserCreditsRecordsMapper.xml new file mode 100644 index 0000000..0e54e80 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserCreditsRecordsMapper.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserMapper.xml new file mode 100644 index 0000000..5a8f4eb --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserMapper.xml @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select user_id, + user_name, + nick_name, + user_type, + email, + phone_number, + avatar, + password, + account_amount, + account_credits, + invitation_code, + parent_id, + vip_level, + promotion_level, + status, + delivery_status, + steam_id, + transaction_link, + real_name, + id_num, + certify_id, + is_real_check, + commission_rate, + login_ip, + login_date, + create_by, + create_time, + update_by, + update_time, + remark, + del_flag + from tt_user + + + + + + + + + + update tt_user + set bean = (bean + #{money}) + where user_id = #{userId} + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserSourceReportMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserSourceReportMapper.xml new file mode 100644 index 0000000..a6b3b77 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtUserSourceReportMapper.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtWelfareMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtWelfareMapper.xml new file mode 100644 index 0000000..d731d51 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtWelfareMapper.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + select welfare_id, welfare_name, type, vip_level, box_id, create_time, update_time from tt_welfare + + + + + + + + insert into tt_welfare + + welfare_name, + type, + vip_level, + box_id, + create_time, + update_time, + + + #{welfareName}, + #{type}, + #{vipLevel}, + #{boxId}, + #{createTime}, + #{updateTime}, + + + + + update tt_welfare + + welfare_name = #{welfareName}, + type = #{type}, + vip_level = #{vipLevel}, + box_id = #{boxId}, + create_time = #{createTime}, + update_time = #{updateTime}, + + where welfare_id = #{welfareId} + + + + delete from tt_welfare where welfare_id = #{welfareId} + + + + delete from tt_welfare where welfare_id in + + #{welfareId} + + + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtWelfareMonthlyRechargesMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtWelfareMonthlyRechargesMapper.xml new file mode 100644 index 0000000..181ec05 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TtWelfareMonthlyRechargesMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + select welfare_id, welfare_name, type, vip_level, amount, box_id from tt_welfare_monthly_recharges + + + + + + + + insert into tt_welfare_monthly_recharges + + welfare_id, + welfare_name, + type, + vip_level, + amount, + box_id, + + + #{welfareId}, + #{welfareName}, + #{type}, + #{vipLevel}, + #{amount}, + #{boxId}, + + + + + update tt_welfare_monthly_recharges + + welfare_name = #{welfareName}, + type = #{type}, + vip_level = #{vipLevel}, + amount = #{amount}, + box_id = #{boxId}, + + where welfare_id = #{welfareId} + + + + delete from tt_welfare_monthly_recharges where welfare_id = #{welfareId} + + + + delete from tt_welfare_monthly_recharges where welfare_id in + + #{welfareId} + + + \ No newline at end of file diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.xml new file mode 100644 index 0000000..1cbc733 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + INNER JOIN tt_user u ON r.user_id = u.user_id + AND IFNULL(u.is_blank_account, 0) != 1 + AND u.user_type != '03' + AND u.`status` != '1' + AND u.del_flag = '0' + + + + + + AND r.lottery_id = #{lotteryId} + + = #{minPrice} ]]> + + + + + + + + + + + diff --git a/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/WebsiteSetupMapper.xml b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/WebsiteSetupMapper.xml new file mode 100644 index 0000000..366ea99 --- /dev/null +++ b/skins-service/service-admin/src/main/resources/com/ruoyi/admin/mapper/WebsiteSetupMapper.xml @@ -0,0 +1,80 @@ + + + + + + + + + + TRUNCATE TABLE sys_job_log; + + + TRUNCATE TABLE sys_logininfor; + + + TRUNCATE TABLE sys_oper_log; + + + TRUNCATE TABLE tt_box_records; + + + TRUNCATE TABLE tt_fight; + + + TRUNCATE TABLE tt_fight_result; + + + TRUNCATE TABLE tt_fight_user; + + + TRUNCATE TABLE tt_vip_level; + + + TRUNCATE TABLE tt_promotion_level; + + + TRUNCATE TABLE tt_ornaments_level; + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/pom.xml b/skins-service/service-playingmethod/pom.xml new file mode 100644 index 0000000..f72988b --- /dev/null +++ b/skins-service/service-playingmethod/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + com.ruoyi + skins-service + 4.8.2 + + + service-playingmethod + + + + + com.ruoyi + ruoyi-framework + + + + com.ruoyi + service-admin + 4.8.2 + + + + com.ruoyi + service-user + 4.8.2 + + + + + com.ruoyi + message-service + 4.8.2 + + + + com.ruoyi + reward-service + 4.8.2 + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/config/FightRabbitConsumer.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/config/FightRabbitConsumer.java new file mode 100644 index 0000000..6d3886c --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/config/FightRabbitConsumer.java @@ -0,0 +1,42 @@ +package com.ruoyi.playingmethod.config; + +import com.rabbitmq.client.Channel; +import com.ruoyi.admin.service.TtFightService; +import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig; +import com.ruoyi.common.rabbitmq.config.FightQueueConfig; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.playingmethod.service.ApiRollService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Date; + +@Component +@Slf4j +public class FightRabbitConsumer { + + // private final TtFightService ttFightService; + // + // public FightRabbitConsumer(TtFightService ttFightService) { + // this.ttFightService = ttFightService; + // } + // + // @RabbitListener(queues = FightQueueConfig.FIGHT_QUEUE) + // public void rollReceiveDelayed(String fightId,Message message, Channel channel) throws IOException { + // log.info("===============接收对战队列消息====================" + fightId); + // + // // 通知 MQ 消息已被接收,可以ACK(从队列中删除)了 + // channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); + // try { + // ttFightService.endFight(fightId); + // } catch (Exception e) { + // log.error("============消费失败,尝试消息补发再次消费!=============="); + // log.error(e.getMessage()); + // // 消息补发 true=消息退回到queue(有可能被其它的consumer(集群)接收到) false=只补发给当前的consumer + // channel.basicRecover(false); + // } + // } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/config/LotteryMachineConfig.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/config/LotteryMachineConfig.java new file mode 100644 index 0000000..efcdf5a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/config/LotteryMachineConfig.java @@ -0,0 +1,39 @@ +package com.ruoyi.playingmethod.config; + +import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.admin.util.core.fight.PrizePool; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.redis.config.RedisLock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; + +@Configuration +public class LotteryMachineConfig { + + @Autowired + RedisLock redisLock; + @Autowired + RedisCache redisCache; + @Autowired + TtBoxOrnamentsMapper boxOrnamentsMapper; + + @Bean + public LotteryMachine lotteryMachine(){ + + LotteryMachine machine = LotteryMachine.builder() + .boxOrnamentsMapper(boxOrnamentsMapper) + .redisLock(redisLock) + .redisCache(redisCache) + .prizePools(new HashMap()) + .build(); + + machine.preheat(); + + return machine; + } + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/config/RollRabbitConsumer.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/config/RollRabbitConsumer.java new file mode 100644 index 0000000..08aa923 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/config/RollRabbitConsumer.java @@ -0,0 +1,63 @@ +package com.ruoyi.playingmethod.config; + +import cn.hutool.core.util.ObjectUtil; +import com.rabbitmq.client.Channel; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.playingmethod.service.ApiRollService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.ExchangeTypes; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.*; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@Slf4j +public class RollRabbitConsumer { + + private final ApiRollService apiRollService; + + public RollRabbitConsumer(ApiRollService apiRollService) { + this.apiRollService = apiRollService; + } + + @RabbitListener(queues = DelayedQueueConfig.DLK_QUEUE) + public void rollReceiveDelayed(Message message, Channel channel) throws IOException { + + long deliveryTag = message.getMessageProperties().getDeliveryTag(); + + Integer rollId = Integer.valueOf(new String(message.getBody())); + log.info("【死信队列消费】 roll房开奖,接受到的Roll房ID:{}", rollId); + + try { + // Roll房分配结果逻辑 + R r = apiRollService.endROLL(rollId); + if (!r.getCode().equals(200)) { + log.error("roll开奖 消费失败,尝试消息补发再次消费!" + r.getMsg()); + channel.basicNack(deliveryTag, true, true); + return; + } + if (ObjectUtil.isNotEmpty(r.getMsg())) log.info("{}", r.getMsg()); + + // 通知 MQ 消息已被接收,可以ACK(从队列中删除)了 + channel.basicAck(deliveryTag, true); + } catch (Exception e) { + log.error("roll开奖 消费失败,尝试消息补发再次消费!"); + log.error(e.getMessage()); + channel.basicNack(deliveryTag, true, true); + } + } + + //@RabbitListener(queues = DelayedQueueConfig.DLK_QUEUE) + public void t1(Message message, Channel channel) throws IOException { + + // long deliveryTag = message.getMessageProperties().getDeliveryTag(); + + log.info("mq 测试"); + + System.out.println(new String(message.getBody())); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/BoxIdRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/BoxIdRequest.java new file mode 100644 index 0000000..321e89a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/BoxIdRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class BoxIdRequest { + /** 宝箱ID */ + private Integer boxId; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/ClaimWelfareRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/ClaimWelfareRequest.java new file mode 100644 index 0000000..439dd03 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/ClaimWelfareRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class ClaimWelfareRequest { + private Integer welfareId; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/ClassicBoxRecordPageRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/ClassicBoxRecordPageRequest.java new file mode 100644 index 0000000..8802220 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/ClassicBoxRecordPageRequest.java @@ -0,0 +1,42 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.Min; + +/** + * 经典开箱记录统一查询请求 + * + * 使用规则: + * - 不传任何参数:查询全服记录(匿名访问) + * - boxId 为空:查询全部箱子的记录 + * - boxId 不为空:仅查询当前选中箱子的记录 + */ +@ApiModel("经典开箱记录查询参数") +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class ClassicBoxRecordPageRequest { + + @ApiModelProperty("页码,默认 1") + @Min(value = 1, message = "页码最小 1") + private Integer page = 1; + + @ApiModelProperty("每页条数,默认 20") + @Range(min = 1, max = 50, message = "每页条数 1-50") + private Integer size = 20; + + @ApiModelProperty("选中箱子 ID:为空则查询全部箱子的记录,不为空则仅查询该箱子") + private Integer boxId; + + @ApiModelProperty("排序方式:0-时间降序,1-时间升序,2-价格降序,默认 0") + @Builder.Default + private Integer orderByFie = 0; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/FightHallListRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/FightHallListRequest.java new file mode 100644 index 0000000..ecdeff1 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/FightHallListRequest.java @@ -0,0 +1,34 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 对战大厅列表查询请求 + */ +@Data +public class FightHallListRequest { + + private Integer pageNum; + + private Integer pageSize; + + /** 房间分类(含义随mode变化):mode=1时0-等待中 1-进行中 2-已结束 3-我的 4-全部;mode=2时0-超神(历史全部) 1-最大金额(今日) 2-最大增长(今日) 3-最大倍数(今日);mode=3时4-全服 3-我的 */ + private String category; + + /** 房间模式:0-欧皇 1-非酋 */ + private String model; + + /** 筛选条件:mode=1时0-全部 1-初级(≤200) 2-高级(200~800) 3-巅峰(800+);mode=3时0-全部 1-初级(≤200) 2-高级(200~800) 3-巅峰(800+) */ + private String filter; + + /** 房间号搜索,默认不传 */ + private String keyword; + + /** 房间名称搜索(创建人昵称),默认不传 */ + private String roomName; + + /** 模式:1-战场 2-百佳 3-历史 */ + private String mode; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/FightIdRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/FightIdRequest.java new file mode 100644 index 0000000..8b869fe --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/FightIdRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class FightIdRequest { + + private Integer fightId; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/FightRoomInfoRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/FightRoomInfoRequest.java new file mode 100644 index 0000000..8f5fd98 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/FightRoomInfoRequest.java @@ -0,0 +1,11 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +/** + * 对战房间信息查询请求 + */ +@Data +public class FightRoomInfoRequest { + private Integer fightId; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetBoxListRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetBoxListRequest.java new file mode 100644 index 0000000..f76d697 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetBoxListRequest.java @@ -0,0 +1,11 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class GetBoxListRequest { + + private Integer boxTypeId; + private String homeFlag; + private String boxType; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetFightBoxListRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetFightBoxListRequest.java new file mode 100644 index 0000000..077e58a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetFightBoxListRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class GetFightBoxListRequest { + /** 宝箱类型ID(可选) */ + private Integer boxTypeId; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetFightListRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetFightListRequest.java new file mode 100644 index 0000000..8bac643 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetFightListRequest.java @@ -0,0 +1,13 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class GetFightListRequest { + /** 模式 */ + private String model; + /** 状态 */ + private String status; + /** 用户ID */ + private Integer userId; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackByKeyRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackByKeyRequest.java new file mode 100644 index 0000000..39e3b50 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackByKeyRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class GetRedPackByKeyRequest { + private String redPackKey; +} \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackDetailRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackDetailRequest.java new file mode 100644 index 0000000..6e35854 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackDetailRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class GetRedPackDetailRequest { + private Long recordId; +} \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackListRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackListRequest.java new file mode 100644 index 0000000..5e548d2 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackListRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class GetRedPackListRequest { + private Integer type; +} \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackRequest.java new file mode 100644 index 0000000..d5c52de --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRedPackRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class GetRedPackRequest { + private Long redPackId; +} \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRollListParamRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRollListParamRequest.java new file mode 100644 index 0000000..3260fad --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/GetRollListParamRequest.java @@ -0,0 +1,26 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GetRollListParamRequest { + private String rollName; + + // 按ID精确搜索 + private Integer rollId; + + // 0官方 1主播 + @ApiModelProperty("0官方 1主播") + private Integer rollType; + + // 0全部 1可参与 2我的 3已结束 + @ApiModelProperty("0全部 1可参与 2我的 3已结束") + private Integer rollStatus; + + private Integer userId; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/OpenBoxRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/OpenBoxRequest.java new file mode 100644 index 0000000..49d3075 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/OpenBoxRequest.java @@ -0,0 +1,18 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@Data +public class OpenBoxRequest { + @NotNull(message = "宝箱ID不能为空") + private Integer boxId; + + @NotNull(message = "开箱数量不能为空") + @Min(value = 1, message = "开箱数量不能小于1") + @Max(value = 50, message = "单次开箱数量不能超过50") + private Integer num; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/OpenWelfareBoxRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/OpenWelfareBoxRequest.java new file mode 100644 index 0000000..f56c03d --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/OpenWelfareBoxRequest.java @@ -0,0 +1,16 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import java.util.List; + +@Data +public class OpenWelfareBoxRequest { + + /** 要开启的福利箱子库存ID列表 */ + @NotEmpty(message = "请选择要打开的箱子") + @Size(min = 1, max = 15, message = "每次最少开1个,最多开15个箱子") + private List boxInventoryIds; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/RollIdRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/RollIdRequest.java new file mode 100644 index 0000000..9388f9f --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/RollIdRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class RollIdRequest { + private Integer rollId; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/TenTopFightRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/TenTopFightRequest.java new file mode 100644 index 0000000..b785689 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/TenTopFightRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class TenTopFightRequest { + //十佳对战, 1:最大金额,2:最大增长,3:最大倍数 + private Integer maxType; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/TenTopUpgradeRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/TenTopUpgradeRequest.java new file mode 100644 index 0000000..906e445 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/contract/api/request/TenTopUpgradeRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.contract.api.request; + +import lombok.Data; + +@Data +public class TenTopUpgradeRequest { + private Boolean isVictory; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiBattleRoyaleController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiBattleRoyaleController.java new file mode 100644 index 0000000..25075bd --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiBattleRoyaleController.java @@ -0,0 +1,9 @@ +package com.ruoyi.playingmethod.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/battleRoyale") +public class ApiBattleRoyaleController { +} \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiBindBoxController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiBindBoxController.java new file mode 100644 index 0000000..3a06f6f --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiBindBoxController.java @@ -0,0 +1,383 @@ +package com.ruoyi.playingmethod.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtUserAmountRecordsService; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RateLimiter; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.enums.LimitType; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.PageUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.dto.boxRecords.BoxRecordPageRequest; +import com.ruoyi.domain.dto.boxRecords.TenTopQuery; +import com.ruoyi.domain.dto.boxRecords.queryCondition; +import com.ruoyi.domain.entity.recorde.TtUserAmountRecords; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.*; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.playingmethod.contract.api.request.BoxIdRequest; +import com.ruoyi.playingmethod.contract.api.request.GetBoxListRequest; +import com.ruoyi.playingmethod.contract.api.request.OpenBoxRequest; +import com.ruoyi.playingmethod.contract.api.request.ClassicBoxRecordPageRequest; +import com.ruoyi.playingmethod.service.ApiBindBoxService; +import com.ruoyi.playingmethod.service.ApiBoxRecordsService; +import com.ruoyi.system.service.ISysConfigService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Api(tags = "玩转盲盒模式") +@Slf4j +@RestController +@RequestMapping("/api/bindbox") +public class ApiBindBoxController extends BaseController { + + private final ISysConfigService sysConfigService; + private final ApiBindBoxService bindBoxService; + private final TtUserService userService; + private final TtBoxService boxService; + private final TtUserAmountRecordsService userAmountRecordsService; + + public ApiBindBoxController(ISysConfigService sysConfigService, + ApiBindBoxService bindBoxService, + TtUserAmountRecordsService userAmountRecordsService, + TtUserService userService, + TtBoxService boxService) { + this.sysConfigService = sysConfigService; + this.bindBoxService = bindBoxService; + this.userService = userService; + this.userAmountRecordsService = userAmountRecordsService; + this.boxService = boxService; + } + + @Autowired + private ApiBoxRecordsService apiBoxRecordsService; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private RedisLock redisLock; + + @ApiOperation("获取宝箱信息") + @Anonymous + @PostMapping("/boxData") + public R getBoxData(@RequestBody BoxIdRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance"); + if ("1".equals(bindBoxMaintenance)) { + return R.fail("盲盒开箱功能正在维护中......"); + } + TtBoxA boxData = bindBoxService.getBoxData(request.getBoxId()); + return R.ok(boxData); + } + + @ApiOperation("获取补偿箱信息") + @PostMapping("/getCompBoxData") + public AjaxResult getCompBoxData(@RequestBody BoxIdRequest request) { + List compBoxData = bindBoxService.getCompBoxData(request.getBoxId()); + return success(compBoxData); + } + + @ApiOperation("获取宝箱列表") + @ApiImplicitParams({ + @ApiImplicitParam(name = "boxType", value = "宝箱类型(1普通箱 2对战箱 3补偿箱 4九宫格)", dataType = "String", dataTypeClass = String.class) + }) + @Anonymous + @PostMapping("/getBoxList") + public R> getBoxList(@RequestBody(required=false) GetBoxListRequest request) { + Integer boxTypeId = request != null ? request.getBoxTypeId() : null; + String homeFlag = request != null ? request.getHomeFlag() : null; + String boxType = request != null ? request.getBoxType() : null; + // 经典开箱接口只返回经典宝箱(is_fight = '1'),不混入对战箱 + List boxData = bindBoxService.getBoxList(boxTypeId, homeFlag, boxType, "1"); + if (boxData.isEmpty()) { + return R.ok(null, "没有匹配的数据。"); + } + return R.ok(boxData); + } + + /** + * 盲盒开箱模式开箱 + */ + @ApiOperation("盲盒开箱") + @UpdateUserCache + @RateLimiter(time = 60, count = 30, limitType = LimitType.IP) + @PostMapping("/openBox") + public R openBox(@RequestBody @Validated OpenBoxRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance"); + if ("1".equals(bindBoxMaintenance)) { + return R.fail("盲盒开箱功能正在维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + TtBox ttBox = boxService.getById(request.getBoxId()); + if (ttBox == null) return R.fail("宝箱不存在"); + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired(); + } + // 先做充值校验:新用户(从未充值)直接拦截,返回 703。 + // 归档兼容:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL, + // 避免历史充值流水被归档后误判为“未充值”。 + BigDecimal cacheRecharge = ttUser.getTotalRechargeAmount(); + boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0; + if (!recharged) { + BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource( + Collections.singletonList(ttUser.getUserId()), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0; + } + if (!recharged) { + return R.fail(703, "玩家未充值不能游玩任何游戏"); + } + // 再做余额校验:与本次开箱所需金额(单价 * 数量)比较,不足则拒绝 + BigDecimal openBoxTotal = ttBox.getPrice().multiply(new BigDecimal(request.getNum())); + if (ttUser.getAccountAmount() == null || ttUser.getAccountAmount().compareTo(openBoxTotal) < 0) { + return R.fail(702, "账户余额不足"); + } + + // BigDecimal openBoxBeanTotal = ttBox.getPrice().multiply(new BigDecimal(num)); + // if (ttUser.getAccountAmount().compareTo(openBoxBeanTotal) < 0) return R.fail("您的账户游戏币不足!"); + // 控制层防重锁:按 userId 串行,锁包裹整个 @Transactional 的 blindBox,解锁晚于提交, + // 避免弱网重复提交导致重复开箱/重复扣款(锁粒度=单用户,不影响其他用户) + String lockKey = RedisConstants.OPEN_BOX_LOCK + ttUser.getUserId(); + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + log.warn("开箱加锁失败, userId={}", ttUser.getUserId()); + return R.fail("开箱处理中,请勿重复操作"); + } + try { + // P1#1: 锁内重新读取用户信息,避免 TOCTOU 竞态 + ttUser = userService.getById(ttUser.getUserId()); + return bindBoxService.blindBox(ttUser, ttBox, request.getNum()); + } finally { + redisLock.unlock(lockKey); + } + } + + @ApiOperation("获取历史开箱信息") + @PostMapping("/getBindBoxHistory") + public R getBindBoxHistory(@RequestBody @Validated queryCondition param) { + if (param.getStatus().equals(1) || param.getStatus().equals(10)) { + return R.fail("非法的状态参数。"); + } + PageUtils.clearPage(); + List ttBoxRecordsVOS = apiBoxRecordsService.byCondition(param); + return R.ok(ttBoxRecordsVOS); + } + + @ApiOperation("获取个人历史开箱记录") + @PostMapping("/myBoxHistory") + public R> myBoxHistory(@RequestBody queryCondition param) { + Long userId = getUserId(); + if (ObjectUtil.isNull(userId)) { + return R.fail("登录过期。"); + } + param.setUserId(userId.intValue()); + param.setOrderByFie(0); + param.setStatus(Arrays.asList( + TtboxRecordStatus.IN_PACKSACK_ON.getCode(), + TtboxRecordStatus.APPLY_DELIVERY.getCode(), + TtboxRecordStatus.DELIVERY_YET.getCode(), + TtboxRecordStatus.RESOLVE.getCode() + )); + // 仅展示经典开箱(盲盒)来源的记录,避免混入极速糖果/疯狂转盘等其他游戏记录 + param.setSource(Arrays.asList( + TtboxRecordSource.BLIND_BOX.getCode() + )); + PageUtils.clearPage(); + List list = apiBoxRecordsService.byCondition(param); + return R.ok(list); + } + + @ApiOperation("获取当前补偿值") + @PostMapping("/getCompValue") + public AjaxResult getCompValue(@RequestBody BoxIdRequest request) { + Long userId = getUserId(); + TtUser ttUser = userService.getById(userId); + TtBox ttBox = boxService.getById(request.getBoxId()); + if (Objects.isNull(ttBox)) + return error("宝箱不存在"); + if (!"3".equals(ttBox.getBoxType())) + return error("该宝箱不是补偿宝箱"); + return success(bindBoxService.getCompValue(ttUser, ttBox)); + } + + @ApiOperation("盲盒top") + @PostMapping("/top") + public R> topBlinkBox() { + Long userId = getUserId(); + if (ObjectUtil.isNull(userId)) { + return R.fail("登录过期。"); + } + queryCondition param = new queryCondition(); + param.setUserId(userId.intValue()); + param.setOrderByFie(2); + param.setSource(Collections.singletonList(TtboxRecordSource.BLIND_BOX.getCode())); + param.setStatus(Arrays.asList(TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode(), TtboxRecordStatus.DELIVERY_YET.getCode())); + param.setPage(1); + param.setSize(1); + PageUtils.clearPage(); + List ttBoxRecordsVOS = new ArrayList<>(apiBoxRecordsService.byCondition(param)); + param.setSource(Collections.singletonList(TtboxRecordSource.FIGHT.getCode())); + ttBoxRecordsVOS.addAll(apiBoxRecordsService.byCondition(param)); + param.setSource(Collections.singletonList(TtboxRecordSource.UPGRADE.getCode())); + ttBoxRecordsVOS.addAll(apiBoxRecordsService.byCondition(param)); + return R.ok(ttBoxRecordsVOS); + } + + @ApiOperation("十佳掉落") + @PostMapping("/tenTop") + public R> tenTop() { + TenTopQuery param = new TenTopQuery(); + param.setSource(Collections.singletonList(TtboxRecordSource.BLIND_BOX.getCode())); + param.setStatus(Arrays.asList(TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode(), TtboxRecordStatus.DELIVERY_YET.getCode())); + param.setPage(1); + param.setSize(10); + PageUtils.clearPage(); + List ttBoxRecordsVOS = apiBoxRecordsService.tenTopQuery(param); + return R.ok(ttBoxRecordsVOS); + } + + @ApiOperation("今日百佳") + @PostMapping("/todayTopHundred") + public R> todayTopHundred() { + List status = Arrays.asList( + TtboxRecordStatus.IN_PACKSACK_ON.getCode(), + TtboxRecordStatus.APPLY_DELIVERY.getCode(), + TtboxRecordStatus.DELIVERY_YET.getCode(), + TtboxRecordStatus.RESOLVE.getCode() + ); + List source = Arrays.asList( + TtboxRecordSource.BLIND_BOX.getCode() + ); + PageUtils.clearPage(); + List list = apiBoxRecordsService.topHundredQuery(status, source, true); + return R.ok(list); + } + + @ApiOperation("历史百佳") + @PostMapping("/historyTopHundred") + public R> historyTopHundred() { + List status = Arrays.asList( + TtboxRecordStatus.IN_PACKSACK_ON.getCode(), + TtboxRecordStatus.APPLY_DELIVERY.getCode(), + TtboxRecordStatus.DELIVERY_YET.getCode(), + TtboxRecordStatus.RESOLVE.getCode() + ); + List source = Arrays.asList( + TtboxRecordSource.BLIND_BOX.getCode() + ); + PageUtils.clearPage(); + List list = apiBoxRecordsService.topHundredQuery(status, source, false); + return R.ok(list); + } + + @ApiOperation("实时 - 所有人历史开箱按时间排序") + @Anonymous + @PostMapping("/realtime") + public R> realtime(@RequestBody @Validated BoxRecordPageRequest request) { + queryCondition param = new queryCondition(); + param.setPage(request.getPage()); + param.setSize(request.getSize()); + // 仅展示当前选中箱子的开箱记录 + param.setBoxId(request.getBoxId()); + param.setOrderByFie(0); + param.setStatus(Arrays.asList( + TtboxRecordStatus.IN_PACKSACK_ON.getCode(), + TtboxRecordStatus.APPLY_DELIVERY.getCode(), + TtboxRecordStatus.DELIVERY_YET.getCode(), + TtboxRecordStatus.RESOLVE.getCode() + )); + param.setSource(Arrays.asList( + TtboxRecordSource.BLIND_BOX.getCode() + )); + PageUtils.clearPage(); + List list = apiBoxRecordsService.byCondition(param); + return R.ok(list); + } + + @ApiOperation("欧皇 - 所有人开箱按最高价格排序") + @Anonymous + @PostMapping("/luckyKing") + public R> luckyKing(@RequestBody @Validated BoxRecordPageRequest request) { + queryCondition param = new queryCondition(); + param.setPage(request.getPage()); + param.setSize(request.getSize()); + // 仅展示当前选中箱子的开箱记录 + param.setBoxId(request.getBoxId()); + param.setOrderByFie(2); + param.setStatus(Arrays.asList( + TtboxRecordStatus.IN_PACKSACK_ON.getCode(), + TtboxRecordStatus.APPLY_DELIVERY.getCode(), + TtboxRecordStatus.DELIVERY_YET.getCode(), + TtboxRecordStatus.RESOLVE.getCode() + )); + param.setSource(Arrays.asList( + TtboxRecordSource.BLIND_BOX.getCode() + )); + // 仅展示金色品质(饰品级别 ID=1)的饰品 + param.setOrnamentLevelIds(Arrays.asList(1)); + PageUtils.clearPage(); + List list = apiBoxRecordsService.byCondition(param); + return R.ok(list); + } + + /** + * 经典开箱记录统一接口(支持匿名查询) + * + * 使用场景: + * - 全服全部箱子:不传参数 / 不登录 → 查看所有用户的所有箱子的开箱记录 + * - 全服当前箱子:不传 userId + 传 boxId / 不登录 → 查看指定箱子的全服记录 + */ + @ApiOperation("经典开箱记录") + @Anonymous + @PostMapping("/classicRecords") + public R> classicRecords(@RequestBody @Validated ClassicBoxRecordPageRequest request) { + // 构建查询条件 + queryCondition condition = new queryCondition(); + condition.setPage(request.getPage()); + condition.setSize(request.getSize()); + condition.setBoxId(request.getBoxId()); + condition.setOrderByFie(request.getOrderByFie()); + + // 固定过滤:仅展示经典开箱来源(source=1),有效状态包括背包显示、申请提货、已提货、分解 + condition.setStatus(Arrays.asList( + TtboxRecordStatus.IN_PACKSACK_ON.getCode(), // 0-在背包显示 + TtboxRecordStatus.APPLY_DELIVERY.getCode(), // 2-申请提货 + TtboxRecordStatus.DELIVERY_YET.getCode(), // 3-已经提货 + TtboxRecordStatus.RESOLVE.getCode() // 5-分解 + )); + condition.setSource(Arrays.asList(TtboxRecordSource.BLIND_BOX.getCode())); + PageUtils.clearPage(); + List records = apiBoxRecordsService.byCondition(condition); + return R.ok(records); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiBoxRecordsController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiBoxRecordsController.java new file mode 100644 index 0000000..6993e15 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiBoxRecordsController.java @@ -0,0 +1,38 @@ +package com.ruoyi.playingmethod.controller; + +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.PageUtils; +import com.ruoyi.domain.dto.boxRecords.queryCondition; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.playingmethod.service.ApiBoxRecordsService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Api(tags = "出货记录") +@RestController +@RequestMapping("/api/boxRecords") +public class ApiBoxRecordsController { + + @Autowired + private ApiBoxRecordsService apiBoxRecordsService; + + @ApiOperation("获取历史出货记录") + @PostMapping("/historyByCondition") + public R getBindBoxHistory(@RequestBody @Validated queryCondition param) { + if (param.getStatus().contains(1) || param.getStatus().contains(10)) { + return R.fail("非法的状态参数。"); + } + PageUtils.clearPage(); + List ttBoxRecordsVOS = apiBoxRecordsService.byCondition(param); + return R.ok(ttBoxRecordsVOS); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiCompoundController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiCompoundController.java new file mode 100644 index 0000000..ef9d002 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiCompoundController.java @@ -0,0 +1,56 @@ +package com.ruoyi.playingmethod.controller; + +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.playingmethod.service.ApiCompoundService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.domain.vo.UserPackSackDataVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Api(tags = "合成") +@RestController +@RequestMapping("/api/compound") +public class ApiCompoundController extends BaseController { + + private final ISysConfigService sysConfigService; + private final TtUserService userService; + private final ApiCompoundService compoundService; + + public ApiCompoundController(ISysConfigService sysConfigService, + TtUserService userService, + ApiCompoundService compoundService) { + this.sysConfigService = sysConfigService; + this.userService = userService; + this.compoundService = compoundService; + } + + @ApiOperation("合成") + @PostMapping("/compound") + public R compound(@RequestBody List packSackIds) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String compoundMaintenance = sysConfigService.selectConfigByKey("compoundMaintenance"); + if ("1".equals(compoundMaintenance)) { + return R.fail("汰换合成功能正在维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + return compoundService.compound(packSackIds, ttUser); + } + + @ApiOperation("获取用户合成记录") + @PostMapping("/getUserCompoundRecord") + public PageDataInfo getUserCompoundRecord() { + startPage(); + List userCompoundRecord = compoundService.getUserCompoundRecord(getUserId().intValue()); + return getPageData(userCompoundRecord); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiExponentController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiExponentController.java new file mode 100644 index 0000000..357881e --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiExponentController.java @@ -0,0 +1,153 @@ +package com.ruoyi.playingmethod.controller; + +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.PageUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.dto.exponent.ExponentJoinParam; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.playingmethod.contract.api.request.BoxIdRequest; +import com.ruoyi.playingmethod.contract.api.request.OpenBoxRequest; +import com.ruoyi.playingmethod.mapper.ApiBindBoxMapper; +import com.ruoyi.playingmethod.service.ApiExponentService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + + +@Api(tags = "指数盲盒") +@RestController +@RequestMapping("/api/exponent") +public class ApiExponentController extends BaseController { + + @Autowired + private ApiExponentService apiExponentService; + + @Autowired + private TtUserService ttUserService; + + @Autowired + private ApiBindBoxMapper bindBoxMapper; + + @Autowired + private TtBoxService boxService; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private RedisLock redisLock; + + @ApiOperation("加入指数盲盒") + @Anonymous + @UpdateUserCache + @PostMapping("/join") + public R join(@RequestBody @Validated ExponentJoinParam param) { + TtUser ttUser = ttUserService.getById(getUserId()); + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired(); + } + + // 归档兼容充值校验:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL, + // 避免历史充值流水被归档后误判为“未充值”。 + BigDecimal cacheRecharge = ttUser.getTotalRechargeAmount(); + boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0; + if (!recharged) { + BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource( + Collections.singletonList(ttUser.getUserId()), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0; + } + if (!recharged) { + return R.fail(703, "玩家未充值不能游玩任何游戏"); + } + + // 控制层防重锁:按 userId 串行,避免弱网重复提交导致重复扣款/重复参与(锁粒度=单用户) + String lockKey = RedisConstants.EXPONENT_JOIN_LOCK + ttUser.getUserId(); + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + return R.fail("处理中,请勿重复操作"); + } + try { + return apiExponentService.join(ttUser, param); + } finally { + redisLock.unlock(lockKey); + } + } + + @ApiOperation("获取宝箱列表") + @ApiImplicitParams({ + @ApiImplicitParam(name = "boxType", value = "宝箱类型(1普通箱 2对战箱 3补偿箱 4九宫格 5指数盲盒)", dataType = "String", dataTypeClass = String.class) + }) + @Anonymous + @PostMapping("/box") + public R box(@RequestBody BoxIdRequest request) { + TtUser ttUser = ttUserService.getById(getUserId()); + return apiExponentService.box(ttUser, request.getBoxId()); + } + + @ApiOperation("最近十期结果") + @Anonymous + @PostMapping("/recenttenperiod") + public R recenttenperiod() { + PageUtils.clearPage(); + return apiExponentService.recenttenperiod(); + } + + @ApiOperation("结果信息") + @Anonymous + @PostMapping("/info") + public R info() { + PageUtils.clearPage(); + TtUser ttUser = ttUserService.getById(getUserId()); + return apiExponentService.info(ttUser); + } + + @ApiOperation("指数记录") + @Anonymous + @PostMapping("/records") + public R records() { + startPage(); + TtUser ttUser = ttUserService.getById(getUserId()); + return apiExponentService.records(ttUser); + } + + @ApiOperation("我的箱子") + @Anonymous + @PostMapping("/mybox") + public R mybox() { + TtUser ttUser = ttUserService.getById(getUserId()); + return apiExponentService.mybox(ttUser); + } + @ApiOperation("盲盒开箱") + @UpdateUserCache + @PostMapping("/openBox") + public R openBox(@RequestBody OpenBoxRequest request) { + TtUser ttUser = ttUserService.getById(getUserId()); + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired(); + } + TtBox ttBox = boxService.getById(request.getBoxId()); + return apiExponentService.openbox(ttUser, ttBox, request.getNum()); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiFightController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiFightController.java new file mode 100644 index 0000000..1cf9cb7 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiFightController.java @@ -0,0 +1,667 @@ +package com.ruoyi.playingmethod.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.*; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.constant.ErrorCode; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.RandomUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.dto.fight.FightDetailParam; +import com.ruoyi.domain.dto.fight.FightOnMyOwnParam; +import com.ruoyi.domain.dto.fight.QuickJoinFightParam; +import com.ruoyi.domain.dto.fight.BaiJiaFightParam; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateFightBody; +import com.ruoyi.domain.other.FightBoutData; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtBoxVO; +import com.ruoyi.domain.vo.ApiFightListDataVO; +import com.ruoyi.domain.vo.FightResultDataVO; +import com.ruoyi.domain.vo.fight.FightBoxVO; +import com.ruoyi.domain.vo.fight.FightResultVO; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; +import com.ruoyi.playingmethod.model.vo.ApiFightRankingVO; +import com.ruoyi.playingmethod.service.ApiBindBoxService; +import com.ruoyi.playingmethod.service.ApiFightService; +import com.ruoyi.playingmethod.contract.api.request.BoxIdRequest; +import com.ruoyi.domain.other.TtBoxA; +import com.ruoyi.system.service.ISysConfigService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.Collections; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; +import java.util.concurrent.TimeUnit; + +@Api(tags = "对战模式") +@Slf4j +@RestController +@RequestMapping("/api/fight") +public class ApiFightController extends BaseController { + + private final TtFightResultService fightResultService; + private final ISysConfigService sysConfigService; + private final TtUserService userService; + private final ApiFightService apiFightService; + private final RedisCache redisCache; + + @Autowired + private TtBoxOrnamentsService boxOrnamentsService; + + @Autowired + private TtOrnamentService ttOrnamentService; + + @Autowired + private LotteryMachine lotteryMachine; + + @Autowired + private TtBoxService boxService; + + @Autowired + private TtUserMapper ttUserMapper; + + public ApiFightController(TtFightResultService fightResultService, + ISysConfigService sysConfigService, + TtUserService userService, + ApiFightService apiFightService, + RedisCache redisCache) { + this.fightResultService = fightResultService; + this.sysConfigService = sysConfigService; + this.userService = userService; + this.apiFightService = apiFightService; + this.redisCache = redisCache; + } + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private ApiBindBoxService bindBoxService; + + public R checkLogin() { + Long userId; + try { + userId = getUserId(); + if (ObjectUtil.isEmpty(userId)) return R.fail(ErrorCode.LOGIN_EXPIRED, "登录过期,请重新登录。"); + return R.ok(userId); + } catch (Exception e) { + return R.fail("登录过期,请重新登录。"); + } + } + + /** + * Step1 创建对战房间 + */ + @ApiOperation("创建对战房间") + @UpdateUserCache + @PostMapping("/createFight") + public R createFight(@Validated @RequestBody CreateFightBody createFightParam) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance"); + if ("1".equals(fightMaintenance)) { + return R.fail("开箱对战功能正在维护中......"); + } + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) { + return checkLogin; + } + Integer userId = ((Long) checkLogin.getData()).intValue(); + TtUser player = userService.getById(userId); + // 归档兼容充值校验:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL, + // 避免历史充值流水被归档后误判为"未充值"。 + BigDecimal cacheRecharge = player.getTotalRechargeAmount(); + boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0; + if (!recharged) { + BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource( + Collections.singletonList(userId), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0; + } + if (!recharged) { + // 未充值统一返回业务码 702,便于前端识别并引导充值 + return R.fail(702, "您的账户未进行充过值!"); + } + + return apiFightService.createFight(createFightParam, player); + } + + /** + * 复刻对战房间:复制目标房间的宝箱配置、人数、组队模式等,创建一个新房间,当前用户成为房主 + */ + @ApiOperation("复刻对战房间") + @UpdateUserCache + @PostMapping("/copyFight") + public R copyFight(@RequestBody FightDetailParam param) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance"); + if ("1".equals(fightMaintenance)) { + return R.fail("开箱对战功能正在维护中......"); + } + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) { + return checkLogin; + } + Integer userId = ((Long) checkLogin.getData()).intValue(); + TtUser player = userService.getById(userId); + // 归档兼容充值校验:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL + BigDecimal cacheRecharge = player.getTotalRechargeAmount(); + boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0; + if (!recharged) { + BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource( + Collections.singletonList(userId), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0; + } + if (!recharged) { + // 未充值统一返回业务码 702,便于前端识别并引导充值 + return R.fail(702, "您的账户未进行充过值!"); + } + + return apiFightService.copyFight(param.getFightId(), player); + } + + /** + * Step2 加入房间(占座),先行HTTP + */ + @ApiOperation("加入对战房间") + @PostMapping("/joinFightRoom") + public R joinFightRoom(@RequestBody FightDetailParam param) { + Integer fightId = param.getFightId(); + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance"); + if ("1".equals(fightMaintenance)) { + return R.fail("开箱对战功能正在维护中......"); + } + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + Integer userId = ((Long) checkLogin.getData()).intValue(); + TtUser player = userService.getById(userId); + // 归档兼容充值校验:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL, + // 避免历史充值流水被归档后误判为"未充值"。 + BigDecimal cacheRecharge = player.getTotalRechargeAmount(); + boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0; + if (!recharged) { + BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource( + Collections.singletonList(userId), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0; + } + if (!recharged) { + // 未充值统一返回业务码 702,便于前端识别并引导充值 + return R.fail(702, "您的账户未进行充过值!"); + } + + return apiFightService.joinFight(fightId, player, param.getSeatCode()); + } + + /** + * 退出房间 + */ + @ApiOperation("退出房间") + @PostMapping("/fightRoomExit") + public R fightRoomExit(@RequestBody FightDetailParam param) { + Integer fightId = param.getFightId(); + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......"); + String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance"); + if ("1".equals(fightMaintenance)) return R.fail("开箱对战功能正在维护中......"); + + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + Integer userId = ((Long) checkLogin.getData()).intValue(); + + TtUser player = userService.getById(userId); + + return apiFightService.fightRoomExit(fightId, player); + } + + /** + * Step3 玩家准备游戏 + */ + @ApiOperation("玩家准备游戏") + @UpdateUserCache + @PostMapping("/seatrReady") + public R seatrReady(@RequestBody FightDetailParam param) { + Integer fightId = param.getFightId(); + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance"); + if ("1".equals(fightMaintenance)) { + return R.fail("开箱对战功能正在维护中......"); + } + + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + Integer userId = ((Long) checkLogin.getData()).intValue(); + + TtUser player = userService.getById(userId); + + return apiFightService.seatrReady(fightId, player); + } + + /** + * Step4 开始游戏(前端满人自动开始) + */ + @ApiOperation("开始游戏") + @PostMapping("/fightBegin") + public R fightBegin(@RequestBody FightDetailParam param) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance"); + if ("1".equals(fightMaintenance)) { + return R.fail("开箱对战功能正在维护中......"); + } + + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + Integer userId = ((Long) checkLogin.getData()).intValue(); + + TtUser player = userService.getById(userId); + + return apiFightService.fightBegin(param.getFightId(), player); + } + + /** + * 游戏结束标记 + */ + @ApiOperation("游戏结束标记") + @PostMapping("/fightEnd") + public R fightEnd(@RequestBody FightDetailParam param) { + Integer fightId = param.getFightId(); + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......"); + String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance"); + if ("1".equals(fightMaintenance)) return R.fail("开箱对战功能正在维护中......"); + + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + + return apiFightService.fightEnd(fightId, getUserId()); + } + + /** + * 抽奖机测试 + */ + // @ApiOperation("抽奖机测试") + // @GetMapping("LotteryMachineTest") + public R LotteryMachineTest() { + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + Integer userId = ((Long) checkLogin.getData()).intValue(); + TtUser player = userService.getById(userId); + + TtBox box = boxService.getById(2L); + String ornamentId = lotteryMachine.singleLottery(player, box); + + return R.ok(ornamentId); + } + + @ApiOperation("我参与的历史对战记录") + @PostMapping("/fightOnMyOwn") + public R fightOnMyOwn(@RequestBody FightOnMyOwnParam param) { + Long userId = getUserId(); + if (ObjectUtil.isNull(userId)) return R.fail(ErrorCode.LOGIN_EXPIRED, "登录过期。"); + + param.setPlayerId(userId.intValue()); + + List list = apiFightService.getFightList(param); + return R.ok(list); + } + + /** + * 快速加入对战房间 - 在本阵营(model 0=欧皇 1=非酋)内返回等待中且有空座位、 + * 总价不超过用户余额的最高价房间 id(不落座,需登录) + */ + @ApiOperation("快速加入对战房间") + @UpdateUserCache + @PostMapping("/quickJoinFightRoom") + public R quickJoinFightRoom(@RequestBody QuickJoinFightParam param) { + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return R.fail(ErrorCode.LOGIN_EXPIRED, checkLogin.getMsg()); + + Integer userId = ((Long) checkLogin.getData()).intValue(); + + return apiFightService.quickJoinFightRoom(userId, param != null ? param.getModel() : null); + } + + @ApiOperation("全服历史对战记录") + @PostMapping("/fightOnAll") + public R fightOnAll(@RequestBody FightOnMyOwnParam param) { + List list = apiFightService.getFightList(param); + return R.ok(list); + } + + @ApiOperation("历史对战详情") + @PostMapping("/fightDetail") + public R fightDetail(@RequestBody FightDetailParam param) { + return apiFightService.fightDetail(param); + } + + @ApiOperation("获取指定记录更早的历史对战") + @PostMapping("/earlierHistory") + public R earlierHistory(@RequestBody FightDetailParam param) { + return apiFightService.earlierHistory(param); + } + + @ApiOperation("获取对战宝箱详情") + @PostMapping("/simpleBoxDetail") + public PageDataInfo simpleBoxDetail(@RequestBody Map param) { + startPage(); + Integer boxId = param.get("boxId"); + List list = boxOrnamentsService.simpleBoxDetail(boxId); + return getPageData(list); + } + + @ApiOperation("获取对战宝箱") + @PostMapping("/getFightBoxList") + @Anonymous + public R> getFightBoxList(@RequestBody(required = false) Map param) { + Integer boxTypeId = (param != null) ? param.get("boxTypeId") : null; + List boxData = apiFightService.getFightBoxList(boxTypeId); + if (ObjectUtil.isEmpty(boxData) || boxData.isEmpty()) return R.ok(null, "没有匹配的数据。"); + return R.ok(boxData); + } + + @ApiOperation("获取对战列表") + @Anonymous + @PostMapping("/getFightList") + public PageDataInfo getFightList(@RequestBody(required = false) Map param) { + startPage(); + String model = param != null ? (String) param.get("model") : null; + String status = param != null ? (String) param.get("status") : null; + Integer userId = param != null && param.get("userId") != null ? Integer.valueOf(param.get("userId").toString()) : null; + List list = apiFightService.getFightList(model, status, userId, null); + return getPageData(list); + } + + @ApiOperation("获取对战大厅房间列表(HTTP)") + @Anonymous + @PostMapping("/hall") + public R> fightHall(@RequestBody(required = false) Map param) { + Integer pageNum = param != null && param.get("pageNum") != null ? Integer.valueOf(param.get("pageNum").toString()) : 1; + Integer pageSize = param != null && param.get("pageSize") != null ? Integer.valueOf(param.get("pageSize").toString()) : 10; + String status = param != null ? (String) param.get("status") : null; + String model = param != null ? (String) param.get("model") : null; + BigDecimal boxPriceTotalMin = param != null && param.get("boxPriceTotalMin") != null ? new BigDecimal(param.get("boxPriceTotalMin").toString()) : null; + BigDecimal boxPriceTotalMax = param != null && param.get("boxPriceTotalMax") != null ? new BigDecimal(param.get("boxPriceTotalMax").toString()) : null; + Page pageInfo = new Page<>(pageNum, pageSize); + pageInfo.setOptimizeCountSql(false); + + LambdaQueryWrapper fightQuery = new LambdaQueryWrapper<>(); + fightQuery + .eq(status != null, TtFight::getStatus, status) + .eq(model != null, TtFight::getModel, model) + .between(boxPriceTotalMin != null && boxPriceTotalMax != null, + TtFight::getBoxPriceTotal, boxPriceTotalMin, boxPriceTotalMax) + .ge(boxPriceTotalMin == null && boxPriceTotalMax != null, + TtFight::getBoxPriceTotal, boxPriceTotalMax) + .orderByDesc(TtFight::getCreateTime); + List fightList = apiFightService.page(pageInfo, fightQuery).getRecords(); + + Map result = new HashMap<>(); + if (CollectionUtils.isEmpty(fightList)) { + result.put("rows", new ArrayList<>()); + result.put("total", 0); + return R.ok(result, "所有对战房间"); + } + + List boxIds = new ArrayList<>(); + for (TtFight fight : fightList) { + if (fight == null || MapUtils.isEmpty(fight.getBoxData())) { + continue; + } + for (Map.Entry entry : fight.getBoxData().entrySet()) { + Object value = entry.getValue(); + if (value == null) { + continue; + } + Integer boxId; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + boxId = map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null; + } else { + boxId = ((FightBoxVO) value).getBoxId(); + } + if (boxId != null) { + boxIds.add(boxId); + } + } + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxIds); + List boxes = boxService.list(queryWrapper); + Map boxMap = boxes.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + + for (TtFight fight : fightList) { + if (fight == null || MapUtils.isEmpty(fight.getBoxData())) { + continue; + } + for (Map.Entry entry : fight.getBoxData().entrySet()) { + Object value = entry.getValue(); + if (value == null) { + continue; + } + FightBoxVO vo; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + vo = new FightBoxVO( + map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), new BigDecimal(0))); + } else { + vo = (FightBoxVO) value; + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), new BigDecimal(0))); + } + fight.getBoxData().put(vo.getBoxId().toString(), vo); + } + } + + apiFightService.enrichOwnerNickNames(fightList); + + result.put("rows", fightList); + result.put("total", apiFightService.page(pageInfo, fightQuery).getTotal()); + return R.ok(result, "所有对战房间"); + } + + @ApiOperation("获取对战数据") + @PostMapping("/getFightData") + public R getFightData(@RequestBody FightDetailParam param) { + ApiFightListDataVO apiFightListDataVO = apiFightService.getFightList(null, null, null, param.getFightId()).get(0); + return R.ok(apiFightListDataVO); + } + + // @ApiOperation("获取对战结果") + // @GetMapping("/getFightResult/{fightId}") + // public R getFightResult(@PathVariable("fightId") Integer fightId) { + // FightResultDataVO fightResultDataVO = fightResultService.getFightResult(fightId); + // return R.ok(fightResultDataVO); + // } + + @ApiOperation("获取对战结果") + @PostMapping("/getFightResult") + public R getFightResult(@RequestBody FightDetailParam param) { + FightResultVO fightResultVO = fightResultService.getFightResult(param.getFightId()); + return R.ok(fightResultVO); + } + + @ApiOperation("存储对战回合数据") + @PostMapping("/saveFightBoutData") + public R saveFightBoutData(@RequestBody FightBoutData fightBoutData) { + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + String key = "fight_bout_data:fight_" + fightBoutData.getFightId(); + redisCache.setCacheObject(key, fightBoutData, fightBoutData.getExpirationTime(), TimeUnit.MILLISECONDS); + return R.ok(); + } + + @ApiOperation("获取对战回合数") + @PostMapping("/getFightBoutNum") + public R getFightBoutNum(@RequestBody FightDetailParam param) { + Integer fightId = param.getFightId(); + String key = "fight_bout_data:fight_" + fightId; + FightBoutData fightBoutData = redisCache.getCacheObject(key); + if (StringUtils.isNotNull(fightBoutData)) { + return R.ok(fightBoutData.getBoutNum()); + } else { + // return R.fail("对战回合不存在"); + return R.ok(); + } + } + + /** + * 获取当前回合开箱结果 + */ + // @GetMapping("/getFightRecord") + public R getFightRecord(@RequestParam(value = "fightId", required = false) Integer fightId, + @RequestParam(value = "round", required = false) Integer round, + @RequestParam(value = "rounds", required = false) Integer rounds) { + // System.out.println("=========================获取当前回合开箱结果============================="); + // System.out.println("fightId:" + fightId); + // System.out.println("round:" + round); + // System.out.println("rounds:" + rounds); + + log.info("获取当前回合开箱结果 api /{} 参数fightId{} round{} rounds{}", "getFightRecord", fightId, round, rounds); + + // 检查是否已经开始游戏 + TtFight fight = apiFightService.getById(fightId); + if (ObjectUtil.isEmpty(fight)) return R.fail("不存在的对局"); + if (fight.getStatus().equals(0)) return R.fail("对局尚未开始,请稍后重试。"); + + FightResultDataVO fightResultDataVO = apiFightService.getFightRecord(fightId, round, rounds); + if (ObjectUtil.isEmpty(fightResultDataVO)) { + return R.fail("对局fightId 没有产生结果"); + } + return R.ok(fightResultDataVO); + } + + @ApiOperation("获取对战排行榜") + @Anonymous + @PostMapping("/fightRanking") + public AjaxResult getFightRanking() { + // 创建日期格式化对象,指定日期格式 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + + // 获取今天日期 + Date today = new Date(); + String formattedTodayDate = formatter.format(today); + List todayFightRanking = apiFightService.getFightRankingByDate(formattedTodayDate); + + // 获取昨天日期 + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -1); + Date yesterday = cal.getTime(); + String formattedYesterdayDate = formatter.format(yesterday); + List yesterdayFightRanking = apiFightService.getFightRankingByDate(formattedYesterdayDate); + + Map> map = new HashMap<>(); + map.put("todayFightRanking", todayFightRanking); + map.put("yesterdayFightRanking", yesterdayFightRanking); + return AjaxResult.success(map); + } + + @ApiOperation("十佳对战, 1:最大金额,2:最大增长,3:最大倍数") + @Anonymous + @PostMapping("/tenTopFight") + public AjaxResult tenTopFight(@RequestBody BaiJiaFightParam param) { + return AjaxResult.success(apiFightService.tenTopFight(param.getMaxType(), param.getModel(), param.getMinPrice(), param.getMaxPrice(), param.getKeyword())); + } + + @ApiOperation("邀请机器人加入对战房间") + @PostMapping({"/joinFightRoomForRobot"}) + public R joinFightRoomForRobot(@RequestBody FightDetailParam param) { + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + Integer fightId = param.getFightId(); + String websiteMaintenance = this.sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } else { + String fightMaintenance = this.sysConfigService.selectConfigByKey("fightMaintenance"); + if ("1".equals(fightMaintenance)) { + return R.fail("开箱对战功能正在维护中......"); + } else { + List robotIdList = new LambdaQueryChainWrapper(this.ttUserMapper) + .eq(TtUser::getUserType, UserType.ROBOT_USER.getCode()).list(); + if (CollectionUtils.isEmpty(robotIdList)) { + return R.ok(); + } else { + TtUser player = RandomUtil.choice(robotIdList); + R objectR = this.apiFightService.joinFight(fightId, player); + if (objectR.getCode() == 500) { + return objectR; + } else { + log.info("机器人{}加入对战{}", player.getUserId(), fightId); + R fight = this.apiFightService.seatrReady(fightId, player); + log.info("fight robot task fight is {}", fight == null ? "null" : fight.toString()); + FightBoutData fightBoutData = new FightBoutData(); + fightBoutData.setFightId(fightId); + fightBoutData.setBoutNum(1); + fightBoutData.setExpirationTime(16000); + String key = "fight_bout_data:fight_" + fightBoutData.getFightId(); + this.redisCache.setCacheObject(key, fightBoutData, fightBoutData.getExpirationTime(), TimeUnit.MILLISECONDS); + this.apiFightService.fightBegin(fightId, player); + return R.ok(); + } + } + } + } + } + + @ApiOperation("对战宝箱详情") + @Anonymous + @PostMapping("/boxData") + public R getFightBoxData(@RequestBody BoxIdRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtBoxA boxData = bindBoxService.getFightBoxData(request.getBoxId()); + return R.ok(boxData); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiFightRoomController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiFightRoomController.java new file mode 100644 index 0000000..1dceb64 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiFightRoomController.java @@ -0,0 +1,236 @@ +package com.ruoyi.playingmethod.controller; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.other.FightBoutData; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO; +import com.ruoyi.domain.vo.fight.FightBoxVO; +import com.ruoyi.domain.vo.fight.FightResultVO; +import com.ruoyi.playingmethod.contract.api.request.FightRoomInfoRequest; +import com.ruoyi.playingmethod.service.ApiFightService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 对战房间 HTTP 接口(原 WsFightRoom WebSocket 接口的 HTTP 替代) + */ +@Api(tags = "对战房间HTTP接口") +@Slf4j +@RestController +@RequestMapping("/api/fight/room") +public class ApiFightRoomController extends BaseController { + + @Autowired + private ApiFightService apiFightService; + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private TtBoxOrnamentsMapper boxOrnamentsMapper; + + @Autowired + private TtBoxMapper ttBoxMapper; + + @Autowired + private RedisCache redisCache; + + @ApiOperation("获取对战房间信息/结果") + @PostMapping("/info") + public R info(@RequestBody FightRoomInfoRequest request) { + Long userId = getUserId(); + Integer fightId = request.getFightId(); + + if (ObjectUtil.isEmpty(fightId)) { + return R.fail("对局ID不能为空"); + } + + TtFight fight = apiFightService.getById(fightId); + if (ObjectUtil.isEmpty(fight)) { + return R.fail("不存在的对局ID:" + fightId); + } + + // 构建宝箱数据及饰品详情 + Map boxData = fight.getBoxData(); + ArrayList fightBoxVOList = new ArrayList<>(); + if (boxData != null && !boxData.isEmpty()) { + boxData.keySet().stream() + .sorted(Comparator.comparingInt(Integer::parseInt)) + .forEach(boxId -> { + List boxOrnamentsVOS = boxOrnamentsMapper.selectTtBoxOrnamentsList(Integer.valueOf(boxId)); + FightBoxVO fightBoxVO = JSONUtil.toBean(JSONUtil.toJsonStr(boxData.get(boxId)), FightBoxVO.class); + fightBoxVO.setOrnaments(boxOrnamentsVOS); + boxData.put(boxId, fightBoxVO); + fightBoxVOList.add(boxData.get(boxId)); + }); + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxData.keySet()); + List boxes = ttBoxMapper.selectList(queryWrapper); + var boxMapObj = boxes.stream().filter(Objects::nonNull).collect(Collectors.groupingBy(TtBox::getBoxId)); + Map boxMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + + for (FightBoxVO vo : fightBoxVOList) { + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), new BigDecimal(0))); + if (boxMapObj.get(vo.getBoxId()) != null) { + vo.setBoxName(boxMapObj.get(vo.getBoxId()).getFirst().getBoxName()); + } + } + + fight.getBoxData().keySet().forEach(boxId -> { + Object value = fight.getBoxData().get(boxId); + FightBoxVO a = null; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + a = new FightBoxVO( + map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO)); + } else { + a = (FightBoxVO) value; + a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO)); + } + fight.getBoxData().put(boxId, a); + }); + } + + // 如果游戏结束,发送结果集数据 + if (fight.getStatus().equals(2) || fight.getStatus().equals(3)) { + LambdaQueryWrapper boxRecordsQuery = new LambdaQueryWrapper<>(); + boxRecordsQuery.eq(TtBoxRecords::getFightId, fightId); + List allBoxRecords = boxRecordsService.list(boxRecordsQuery); + + // 分离正常回合结果和赛后奖励 + List roundResults = allBoxRecords.stream() + .filter(r -> r.getFightRoundNumber() != null && r.getFightRoundNumber() > 0) + .collect(Collectors.toList()); + List endGameRecords = allBoxRecords.stream() + .filter(r -> r.getFightRoundNumber() == null || r.getFightRoundNumber() <= 0) + .collect(Collectors.toList()); + + // 胜利方获得的所有饰品 + List winnerIds = fight.getWinnerIds() != null ? fight.getWinnerIds() : new java.util.ArrayList<>(); + List winnerPrize = allBoxRecords.stream() + .filter(r -> r.getHolderUserId() != null && winnerIds.contains(r.getHolderUserId())) + .filter(r -> r.getOrnamentId() != null) + .collect(Collectors.toList()); + + // 失败方安慰奖 + List endGamePrize = endGameRecords.stream() + .filter(r -> r.getHolderUserId() != null && !winnerIds.contains(r.getHolderUserId())) + .collect(Collectors.toList()); + + // 计算饰品总价 + BigDecimal totalOrnamentsPrice = allBoxRecords.stream() + .map(TtBoxRecords::getOrnamentsPrice) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + FightResultVO resultVO = FightResultVO.builder() + .currentRound(-1L) + .fight(fight) + .winnerIds(winnerIds) + .fightResult(roundResults) + .winnerPrize(winnerPrize) + .endGamePrize(endGamePrize) + .totalOrnamentsPrice(totalOrnamentsPrice) + .fightBoxVOList(fightBoxVOList) + .build(); + + log.info("用户{}查询房间{},对局已结束", userId, fightId); + return R.ok(resultVO, "对局已结束,对局最新信息"); + + } else if (fight.getStatus().equals(1)) { + LambdaQueryWrapper boxRecordsQuery = new LambdaQueryWrapper<>(); + boxRecordsQuery.eq(TtBoxRecords::getFightId, fightId) + .eq(TtBoxRecords::getStatus, 0); + + List allBoxRecords; + String key = "fight_result:fight_" + fightId; + allBoxRecords = redisCache.getCacheObject(key); + + if (Objects.isNull(allBoxRecords)) { + allBoxRecords = boxRecordsService.list(boxRecordsQuery); + } + + Integer currentRound = 0; + FightBoutData fightBoutData = redisCache.getCacheObject("fight_bout_data:fight_" + fightId); + if (!Objects.isNull(fightBoutData)) { + currentRound = fightBoutData.getBoutNum(); + } + + // 分离正常回合结果和赛后奖励 + List roundResults = allBoxRecords.stream() + .filter(r -> r.getFightRoundNumber() != null && r.getFightRoundNumber() > 0) + .collect(Collectors.toList()); + List endGameRecords = allBoxRecords.stream() + .filter(r -> r.getFightRoundNumber() == null || r.getFightRoundNumber() <= 0) + .collect(Collectors.toList()); + + // 胜利方获得的所有饰品 + List winnerIds = fight.getWinnerIds() != null ? fight.getWinnerIds() : new java.util.ArrayList<>(); + List winnerPrize = allBoxRecords.stream() + .filter(r -> r.getHolderUserId() != null && winnerIds.contains(r.getHolderUserId())) + .filter(r -> r.getOrnamentId() != null) + .collect(Collectors.toList()); + + // 失败方安慰奖 + List endGamePrize = endGameRecords.stream() + .filter(r -> r.getHolderUserId() != null && !winnerIds.contains(r.getHolderUserId())) + .collect(Collectors.toList()); + + // 计算饰品总价 + BigDecimal totalOrnamentsPrice = allBoxRecords.stream() + .map(TtBoxRecords::getOrnamentsPrice) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + FightResultVO resultVO = FightResultVO.builder() + .currentRound(currentRound.longValue()) + .fight(fight) + .winnerIds(winnerIds) + .fightResult(roundResults) + .winnerPrize(winnerPrize) + .endGamePrize(endGamePrize) + .totalOrnamentsPrice(totalOrnamentsPrice) + .fightBoxVOList(fightBoxVOList) + .build(); + + log.info("用户{}查询房间{},对局进行中", userId, fightId); + return R.ok(resultVO, "对局进行中,对局最新信息"); + + } else if (fight.getStatus().equals(0)) { + log.info("用户{}查询房间{},对局准备中", userId, fightId); + return R.ok(fight, "对局准备中,对战房间最新信息"); + } + + return R.fail("未知对局状态"); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiLuckyUpgradeController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiLuckyUpgradeController.java new file mode 100644 index 0000000..6286973 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiLuckyUpgradeController.java @@ -0,0 +1,178 @@ +package com.ruoyi.playingmethod.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.upgrade.MyUpgradeRecordRequest; +import com.ruoyi.domain.dto.upgrade.UpgradeCondition; +import com.ruoyi.domain.dto.upgrade.UpgradeRecordPageRequest; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.other.ApiLuckyUpgradeBody; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.UpgradeBodyA; +import com.ruoyi.domain.vo.ApiLuckyOrnamentsDataVO; +import com.ruoyi.domain.vo.ApiLuckyUpgradeRecentPullVO; +import com.ruoyi.playingmethod.contract.api.request.TenTopUpgradeRequest; +import com.ruoyi.playingmethod.service.ApiLuckyUpgradeService; +import com.ruoyi.playingmethod.service.ApiUpgradeRecordService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.service.ApiUserPackSackService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Api(tags = "幸运升级模式") +@RestController +@RequestMapping("/api/luckyUpgrade") +public class ApiLuckyUpgradeController extends BaseController { + + @Autowired + private ApiUserPackSackService userPackSackService; + + @Autowired + private ApiUpgradeRecordService upgradeRecordService; + + private final TtUserService userService; + private final ApiLuckyUpgradeService apiLuckyUpgradeService; + private final ISysConfigService sysConfigService; + + public ApiLuckyUpgradeController(TtUserService userService, + ApiLuckyUpgradeService apiLuckyUpgradeService, + ISysConfigService sysConfigService) { + this.userService = userService; + this.apiLuckyUpgradeService = apiLuckyUpgradeService; + this.sysConfigService = sysConfigService; + } + + public R checkLogin(){ + Long userId; + try { + userId = getUserId(); + if (ObjectUtil.isEmpty(userId)) AjaxResult.error(401,"登录过期,请重新登录。"); + return R.ok(userId); + }catch (Exception e){ + return R.fail(401,"登录过期,请重新登录。"); + } + } + + @ApiOperation("获取可升级的饰品列表") + @Anonymous + @PostMapping("/getOrnamentsList") + public R> getOrnamentsList(@RequestBody ApiLuckyUpgradeBody apiLuckyUpgradeBody){ + // 幸运值按用户类型区分:已登录取真实类型,未登录默认普通用户 + String userType = UserType.COMMON_USER.getCode(); + R login = checkLogin(); + if (login.getCode().equals(200) && login.getData() != null) { + TtUser user = userService.getById(((Long) login.getData()).intValue()); + if (user != null) { + // 白号按 06 读取自己的幸运值(兼容历史数据只改了 is_blank_account、没改 user_type 的行) + if (Integer.valueOf(1).equals(user.getIsBlankAccount())) { + userType = UserType.BLANK_ACCOUNT.getCode(); + } else if (ObjectUtil.isNotEmpty(user.getUserType())) { + userType = user.getUserType(); + } + } + } + apiLuckyUpgradeBody.setUserType(userType); + List list = apiLuckyUpgradeService.getOrnamentsList(apiLuckyUpgradeBody); + return R.ok(list); + } + + @ApiOperation("幸运升级") + @UpdateUserCache + @PostMapping("/upgrade") + public R upgrade(@RequestBody @Validated UpgradeBodyA upgradeParam){ + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......"); + + String luckyUpgradeMaintenance = sysConfigService.selectConfigByKey("luckyUpgradeMaintenance"); + if ("1".equals(luckyUpgradeMaintenance)) return R.fail("幸运升级维护中......"); + + R check = checkLogin(); + if (!check.getCode().equals(200)) return R.fail(401,"登录过期,请重新登录。"); + Integer userId = ((Long)check.getData()).intValue(); + + TtUser ttUser = userService.getById(userId); + + // 余额校验(与本次升级所需金额比较)与充值记录校验均由 service 内部 upgradeCheck 统一处理。 + return apiLuckyUpgradeService.upgrade(ttUser, upgradeParam); + } + + @ApiOperation("获取幸运升级历史记录") + @PostMapping("/getUpgradeRecord") + public R getUpgradeRecord(@RequestBody @Validated UpgradeCondition param){ + return upgradeRecordService.historyDetail(param); + } + + @ApiOperation("获取幸运升级十佳记录") + @PostMapping("/tenTopUpgradeRecord") + public R tenTopUpgradeRecord(@RequestBody(required = false) TenTopUpgradeRequest request){ + Boolean isVictory = (request == null) ? null : request.getIsVictory(); + R result = upgradeRecordService.tenTopDetail(isVictory); + + // 确保结果格式一致 + if (!result.getCode().equals(200)) { + return result; + } + + Object data = result.getData(); + if (data != null && CollectionUtils.isEmpty((List)data)) { + result.setData(new ArrayList<>()); // 确保返回空列表而非 null + } + + return result; + } + + @ApiOperation("获取个人幸运升级历史记录列表") + @PostMapping("/myHistoryRecord") + public R myHistoryRecord(@RequestBody(required = false) @Validated MyUpgradeRecordRequest request){ + R check = checkLogin(); + if (!check.getCode().equals(200)) { + return R.fail(401, "登录过期,请重新登录。"); + } + Integer userId = ((Long) check.getData()).intValue(); + if (request == null) { + request = new MyUpgradeRecordRequest(); + } + return upgradeRecordService.myHistoryRecord(userId, request); + } + + @ApiOperation("获取当前饰品最近100条拉取记录(含用户头像、概率、饰品全名、磨损度、品质图、图片、价格)") + @PostMapping("/recentPulls") + public R> recentPulls(@RequestBody @Validated UpgradeCondition param) { + if (param.getUpgradeRecordId() == null) { + return R.fail("upgradeRecordId(升级饰品ID)不能为空"); + } + List list = upgradeRecordService.getRecentPulls(param.getUpgradeRecordId().intValue()); + return R.ok(list); + } + + @ApiOperation("幸运升级记录分页(scope: 0全服/1我的;itemScope: 0全部饰品/1当前饰品;均需登录)") + @PostMapping("/recordPage") + public R recordPage(@RequestBody @Validated UpgradeRecordPageRequest req) { + // 记录页无论全服/我的均需登录 + R check = checkLogin(); + if (!check.getCode().equals(200) || check.getData() == null) { + return R.fail(401, "登录过期,请重新登录。"); + } + Integer userId = ((Long) check.getData()).intValue(); + // scope=1(我的) 才按 userId 过滤;scope=0(全服) 不过滤用户 + Integer filterUserId = Integer.valueOf(1).equals(req.getScope()) ? userId : null; + // itemScope=1(当前饰品) 时 upgradeId 必填 + if (Integer.valueOf(1).equals(req.getItemScope()) && req.getUpgradeId() == null) { + return R.fail("当前饰品模式下 upgradeId(可升级饰品ID) 不能为空"); + } + return upgradeRecordService.recordPage(filterUserId, req); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiRedPackContoller.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiRedPackContoller.java new file mode 100644 index 0000000..39d0e29 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiRedPackContoller.java @@ -0,0 +1,102 @@ +package com.ruoyi.playingmethod.controller; + +import com.ruoyi.common.annotation.NoRepeatSubmit; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.domain.other.TtRedPack; +import com.ruoyi.domain.other.TtRedPacketRecord; +import com.ruoyi.playingmethod.contract.api.request.GetRedPackByKeyRequest; +import com.ruoyi.playingmethod.contract.api.request.GetRedPackDetailRequest; +import com.ruoyi.playingmethod.contract.api.request.GetRedPackListRequest; +import com.ruoyi.playingmethod.contract.api.request.GetRedPackRequest; +import com.ruoyi.playingmethod.service.ApiRedPackService; +import com.ruoyi.thirdparty.wechat.domain.PtLoginUser; +import com.ruoyi.thirdparty.wechat.service.ApiTokenService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 不用这个api领红包了 + */ +@Deprecated +@RestController +@RequestMapping("/api/redPackData") +@Api(tags = "红包") +public class ApiRedPackContoller extends BaseController { + + @Autowired + private ApiTokenService tokenService; + + @Autowired + private ApiRedPackService redPackService; + + // 已停用(无调用方) + // @PostMapping("/getRedPackList") + @UserPermission + @NoRepeatSubmit + @ApiOperation(value = "获取红包列表") + public TableDataInfo getRedPackList(HttpServletRequest request, @RequestBody GetRedPackListRequest getRedPackListRequest) { + PtLoginUser user = tokenService.getLoginUser(request); + + startPage(); + List redPacks = redPackService.getRedPackList(user, getRedPackListRequest.getType()); + + return getDataTable(redPacks); + } + + @Deprecated + // 已停用(无调用方) + // @PostMapping("/getRedPack") + @UserPermission + @NoRepeatSubmit + @ApiOperation(value = "领取红包") + public AjaxResult getRedPack(@RequestBody GetRedPackRequest request) { + Long userId = getUserId(); + return redPackService.getRedPack(userId, request.getRedPackId()); + } + + // 已停用:口令兑换请走 POST /api/bonus/receiveRedPacket(唯一的正规入口) + // @PostMapping("/getRedPackByKey") + @UserPermission + @NoRepeatSubmit + @ApiOperation(value = "根据红包口令兑换红包") + public AjaxResult getRedPackByKey(@RequestBody GetRedPackByKeyRequest request) { + LoginUser loginUser = getLoginUser(); + if (request.getRedPackKey() == null || "".equals(request.getRedPackKey())) { + return AjaxResult.error("没有填写红包口令"); + } + return redPackService.getRedPackByKey(loginUser, request.getRedPackKey()); + } + + // 已停用(无调用方) + // @PostMapping("/getActivityDataHis") + @UserPermission + @NoRepeatSubmit + @ApiOperation(value = "查询参与的红包活动记录") + public TableDataInfo getBoxDataHis(HttpServletRequest request) { + PtLoginUser user = tokenService.getLoginUser(request); + startPage(); + List boxRecords = redPackService.getActivityDataHis(user); + + return getDataTable(boxRecords); + } + + // 已停用(无调用方) + // @PostMapping("/getRedPackDataDetail") + @UserPermission + @NoRepeatSubmit + @ApiOperation(value = "查询历史记录") + public AjaxResult getRedPackDataDetail(HttpServletRequest request, @RequestBody GetRedPackDetailRequest getRedPackDetailRequest) { + PtLoginUser user = tokenService.getLoginUser(request); + return redPackService.getRedPackDataDetail(user, getRedPackDetailRequest.getRecordId()); + } + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiReplacementRecordController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiReplacementRecordController.java new file mode 100644 index 0000000..22a60ee --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiReplacementRecordController.java @@ -0,0 +1,42 @@ +package com.ruoyi.playingmethod.controller; + +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.playingmethod.model.body.SynthesizeRequest; +import com.ruoyi.playingmethod.service.IApiReplacementRecordService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; + + +/** + * 汰换记录Controller + * + * @author junhai + * @date 2023-09-10 + */ +@RestController +@RequestMapping("/api/replacementRecord") +@Api(tags = "汰换记录") +public class ApiReplacementRecordController extends BaseController { + + @Autowired + private IApiReplacementRecordService ttReplacementRecordService; + + @PostMapping("/synthesizeItems") + @UserPermission + @ApiOperation("汰换饰品") + public AjaxResult synthesizeItems(@RequestBody SynthesizeRequest synthesizeRequest) { + LoginUser loginUser = getLoginUser(); + return ttReplacementRecordService.synthesizeItems(loginUser, synthesizeRequest.getBoIds()); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiRollController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiRollController.java new file mode 100644 index 0000000..ac8267e --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiRollController.java @@ -0,0 +1,193 @@ +package com.ruoyi.playingmethod.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig; +import com.ruoyi.domain.dto.roll.GetRollOpenPrizeParam; +import com.ruoyi.domain.dto.roll.GetRollPlayersParam; +import com.ruoyi.domain.dto.roll.GetRollPrizePool; +import com.ruoyi.domain.entity.roll.TtRoll; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsVO; +import com.ruoyi.domain.vo.roll.RollUserVO; +import com.ruoyi.playingmethod.contract.api.request.GetRollListParamRequest; +import com.ruoyi.playingmethod.contract.api.request.RollIdRequest; +import com.ruoyi.playingmethod.service.ApiRollService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.domain.vo.RollDetailsDataVO; +import com.ruoyi.domain.vo.RollListDataVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; + +@Api(tags = "ROLL房模式") +@RestController +@RequestMapping("/api/roll") +public class ApiRollController extends BaseController { + + private final ISysConfigService sysConfigService; + private final TtUserService userService; + private final ApiRollService apiRollService; + + public ApiRollController(ISysConfigService sysConfigService, + TtUserService userService, + ApiRollService apiRollService) { + this.sysConfigService = sysConfigService; + this.userService = userService; + this.apiRollService = apiRollService; + } + + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class JoinRollParam { + @NotNull(message = "roll房id不能为空。") + private Integer rollId; + private String rollPassword; + } + + @ApiOperation("加入 ROLL 房") + @RepeatSubmit(message = "操作过于频繁,请稍后重试!") + @PostMapping("/joinRoll") + public R joinRoll(@RequestBody @Validated JoinRollParam param) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......"); + String rollMaintenance = sysConfigService.selectConfigByKey("rollMaintenance"); + if ("1".equals(rollMaintenance)) return R.fail("Roll 房功能正在维护中......"); + + Long userId = getUserId(); + if (ObjectUtil.isEmpty(userId)) return R.fail(401, "登录过期,请重新登录。"); + TtUser player = userService.getById(userId); + if (ObjectUtil.isEmpty(player)) return R.fail("异常的用户状态。"); + // 新增:检查用户是否被封禁或已删除 + if ("1".equals(player.getStatus())) return R.fail("您的账号已被封禁,无法参与 Roll 房。"); + if (!"0".equals(player.getDelFlag())) return R.fail("您的账号已被删除,无法参与 Roll 房。"); + + return apiRollService.joinRoll(param, player); + // return StringUtils.isEmpty(msg) ? R.ok("成功加入Roll房!") : R.fail(msg, "加入失败!"); + } + + + @ApiOperation("获取ROLL房列表") + @Anonymous + @PostMapping("/getRollList") + public TableDataInfo getRollList(@RequestBody GetRollListParamRequest param) { + // 支持免登录访问,登录用户可获取个性化数据 + try { + Long userId = getUserId(); + if (userId != null && param.getRollStatus() != null && (param.getRollStatus() == 1 || param.getRollStatus() == 2)) { + param.setUserId(userId.intValue()); + } + } catch (Exception ignore) { + // 未登录时忽略 userId 设置 + } + startPage(); + List list = apiRollService.getRollList(param); + return getDataTable(list); + } + + /** + * 我参与的ROLL房列表 + */ + // @GetMapping("/getMyPartRollList") + public PageDataInfo getMyPartRollList(@RequestBody GetRollListParamRequest param) { + startPage(); + List list = apiRollService.getRollList(param); + return getPageData(list); + } + + @ApiOperation("获取ROLL房详情") + @PostMapping("/getRollDetails") + public R getRollDetails(@RequestBody RollIdRequest request) { + Long userId = getUserId(); + return apiRollService.getRollDetails(request.getRollId(), userId); + } + + @ApiOperation("获取ROLL房参与人员详情") + @PostMapping("/getRollPlayers") + public R> getRollPlayers(@RequestBody @Validated GetRollPlayersParam param) { + return apiRollService.getRollPlayers(param); + } + + @ApiOperation("获取ROLL房奖池详情") + @PostMapping("/getRollPrizePool") + public R> getRollPrizePool(@RequestBody @Validated GetRollPrizePool param) { + return apiRollService.getRollPrizePool(param); + } + + @ApiOperation("获取ROLL开奖详情") + @PostMapping("/getRollOpenPrize") + public R> getRollOpenPrize(@RequestBody @Validated GetRollOpenPrizeParam param) { + return apiRollService.getRollOpenPrize(param); + } + + @ApiOperation("开奖测试,正式环境删除") + @PostMapping("/endRoll") + public R endRoll(@RequestBody RollIdRequest request){ + // TODO: 移除或添加管理员权限校验 + return apiRollService.endROLL(request.getRollId()); + } + + // @Autowired + private RabbitTemplate rabbitTemplate; + + // @GetMapping("/endRoll/{rollId}") + public String mqTest(@PathVariable("rollId") Integer rollId) { + + // Calendar c = Calendar.getInstance(); + // long timeInMillis = c.getTimeInMillis(); + // c.add(Calendar.SECOND, 3); + // long timeInMillis1 = c.getTimeInMillis(); + // long l = timeInMillis1 - timeInMillis; + + MessageProperties messageProperties = new MessageProperties(); + messageProperties.setExpiration("3000"); // 消息的过期属性,单位 ms + Message message = new Message("这条消息 4 秒后过期".getBytes(), messageProperties); + + rabbitTemplate.convertAndSend( + DelayedQueueConfig.OPEN_ROLL_EXCHANGE, + DelayedQueueConfig.OPEN_ROLL_KEY, + message + ); + + return "1"; + } + + @ApiOperation("roll房间号搜索") + @PostMapping("/searchRollId") + public TableDataInfo searchRoll(@RequestBody RollIdRequest request){ + GetRollListParamRequest param = new GetRollListParamRequest(); + param.setRollId(request.getRollId()); + List list = apiRollService.getRollList(param); + TableDataInfo rspData = new TableDataInfo(); + rspData.setCode(200); + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal(list.size()); + return rspData; + } + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiTtAttendanceRecordController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiTtAttendanceRecordController.java new file mode 100644 index 0000000..3ee98af --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiTtAttendanceRecordController.java @@ -0,0 +1,52 @@ +package com.ruoyi.playingmethod.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.playingmethod.entity.TtAttendanceRecord; +import com.ruoyi.playingmethod.service.IApiAttendanceRecordService; +import io.swagger.annotations.Api; +import org.apache.poi.ss.formula.functions.T; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Api(tags = "签到记录") +@RestController +@RequestMapping("/api/attendanceRecord") +public class ApiTtAttendanceRecordController extends BaseController { + private final IApiAttendanceRecordService iApiAttendanceRecordService; + + public ApiTtAttendanceRecordController(IApiAttendanceRecordService iApiAttendanceRecordService) { + this.iApiAttendanceRecordService = iApiAttendanceRecordService; + } + + /** + * 签到 + */ + // @GetMapping("/attendance") + public R attendance() { + System.out.println("==================签到================="); + TtAttendanceRecord ttAttendanceRecord = iApiAttendanceRecordService.selectByUid(getUserId().intValue()); + if (ttAttendanceRecord == null){ + // 增加签到记录 + iApiAttendanceRecordService.insert(getUserId().intValue()); + return R.ok("签到成功"); + } + // List userCompoundRecord = iApiAttendanceRecordService.getUserCompoundRecord(getUserId().intValue()); + return R.fail("签到失败,一天只能签到一次"); + } + + /** + * 七天签到记录 + */ + // @GetMapping("/sevenAttendance") + public R> sevenAttendance() { + System.out.println("==================七天签到================="); + // TtAttendanceRecord ttAttendanceRecord = iApiAttendanceRecordService.selectByUid(getUserId().intValue()); + List ttAttendanceRecordList = iApiAttendanceRecordService.selectSevenAttendance(getUserId().intValue()); + return R.ok(ttAttendanceRecordList); + } + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiUserAvatarController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiUserAvatarController.java new file mode 100644 index 0000000..9eec425 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiUserAvatarController.java @@ -0,0 +1,48 @@ +package com.ruoyi.playingmethod.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.service.TtUserAvatarService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.other.TtUserAvatar; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +@Api(tags = "用户头像") +@RestController +@RequestMapping("/api/userAvatar") +public class ApiUserAvatarController extends BaseController { + + private final TtUserAvatarService userAvatarService; + + public ApiUserAvatarController(TtUserAvatarService userAvatarService) { + this.userAvatarService = userAvatarService; + } + + @ApiOperation("用户头像列表") + @PostMapping("/list") + public R> list() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtUserAvatar::getStatus, "0") + .orderByAsc(TtUserAvatar::getSort); + List list = userAvatarService.list(wrapper); + return R.ok(list); + } + + @ApiOperation("获取头像图片资源路径列表") + @PostMapping("/images") + public R> getAvatarImages() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtUserAvatar::getStatus, "0") + .orderByAsc(TtUserAvatar::getSort); + List avatarUrls = userAvatarService.list(wrapper) + .stream() + .map(TtUserAvatar::getAvatar) + .collect(Collectors.toList()); + return R.ok(avatarUrls); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiWelfareController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiWelfareController.java new file mode 100644 index 0000000..5c26c79 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiWelfareController.java @@ -0,0 +1,117 @@ +package com.ruoyi.playingmethod.controller; + +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.service.TtWelfareService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.domain.entity.TtWelfareBoxInventory; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtWelfare; +import com.ruoyi.domain.vo.OpenBoxVO; +import com.ruoyi.playingmethod.contract.api.request.ClaimWelfareRequest; +import com.ruoyi.playingmethod.contract.api.request.OpenWelfareBoxRequest; +import com.ruoyi.playingmethod.model.ApiWelfare; +import com.ruoyi.playingmethod.service.ApiWelfareService; +import com.ruoyi.system.service.ISysConfigService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@Api(tags = "福利") +@RestController +@RequestMapping("/api/welfare") +public class ApiWelfareController extends BaseController { + + @Autowired + private ApiWelfareService apiWelfareService; + + @Autowired + private TtWelfareService ttWelfareService; + + @Autowired + private ISysConfigService sysConfigService; + + @Autowired + private TtUserService userService; + + @ApiOperation("获取福利列表") + @ApiResponse(code = 200, message = "eligible【是否具备领取条件(0否 1是)】" + + "
claimStatus【领取状态(0未领取 1已领取)】") + @PostMapping("/getWelfareList") + public TableDataInfo getWelfareList() { + startPage(); + Long userId = getUserId(); + List list = apiWelfareService.getWelfareList(userId); + return getDataTable(list); + } + + @ApiOperation("领取福利") + @PostMapping("/claimWelfare") + public AjaxResult claimWelfare(@RequestBody ClaimWelfareRequest request) { + Long userId = getUserId(); + // 检查福利是否存在 + TtWelfare ttWelfare = ttWelfareService.selectTtWelfareByWelfareId(request.getWelfareId()); + if (Objects.isNull(ttWelfare)) { + return error("福利不存在"); + } + // 检查是否尚未具备领取条件 + if (apiWelfareService.checkNotEligible(request.getWelfareId(), userId)) { + return AjaxResult.error("尚未达到领取条件"); + } + // 检查是否已领取 + if (apiWelfareService.checkClaimed(request.getWelfareId(), userId)) { + return AjaxResult.error("该福利已被领取"); + } + // 进入开箱流程 + String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance"); + if ("1".equals(bindBoxMaintenance)) { + return AjaxResult.error("盲盒开箱功能正在维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + if ("0".equals(ttUser.getIsRealCheck())) { + return AjaxResult.realNameRequired(); + } + OpenBoxVO openBoxVO = apiWelfareService.claimWelfare(request.getWelfareId(), userId); + return AjaxResult.success(openBoxVO); + } + + @ApiOperation("每日领取福利箱子") + @PostMapping("/claimDailyBox") + public R claimDailyBox() { + Long userId = getUserId(); + try { + TtWelfareBoxInventory inventory = apiWelfareService.claimDailyBox(userId); + return R.ok(inventory); + } catch (RuntimeException e) { + return R.fail(e.getMessage()); + } + } + + @ApiOperation("批量开启福利箱子(1-15个)") + @PostMapping("/openBoxes") + public R openBoxes(@RequestBody @Validated OpenWelfareBoxRequest request) { + Long userId = getUserId(); + String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance"); + if ("1".equals(bindBoxMaintenance)) { + return R.fail("盲盒开箱功能正在维护中......"); + } + TtUser ttUser = userService.getById(userId); + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired(); + } + try { + List results = apiWelfareService.openWelfareBoxes( + request.getBoxInventoryIds(), userId); + return R.ok(results); + } catch (RuntimeException e) { + return R.fail(e.getMessage()); + } + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiWelfareMonthlyRechargesController.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiWelfareMonthlyRechargesController.java new file mode 100644 index 0000000..fa754b6 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/controller/ApiWelfareMonthlyRechargesController.java @@ -0,0 +1,83 @@ +package com.ruoyi.playingmethod.controller; + +import com.ruoyi.admin.service.ITtWelfareMonthlyRechargesService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtWelfareMonthlyRecharges; +import com.ruoyi.domain.vo.OpenBoxVO; +import com.ruoyi.playingmethod.contract.api.request.ClaimWelfareRequest; +import com.ruoyi.playingmethod.model.ApiWelfare; +import com.ruoyi.playingmethod.model.vo.ApiWelfareMonthlyRechargesVO; +import com.ruoyi.playingmethod.service.ApiWelfareMonthlyRechargesService; +import com.ruoyi.system.service.ISysConfigService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Objects; + +@Api(tags = "每月充值福利") +@RestController +@RequestMapping("/api/welfare/monthlyRecharges") +public class ApiWelfareMonthlyRechargesController extends BaseController { + + @Autowired + private ApiWelfareMonthlyRechargesService apiWelfareMonthlyRechargesService; + + @Autowired + private ITtWelfareMonthlyRechargesService ttWelfareMonthlyRechargesService; + + @Autowired + private ISysConfigService sysConfigService; + + @Autowired + private TtUserService userService; + + @ApiOperation("获取每月充值福利列表") + @ApiResponse(code = 200, message = "eligible【是否具备领取条件(0否 1是)】" + + "
claimStatus【领取状态(0未领取 1已领取)】") + @PostMapping("/list") + public TableDataInfo getWelfareList() { + startPage(); + Long userId = SecurityUtils.getUserId(); + List list = apiWelfareMonthlyRechargesService.getWelfareList(userId); + return getDataTable(list); + } + + @ApiOperation("领取福利") + @PostMapping("/claimWelfare") + public AjaxResult claimWelfare(@RequestBody ClaimWelfareRequest request) { + Long userId = SecurityUtils.getUserId(); + // 检查福利是否存在 + TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges = ttWelfareMonthlyRechargesService.selectTtWelfareMonthlyRechargesById(request.getWelfareId()); + if (Objects.isNull(ttWelfareMonthlyRecharges)) { + return error("福利不存在"); + } + // 检查是否尚未具备领取条件 + if (!apiWelfareMonthlyRechargesService.checkEligible(request.getWelfareId(), userId)) { + return AjaxResult.error("尚未达到领取条件"); + } + // 检查是否已领取 + if (apiWelfareMonthlyRechargesService.checkClaimed(request.getWelfareId(), userId)) { + return AjaxResult.error("该福利已被领取"); + } + // 进入开箱流程 + String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance"); + if ("1".equals(bindBoxMaintenance)) { + return AjaxResult.error("盲盒开箱功能正在维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + if ("0".equals(ttUser.getIsRealCheck())) { + return AjaxResult.realNameRequired(); + } + OpenBoxVO openBoxVO = apiWelfareMonthlyRechargesService.claimWelfare(request.getWelfareId(), userId); + return AjaxResult.success(openBoxVO); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/customException/CreateFightException.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/customException/CreateFightException.java new file mode 100644 index 0000000..2e4ea32 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/customException/CreateFightException.java @@ -0,0 +1,7 @@ +package com.ruoyi.playingmethod.customException; + +public class CreateFightException extends RuntimeException{ + public CreateFightException(String msg){ + super(msg); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/customException/WsRepetitiontException.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/customException/WsRepetitiontException.java new file mode 100644 index 0000000..b379e41 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/customException/WsRepetitiontException.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.customException; + +// websocket用户重复连接异常 +public class WsRepetitiontException extends RuntimeException{ + public WsRepetitiontException(String msg){ + super(msg); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/domain/vo/ApiRobotFightGroupVO.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/domain/vo/ApiRobotFightGroupVO.java new file mode 100644 index 0000000..c2139ef --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/domain/vo/ApiRobotFightGroupVO.java @@ -0,0 +1,20 @@ +package com.ruoyi.playingmethod.domain.vo; + +import com.ruoyi.domain.other.TtRobotFightGroupBox; +import lombok.Data; + +import java.util.List; + +@Data +public class ApiRobotFightGroupVO { + + private Integer groupId; + + private String groupName; + + private Integer seatNum; + + private String model; + + private List ttRobotFightGroupBoxList; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/domain/vo/CompValueVO.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/domain/vo/CompValueVO.java new file mode 100644 index 0000000..caefbeb --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/domain/vo/CompValueVO.java @@ -0,0 +1,13 @@ +package com.ruoyi.playingmethod.domain.vo; + +import lombok.Data; + +@Data +public class CompValueVO { + + /** 当前补偿值 */ + private Integer currentCompValue; + + /** 设定补偿值 */ + private Integer desiredCompValue; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/entity/TtAttendanceRecord.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/entity/TtAttendanceRecord.java new file mode 100644 index 0000000..81cd89c --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/entity/TtAttendanceRecord.java @@ -0,0 +1,47 @@ +package com.ruoyi.playingmethod.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TtAttendanceRecord { + + private static final long serialVersionUID = 1L; + + /** */ + private Integer id; + + /** 用户ID */ + @Excel(name = "用户ID") + private Integer userId; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 获取金币数 */ + @Excel(name = "获取金币数") + private Integer coin; + + @Excel(name = "是否删除") + private String delFlag; + + + private String isStatus = "0"; + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiAttendanceRecordMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiAttendanceRecordMapper.java new file mode 100644 index 0000000..d6752a7 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiAttendanceRecordMapper.java @@ -0,0 +1,19 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.playingmethod.entity.TtAttendanceRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +@Mapper +public interface ApiAttendanceRecordMapper { + TtAttendanceRecord selectByUid(@Param("uid") Integer uid); + + int insertAttendanceRecord(TtAttendanceRecord ttAttendanceRecord); + + + TtAttendanceRecord selectSevenAttendance(@Param("uid") Integer uid,@Param("createTime") String createTime); + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiBattleRoyaleMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiBattleRoyaleMapper.java new file mode 100644 index 0000000..466f2f9 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiBattleRoyaleMapper.java @@ -0,0 +1,7 @@ +package com.ruoyi.playingmethod.mapper; + +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ApiBattleRoyaleMapper { +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiBindBoxMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiBindBoxMapper.java new file mode 100644 index 0000000..b138df6 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiBindBoxMapper.java @@ -0,0 +1,38 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.domain.other.TtBoxUser; +import com.ruoyi.domain.other.TtBoxA; +import com.ruoyi.domain.other.TtOrnamentsA; +import com.ruoyi.domain.other.TtBoxLevelA; +import com.ruoyi.playingmethod.controller.ApiBindBoxController; +import com.ruoyi.playingmethod.domain.vo.CompValueVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApiBindBoxMapper { + + TtBoxA getBoxData(Integer boxId); + + List getBoxOrnamentsList(Integer boxId); + + /** 对战宝箱饰品列表,概率使用 robot_odds */ + List getFightBoxOrnamentsList(Integer boxId); + + List getBoxList( + @Param("boxTypeId") Integer boxTypeId, + @Param("homeFlag") String homeFlag, + @Param("boxType") String boxType, + @Param("isFight") String isFight + ); + + TtOrnamentsA getOrnamentsData(@Param("boxId") Integer boxId, @Param("ornamentsId") Integer ornamentsId, @Param("id") Long id); + + // List getBindBoxHistory(ApiBindBoxController.boxHistoryParam param); + + List getProbabilityDistribution(Integer boxId); + + TtOrnamentsA ornamentsInfo(@Param("boxId") Integer boxId,@Param("ornamentId") String ornamentId); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiBoxOrnamentsMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiBoxOrnamentsMapper.java new file mode 100644 index 0000000..a46242e --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiBoxOrnamentsMapper.java @@ -0,0 +1,10 @@ +package com.ruoyi.playingmethod.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.other.TtBoxOrnaments; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ApiBoxOrnamentsMapper extends BaseMapper { + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiCompoundMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiCompoundMapper.java new file mode 100644 index 0000000..c4fb1ce --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiCompoundMapper.java @@ -0,0 +1,13 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.domain.vo.UserPackSackDataVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ApiCompoundMapper { + UserPackSackDataVO selectCompoundDataById(Long packSackId); + + List selectCompoundRecordByUserId(Integer userId); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiFightMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiFightMapper.java new file mode 100644 index 0000000..4740a3b --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiFightMapper.java @@ -0,0 +1,49 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.domain.other.TtBoxA; +import com.ruoyi.domain.vo.ApiFightListDataVO; +import com.ruoyi.domain.vo.JoinFightUserDataVO; +import com.ruoyi.domain.vo.UserPackSackDataVO; +import com.ruoyi.playingmethod.model.vo.ApiFightRankingVO; +import com.ruoyi.playingmethod.model.vo.ApiFightTenTopVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +@Mapper +public interface ApiFightMapper { + + JoinFightUserDataVO selectJoinFightUserData(Integer fightUserId); + + List selectOrnamentsDataListByUserIdAndFightId(@Param("userId") Integer userId, @Param("fightId") Integer fightId); + + List getFightList(@Param("model") String model, @Param("status") String status, @Param("userId") Integer userId, @Param("fightId") Integer fightId); + + List getFightBoxList(Integer boxTypeId); + + /** + * 获取指定日期的对战排行榜 + */ + List getFightRankingByDate(String date); + + /** + * 盲盒对战十佳对战(keyword 仅支持房间号 fightId 精确查询) + */ + List tenTopFight(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("maxType") Integer maxType, @Param("model") String model, @Param("minPrice") Integer minPrice, @Param("maxPrice") Integer maxPrice, @Param("fightId") Integer fightId); + /** + * 查看对战开箱出来的总金额 + */ + List fightOrnamentsPrice(@Param("fightIds") List fightIds); + + /** + * 查询房主在对战中获得饰品总价值(按 fightId 批量) + */ + List ownerEarnPrice(@Param("fightIds") List fightIds); + + /** + * 查询每个持有者在对战中获得的饰品总价值(按 fightId + holder_user_id 批量) + */ + List holderOrnamentsPrice(@Param("fightIds") List fightIds); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiLuckyUpgradeMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiLuckyUpgradeMapper.java new file mode 100644 index 0000000..c115a87 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiLuckyUpgradeMapper.java @@ -0,0 +1,17 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.domain.dto.upgrade.UpgradeCondition; +import com.ruoyi.domain.vo.ApiLuckyOrnamentsDataVO; +import com.ruoyi.domain.other.ApiLuckyUpgradeBody; +import com.ruoyi.domain.vo.ApiLuckyUpgradeRecordDataVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApiLuckyUpgradeMapper { + List getOrnamentsList(ApiLuckyUpgradeBody apiLuckyUpgradeBody); + + List getUpgradeRecord(UpgradeCondition param); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiReplacementRecordMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiReplacementRecordMapper.java new file mode 100644 index 0000000..8ca0292 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiReplacementRecordMapper.java @@ -0,0 +1,80 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.other.TtReplacementRecord; +import com.ruoyi.domain.vo.UserPackSackDataVO; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 汰换记录Mapper接口 + * + * @author junhai + * @date 2023-09-10 + */ +public interface ApiReplacementRecordMapper { + /** + * 查询汰换记录 + * + * @param id 汰换记录主键 + * @return 汰换记录 + */ + public TtReplacementRecord selectTtReplacementRecordById(Long id); + + /** + * 查询汰换记录列表 + * + * @param ttReplacementRecord 汰换记录 + * @return 汰换记录集合 + */ + public List selectTtReplacementRecordList(TtReplacementRecord ttReplacementRecord); + + /** + * 新增汰换记录 + * + * @param ttReplacementRecord 汰换记录 + * @return 结果 + */ + public int insertTtReplacementRecord(TtReplacementRecord ttReplacementRecord); + + /** + * 修改汰换记录 + * + * @param ttReplacementRecord 汰换记录 + * @return 结果 + */ + public int updateTtReplacementRecord(TtReplacementRecord ttReplacementRecord); + + /** + * 删除汰换记录 + * + * @param id 汰换记录主键 + * @return 结果 + */ + public int deleteTtReplacementRecordById(Long id); + + /** + * 批量删除汰换记录 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteTtReplacementRecordByIds(Long[] ids); + + /** + * 根据以下参数进行饰品的查询 + * + * @param map + * @return + */ + List findByPriceRange(Map map); + + /** + * 查询背包饰品 + */ + List selectUserPackSack(@Param("userId") Integer userId, + @Param("itemIds") List itemIds); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiRobotFightGroupMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiRobotFightGroupMapper.java new file mode 100644 index 0000000..805f5df --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiRobotFightGroupMapper.java @@ -0,0 +1,12 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.playingmethod.domain.vo.ApiRobotFightGroupVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ApiRobotFightGroupMapper { + + public List selectApiRobotFightGroupBoxVOList(); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiRollMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiRollMapper.java new file mode 100644 index 0000000..4f62ba7 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiRollMapper.java @@ -0,0 +1,19 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.domain.other.JackpotData; +import com.ruoyi.domain.vo.RollDetailsDataVO; +import com.ruoyi.domain.vo.RollListDataVO; +import com.ruoyi.playingmethod.contract.api.request.GetRollListParamRequest; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ApiRollMapper { + + List getRollList(GetRollListParamRequest param); + + RollDetailsDataVO getRollDetails(Integer rollId); + + List getJackpotData(Integer jackpotId); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiTtUserBlendErcashMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiTtUserBlendErcashMapper.java new file mode 100644 index 0000000..68bc73a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiTtUserBlendErcashMapper.java @@ -0,0 +1,40 @@ +package com.ruoyi.playingmethod.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.other.TtUpgradeRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface ApiTtUserBlendErcashMapper extends BaseMapper { + + /** + * 查询单用户指定 source/type 的累计金额(主表 + 归档表 UNION ALL) + */ + @Select("SELECT IFNULL(SUM(ABS(tube.amount)), 0) FROM (" + + "SELECT amount FROM tt_user_blend_ercash WHERE user_id = #{userId} AND source = #{source} AND type = #{type} " + + "UNION ALL " + + "SELECT amount FROM tt_user_blend_ercash_archive WHERE user_id = #{userId} AND source = #{source} AND type = #{type}" + + ") tube") + BigDecimal sumRechargeWithArchive(@Param("userId") Integer userId, + @Param("source") Integer source, + @Param("type") Integer type); + + /** + * 批量查询指定用户列表的累计金额(兼容 sumAmountByUserIdsAndSource 接口) + */ + default BigDecimal sumAmountByUserIdsAndSource(List userIds, Integer source, Integer type) { + if (userIds == null || userIds.isEmpty()) return BigDecimal.ZERO; + BigDecimal total = BigDecimal.ZERO; + for (Integer uid : userIds) { + BigDecimal amt = sumRechargeWithArchive(uid, source, type); + if (amt != null) total = total.add(amt); + } + return total; + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiUpgradeRecordMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiUpgradeRecordMapper.java new file mode 100644 index 0000000..5b1f788 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiUpgradeRecordMapper.java @@ -0,0 +1,29 @@ +package com.ruoyi.playingmethod.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.ruoyi.domain.other.TtUpgradeRecord; +import com.ruoyi.domain.vo.ApiLuckyUpgradeRecentPullVO; +import com.ruoyi.domain.vo.ApiUpgradeRecordItemVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApiUpgradeRecordMapper extends BaseMapper { + + /** 最近100条拉取记录(按 upgradeId 查,所有用户) */ + List getRecentPulls(@Param("upgradeId") Integer upgradeId); + + /** + * 幸运升级记录分页(全服/我的 × 当前饰品/全部饰品) + * + * @param page 分页参数 + * @param userId 非空=仅查该用户(我的);null=全服 + * @param upgradeId 非空=仅查该可升级饰品(当前饰品);null=全部饰品 + */ + IPage selectRecordPage(IPage page, + @Param("userId") Integer userId, + @Param("upgradeId") Long upgradeId); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiWelfareBoxInventoryMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiWelfareBoxInventoryMapper.java new file mode 100644 index 0000000..c58a288 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiWelfareBoxInventoryMapper.java @@ -0,0 +1,37 @@ +package com.ruoyi.playingmethod.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtWelfareBoxInventory; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApiWelfareBoxInventoryMapper extends BaseMapper { + + /** + * 检查用户今日是否已领取 + */ + boolean checkTodayClaimed(@Param("userId") Long userId); + + /** + * 查询今日已领取的用户ID列表(定时任务批量发放用) + */ + List selectTodayClaimedUserIds(); + + /** + * 批量插入福利箱子库存 + */ + int batchInsertInventory(@Param("list") List list); + + /** + * 查询用户未开启的福利箱子 + */ + List getUnopenedBoxes(@Param("userId") Long userId); + + /** + * 批量更新状态 + */ + int batchUpdateStatus(@Param("ids") List ids, @Param("status") Integer status); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiWelfareMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiWelfareMapper.java new file mode 100644 index 0000000..18ed2e8 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiWelfareMapper.java @@ -0,0 +1,33 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.playingmethod.model.ApiWelfare; +import com.ruoyi.playingmethod.model.ApiWelfareRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface ApiWelfareMapper { + + List getWelfareList(Long userId); + + /** + * 检查是否已领取 + */ + boolean checkClaimed( + @Param("welfareId") Integer welfareId, + @Param("userId") Long userId, + @Param("type") String type); + + Integer getBoxIdByWelfareId(Integer welfareId); + + int saveClaimWelfareRecord(ApiWelfareRecord apiWelfareRecord); + + /** + * 批量保存领取记录(定时任务批量发放用) + */ + int batchSaveClaimRecord(@Param("list") List list); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiWelfareMonthlyRechargesMapper.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiWelfareMonthlyRechargesMapper.java new file mode 100644 index 0000000..06546b6 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/mapper/ApiWelfareMonthlyRechargesMapper.java @@ -0,0 +1,33 @@ +package com.ruoyi.playingmethod.mapper; + +import com.ruoyi.playingmethod.model.ApiWelfare; +import com.ruoyi.playingmethod.model.ApiWelfareRecord; +import com.ruoyi.playingmethod.model.vo.ApiWelfareMonthlyRechargesVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface ApiWelfareMonthlyRechargesMapper { + + List getWelfareList(Long userId); + + /** + * 查询当月充值量 + */ + BigDecimal selectRechargeRecordsOfMonth(Long userId); + + /** + * 检查是否已领取 + */ + boolean checkClaimed( + @Param("welfareId") Integer welfareId, + @Param("userId") Long userId, + @Param("type") String type); + + Integer getBoxIdByWelfareId(Integer welfareId); + + int saveClaimWelfareRecord(ApiWelfareRecord apiWelfareRecord); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/ApiWelfare.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/ApiWelfare.java new file mode 100644 index 0000000..3c8717e --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/ApiWelfare.java @@ -0,0 +1,51 @@ +package com.ruoyi.playingmethod.model; + +import com.ruoyi.domain.other.TtBox; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ApiWelfare { + + /** + * 福利ID + */ + private Integer welfareId; + + /** + * 福利名称 + */ + private String welfareName; + + /** + * 福利类型 + */ + private String type; + + /** + * VIP等级 + */ + private Integer vipLevel; + + /** + * 是否具备领取条件(0否 1是) + */ + private String eligible; + + /** + * 领取状态(0未领取 1已领取) + */ + private String claimStatus; + + /** + * 对应等级达标金额 + */ + private BigDecimal rechargeThreshold; + + /** + * 宝箱信息 + */ + private TtBox ttBox; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/ApiWelfareRecord.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/ApiWelfareRecord.java new file mode 100644 index 0000000..4e22b84 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/ApiWelfareRecord.java @@ -0,0 +1,17 @@ +package com.ruoyi.playingmethod.model; + +import lombok.Data; + +import java.util.Date; + +@Data +public class ApiWelfareRecord { + + private Integer welfareId; + + private Long userId; + + private String type; + + private Date createTime; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/body/SynthesizeRequest.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/body/SynthesizeRequest.java new file mode 100644 index 0000000..b7dcdb1 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/body/SynthesizeRequest.java @@ -0,0 +1,12 @@ +package com.ruoyi.playingmethod.model.body; + +import lombok.Data; + +import java.util.List; + +@Data +public class SynthesizeRequest { + + private List boIds; +// private List yoIds; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiFightRankingVO.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiFightRankingVO.java new file mode 100644 index 0000000..1c7a3a8 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiFightRankingVO.java @@ -0,0 +1,17 @@ +package com.ruoyi.playingmethod.model.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ApiFightRankingVO { + + private Long userId; + + private String nickName; + + private String avatar; + + private BigDecimal totalBoxPrice; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiFightTenTopVo.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiFightTenTopVo.java new file mode 100644 index 0000000..b69f2ca --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiFightTenTopVo.java @@ -0,0 +1,29 @@ +package com.ruoyi.playingmethod.model.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ApiFightTenTopVo { + /** + * 对战ID + */ + private Integer id; + /** + * 持有者/玩家ID + */ + private Integer userId; + /** + * 宝箱开出来饰品总价值 + */ + private BigDecimal totalOrnamentsPrice; + /** + * 宝箱开出来饰品减去宝箱总金额 * 玩家个数 + */ + private BigDecimal increaseOrnamentsPrice; + /** + * 倍数:总饰品金额 / (宝箱单价 * 玩家数) + */ + private BigDecimal multiple; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiWelfareMonthlyRechargesVO.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiWelfareMonthlyRechargesVO.java new file mode 100644 index 0000000..44680b5 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiWelfareMonthlyRechargesVO.java @@ -0,0 +1,28 @@ +package com.ruoyi.playingmethod.model.vo; + +import com.ruoyi.domain.other.TtBox; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ApiWelfareMonthlyRechargesVO { + + /** 福利ID */ + private Integer welfareId; + + /** 福利名称 */ + private String welfareName; + + /** 充值金额 */ + private BigDecimal amount; + + /** 是否具备领取条件(0否 1是) */ + private String eligible; + + /** 领取状态(0未领取 1已领取) */ + private String claimStatus; + + /** 宝箱信息 */ + private TtBox ttBox; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiWelfareVO.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiWelfareVO.java new file mode 100644 index 0000000..fa3254e --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/model/vo/ApiWelfareVO.java @@ -0,0 +1,26 @@ +package com.ruoyi.playingmethod.model.vo; + +import com.ruoyi.domain.other.TtBox; +import lombok.Data; + +@Data +public class ApiWelfareVO { + + /** 福利ID */ + private Integer welfareId; + + /** 福利名称 */ + private String welfareName; + + /** 福利类型 */ + private String type; + + /** 是否具备领取条件(0否 1是) */ + private String eligible; + + /** 领取状态(0未领取 1已领取) */ + private String claimStatus; + + /** 宝箱信息 */ + private TtBox ttBox; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/FightRankingRewardTask.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/FightRankingRewardTask.java new file mode 100644 index 0000000..fca7d98 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/FightRankingRewardTask.java @@ -0,0 +1,79 @@ +package com.ruoyi.playingmethod.scheduled; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtFightRankingRewardService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtFightRankingReward; +import com.ruoyi.playingmethod.model.vo.ApiFightRankingVO; +import com.ruoyi.playingmethod.service.ApiFightService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +@Slf4j +@Component("FightRankingRewardTask") +public class FightRankingRewardTask { + + @Autowired + private ApiFightService apiFightService; + + @Autowired + private TtFightRankingRewardService ttFightRankingRewardService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + /** 发放对战排行榜奖励 */ + public void distributeFightRankingReward() { + log.info("开始发放对战排行榜奖励"); + // 查询昨日排行榜 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -1); + Date yesterday = cal.getTime(); + String formattedYesterdayDate = formatter.format(yesterday); + List yesterdayFightRanking = apiFightService.getFightRankingByDate(formattedYesterdayDate); + for (int i = 0; i < yesterdayFightRanking.size(); i++) { + // 查询排行榜对应奖励 + TtFightRankingReward ttFightRankingReward = ttFightRankingRewardService.selectTtFightRankingRewardById(i + 1); + Long userId = yesterdayFightRanking.get(i).getUserId(); + BigDecimal reward = ttFightRankingReward.getReward(); + // 加钱 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, userId) + .setSql("account_amount = account_amount + " + reward); + userService.update(userUpdate); + // 综合消费日志 + TtUser ttUser = userService.getById(userId); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId.intValue()) + .amount(reward.compareTo(BigDecimal.ZERO) > 0 ? reward : null) + // ttUser 已在加钱 SQL 之后重新查询,账户余额已包含本次 reward,直接作为快照,切勿再 add(reward) + .finalAmount(reward.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount() : null) + .total(reward) // 收支合计 + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.FIGHT_RANKING_REWARD.getCode()) + .remark(TtAccountRecordSource.FIGHT_RANKING_REWARD.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttUserBlendErcashMapper.insert(blendErcash); + } + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/FightRobotTask.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/FightRobotTask.java new file mode 100644 index 0000000..358002d --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/FightRobotTask.java @@ -0,0 +1,460 @@ +package com.ruoyi.playingmethod.scheduled; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtFightMapper; +import com.ruoyi.admin.mapper.TtRobotFightJoinConfigMapper; +import com.ruoyi.admin.mapper.TtRobotFightPriceConfigMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.DateTimeUtils; +import com.ruoyi.common.utils.RandomUtil; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.fight.FightSeat; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateFightBody; +import com.ruoyi.domain.other.FightBoutData; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtRobotFightGroupBox; +import com.ruoyi.domain.other.TtRobotFightJoinConfig; +import com.ruoyi.domain.other.TtRobotFightPriceConfig; +import com.ruoyi.domain.vo.ApiFightListDataVO; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.playingmethod.domain.vo.ApiRobotFightGroupVO; +import com.ruoyi.playingmethod.mapper.ApiFightMapper; +import com.ruoyi.playingmethod.mapper.ApiRobotFightGroupMapper; +import com.ruoyi.playingmethod.service.ApiFightService; +import com.ruoyi.system.service.ISysConfigService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Component("FightRobotTask") +public class FightRobotTask { + + @Value("${mkcsgo.fight.roundTime}") + private final Integer fightRoundTimeMs = 4000; + + @Autowired + private RedisCache redisCache; + + @Autowired + private TtFightMapper ttFightMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private ISysConfigService sysConfigService; + + @Autowired + private ApiFightService apiFightService; + + @Autowired + private ApiRobotFightGroupMapper robotFightGroupMapper; + + @Autowired + private ApiFightMapper apiFightMapper; + + @Autowired + private TtBoxMapper boxMapper; + + @Autowired + private TtRobotFightJoinConfigMapper joinConfigMapper; + + @Autowired + private TtRobotFightPriceConfigMapper priceConfigMapper; + + /** + * 机器人加入对战 + */ + public void robotJoinFight() { + log.info("机器人加入对战"); + + // 查询所有处于等待中的对战房间ID(覆盖欧皇model=0与非酋model=1) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("status", 0); + queryWrapper.in("model", "0", "1"); + List fightList = ttFightMapper.selectList(queryWrapper); + if (CollectionUtils.isEmpty(fightList)) { + log.info("没有等待中的对战房间"); + return; + } + + // 查询所有机器人ID + List robotIdList = ttUserMapper.selectObjs( + new QueryWrapper() + .select("user_id") + .eq("user_type", UserType.ROBOT_USER.getCode()) + ); + if (robotIdList.isEmpty()) { + log.info("没有机器人"); + return; + } + + // 查询启用的机器人进房时间档位配置(按最小金额升序) + TtRobotFightJoinConfig queryConfig = new TtRobotFightJoinConfig(); + queryConfig.setStatus("0"); + List joinConfigs = joinConfigMapper.selectTtRobotFightJoinConfigList(queryConfig); + if (CollectionUtils.isEmpty(joinConfigs)) { + log.info("未配置机器人进房时间档位,跳过"); + return; + } + + // 打乱对战ID列表顺序,取前面指定数量的对战ID + Collections.shuffle(fightList); + + List userIdList = fightList.stream().map(TtFight::getUserId).filter(Objects::nonNull) + .collect(Collectors.toList()); + if (userIdList.isEmpty()) { + return; + } + Map userMap = ttUserMapper.selectByIds(userIdList).stream() + .collect(Collectors.toMap(TtUser::getUserId, v -> v)); + + for (TtFight ttFight : fightList) { + TtUser user = userMap.get(ttFight.getUserId()); + if (user == null) { + continue; + } + + // 补偿:满员且全员已准备但仍未开局的僵尸房间(开局请求丢失/异常导致),重新触发开局 + List seats = ttFight.getSeats(); + boolean hasEmptySeat = seats.stream().anyMatch(s -> Objects.isNull(s.getPlayerId())); + if (!hasEmptySeat) { + boolean allReady = seats.stream().allMatch(s -> s.getStatus() != null && s.getStatus().equals(2)); + if (allReady) { + FightBoutData fightBoutData = new FightBoutData(); + fightBoutData.setFightId(ttFight.getId()); + fightBoutData.setBoutNum(1); + fightBoutData.setExpirationTime(fightRoundTimeMs); + String key = "fight_bout_data:fight_" + fightBoutData.getFightId(); + redisCache.setCacheObject(key, fightBoutData, fightBoutData.getExpirationTime(), TimeUnit.MILLISECONDS); + R begin = apiFightService.fightBegin(ttFight.getId(), user); + log.warn("补偿触发满员未开局房间{}开局,结果:{}", ttFight.getId(), begin == null ? "" : begin.getMsg()); + } + continue; + } + + BigDecimal boxPriceTotal = ttFight.getBoxPriceTotal(); + if (boxPriceTotal == null) { + continue; + } + + // 命中金额档位:minAmount < 金额 <= maxAmount(金额超过最大档位则不加机器人) + Optional configOpt = joinConfigs.stream() + .filter(c -> c.getMinAmount() != null && c.getMaxAmount() != null + && boxPriceTotal.compareTo(c.getMinAmount()) > 0 + && boxPriceTotal.compareTo(c.getMaxAmount()) <= 0) + .findFirst(); + if (!configOpt.isPresent()) { + continue; + } + TtRobotFightJoinConfig config = configOpt.get(); + int randomSeconds = config.getRandomSeconds() == null ? 10 : config.getRandomSeconds(); + + long time = config.getJoinSeconds() + RandomUtil.rand(1, randomSeconds + 1); // 在基础秒数上加1~randomSeconds秒随机偏移,错峰加入 + if (ttFight.getCreateTime().getTime() + time * 1000 >= DateTimeUtils.now() * 1000) { + continue; + } + + List fightSeats = ttFight.getSeats(); + + var inRoomPlayerIds = fightSeats.stream() + .map(FightSeat::getPlayerId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + // 收集所有空位 + List emptySeats = fightSeats.stream() + .filter(s -> Objects.isNull(s.getPlayerId())) + .collect(Collectors.toList()); + if (emptySeats.isEmpty()) { + continue; + } + + // 如果房主是机器人且未准备(status=1),先让房主准备 + FightSeat ownerSeat = fightSeats.stream() + .filter(s -> Objects.nonNull(s.getPlayerId()) && s.getPlayerId().equals(ttFight.getUserId()) && s.getStatus().equals(1)) + .findFirst().orElse(null); + if (ownerSeat != null) { + TtUser owner = userMap.get(ttFight.getUserId()); + if (owner != null && UserType.ROBOT_USER.getCode().equals(owner.getUserType())) { + apiFightService.seatrReady(ttFight.getId(), owner); + log.info("机器人房主{}准备对战{}", owner.getUserId(), ttFight.getId()); + } + } + + List availableRobotIds = new ArrayList<>(robotIdList); + availableRobotIds.removeAll(inRoomPlayerIds); + if (availableRobotIds.size() < emptySeats.size()) { + log.warn("可用机器人不足以填满对战{}的空位, 需要{}个, 可用{}个", ttFight.getId(), emptySeats.size(), availableRobotIds.size()); + continue; + } + + // 依次填充空位,每次之间有时间差 + for (int i = 0; i < emptySeats.size(); i++) { + if (i > 0) { + // 第二个及之后的机器人加入延迟1~randomSeconds秒,保证间隔且不会同时涌入 + try { + Thread.sleep(RandomUtil.rand(1, randomSeconds + 1) * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + Integer robotId = availableRobotIds.get(RandomUtil.rand(availableRobotIds.size())); + availableRobotIds.remove(robotId); + TtUser player = ttUserMapper.selectById(robotId); + + R objectR = apiFightService.joinFight(ttFight.getId(), player); + if (!objectR.getCode().equals(200)) { + log.warn("机器人{}加入对战{}失败:{}", robotId, ttFight.getId(), objectR.getMsg()); + break; + } + log.info("机器人{}加入对战{}", robotId, ttFight.getId()); + + // 座位就绪后立即触发准备状态 + apiFightService.seatrReady(ttFight.getId(), player); + log.info("机器人{}准备对战{}", robotId, ttFight.getId()); + + // 最后一个空位填满后直接开始对战 + if (i == emptySeats.size() - 1) { + FightBoutData fightBoutData = new FightBoutData(); + fightBoutData.setFightId(ttFight.getId()); + fightBoutData.setBoutNum(1); + fightBoutData.setExpirationTime(fightRoundTimeMs); + String key = "fight_bout_data:fight_" + fightBoutData.getFightId(); + redisCache.setCacheObject(key, fightBoutData, fightBoutData.getExpirationTime(), TimeUnit.MILLISECONDS); + apiFightService.fightBegin(ttFight.getId(), player); + log.info("对战{}满员,机器人触发开始", ttFight.getId()); + } + } + } + } + + /** + * 机器人创建对战 + * 分别为欧皇(model=0)和非酋(model=1)独立维持房间数量 + */ + public void robotCreateFight() { + log.info("机器人创建对战"); + + // 查询所有机器人ID + List robotIdList = ttUserMapper.selectObjs( + new QueryWrapper() + .select("user_id") + .eq("user_type", UserType.ROBOT_USER.getCode()) + ); + if (robotIdList.isEmpty()) { + log.warn("无可用机器人,停止创建对战"); + return; + } + + // 查询机器人宝箱分组(全量,后续按model过滤) + List apiRobotFightGroupVOList = robotFightGroupMapper.selectApiRobotFightGroupBoxVOList(); + if (CollectionUtils.isEmpty(apiRobotFightGroupVOList)) { + return; + } + if (apiRobotFightGroupVOList.size() > 10000) { + return; + } + + // 预加载所有分组关联的宝箱信息 + Set boxIds = apiRobotFightGroupVOList.stream() + .flatMap(v -> v.getTtRobotFightGroupBoxList().stream()) + .map(TtRobotFightGroupBox::getBoxId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + List allBoxes = Collections.emptyList(); + if (!boxIds.isEmpty()) { + allBoxes = new LambdaQueryChainWrapper<>(boxMapper) + .in(TtBox::getBoxId, boxIds) + .eq(TtBox::getIsFight, "0") + .eq(TtBox::getStatus, "0") + .list(); + } + Map boxMap = allBoxes.stream() + .collect(Collectors.toMap(TtBox::getBoxId, v -> v, (v1, v2) -> v1)); + + // 计算每个分组的总价 + Map groupPriceMap = new HashMap<>(); + for (ApiRobotFightGroupVO groupVO : apiRobotFightGroupVOList) { + BigDecimal boxTotalPrice = BigDecimal.ZERO; + if (CollectionUtils.isNotEmpty(groupVO.getTtRobotFightGroupBoxList())) { + for (TtRobotFightGroupBox groupBox : groupVO.getTtRobotFightGroupBoxList()) { + TtBox box = boxMap.get(groupBox.getBoxId()); + if (box != null) { + BigDecimal multiply = BigDecimal.valueOf(groupBox.getBoxNum()).multiply(box.getPrice()); + boxTotalPrice = boxTotalPrice.add(multiply); + } + } + } + groupPriceMap.put(groupVO.getGroupId(), boxTotalPrice); + } + + // 分别为欧皇(model=0)和非酋(model=1)独立创建房间 + String[] models = {"0", "1"}; + for (String model : models) { + createFightByModel(model, robotIdList, apiRobotFightGroupVOList, groupPriceMap); + } + } + + /** + * 按模式独立创建对战房间 + * @param model 模式:0=欧皇,1=非酋 + * @param robotIdList 机器人ID列表 + * @param allGroups 所有分组配置 + * @param groupPriceMap 分组ID -> 总价映射 + */ + private void createFightByModel(String model, List robotIdList, + List allGroups, + Map groupPriceMap) { + String modelName = "0".equals(model) ? "欧皇" : "非酋"; + log.info("开始为{}模式创建对战房间", modelName); + + // 查询启用的价格档位配置 + TtRobotFightPriceConfig priceQuery = new TtRobotFightPriceConfig(); + priceQuery.setStatus("0"); + List priceConfigs = priceConfigMapper.selectTtRobotFightPriceConfigList(priceQuery); + if (CollectionUtils.isEmpty(priceConfigs)) { + log.warn("{}模式未配置价格档位,跳过创建对战房间", modelName); + return; + } + + // 查询该模式下等待中的房间存量 + List vos = apiFightMapper.getFightList(model, "0", null, null); + if (CollectionUtils.isEmpty(vos)) { + vos = new ArrayList<>(); + } + vos.forEach(v -> { + if (v.getBoxPriceTotal() == null) { + v.setBoxPriceTotal(BigDecimal.ZERO); + } + }); + + // 统计当前各价位对战的存量 + int configSize = priceConfigs.size(); + int[] currentCounts = new int[configSize]; + for (ApiFightListDataVO vo : vos) { + int index = matchPriceConfigIndex(vo.getBoxPriceTotal(), priceConfigs); + if (index >= 0) { + currentCounts[index]++; + } + } + + // 计算需要创建的数量 + int[] needCreateCounts = new int[configSize]; + StringBuilder currentLog = new StringBuilder(); + StringBuilder needLog = new StringBuilder(); + for (int i = 0; i < configSize; i++) { + needCreateCounts[i] = Math.max(priceConfigs.get(i).getTargetCount() - currentCounts[i], 0); + if (i > 0) { + currentLog.append(", "); + needLog.append(", "); + } + currentLog.append(priceConfigs.get(i).getPriceLimit().stripTrailingZeros().toPlainString()).append("档=").append(currentCounts[i]).append("个"); + needLog.append(priceConfigs.get(i).getPriceLimit().stripTrailingZeros().toPlainString()).append("档=").append(needCreateCounts[i]).append("个"); + } + log.info("{}模式存量: {}; 需创建: {}", modelName, currentLog, needLog); + + // 筛选该模式的分组 + List modelGroups = allGroups.stream() + .filter(g -> model.equals(g.getModel())) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(modelGroups)) { + log.info("{}模式无可用分组配置,跳过", modelName); + return; + } + + var groupMap = modelGroups.stream() + .collect(Collectors.toMap(ApiRobotFightGroupVO::getGroupId, v -> v)); + + // 循环处理不同价位的创建任务 + for (int i = 0; i < configSize; i++) { + int needCount = needCreateCounts[i]; + if (needCount <= 0) { + continue; + } + + BigDecimal limitPrice = priceConfigs.get(i).getPriceLimit(); + BigDecimal lowPrice = i > 0 ? priceConfigs.get(i - 1).getPriceLimit() : BigDecimal.ZERO; + + // 筛选出符合当前价位的分组 + List matchedGroups = groupPriceMap.entrySet().stream() + .filter(entry -> groupMap.containsKey(entry.getKey())) + .filter(entry -> entry.getValue().compareTo(lowPrice) > 0 && entry.getValue().compareTo(limitPrice) <= 0) + .map(entry -> groupMap.get(entry.getKey())) + .collect(Collectors.toList()); + + if (CollectionUtils.isNotEmpty(matchedGroups)) { + createFight(matchedGroups, needCount, robotIdList); + } + } + } + + /** + * 根据房间金额匹配价格档位索引 + * @param boxPriceTotal 房间宝箱总金额 + * @param priceConfigs 按 sort_order 升序排列的价格档位配置 + * @return 命中档位索引,未命中返回 -1 + */ + private int matchPriceConfigIndex(BigDecimal boxPriceTotal, List priceConfigs) { + if (boxPriceTotal == null || CollectionUtils.isEmpty(priceConfigs)) { + return -1; + } + for (int i = 0; i < priceConfigs.size(); i++) { + TtRobotFightPriceConfig config = priceConfigs.get(i); + if (config.getPriceLimit() != null && boxPriceTotal.compareTo(config.getPriceLimit()) <= 0) { + return i; + } + } + return -1; + } + + private void createFight(List groupList, int canCreateCount, List robotIdList) { + if (CollectionUtils.isEmpty(groupList) || CollectionUtils.isEmpty(robotIdList)) { + return; + } + + // 创建指定数量的房间 + for (int i = 0; i < canCreateCount; i++) { + // 每个房间创建间隔1-10秒随机延迟,错峰创建 + if (i > 0) { + try { + Thread.sleep(RandomUtil.rand(1, 11) * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + // 随机取一个机器人 + Integer robotId = robotIdList.get(RandomUtil.rand(robotIdList.size())); + TtUser player = ttUserMapper.selectById(robotId); + // 随机取一个宝箱分组 + ApiRobotFightGroupVO apiRobotFightGroupVO = groupList.get(RandomUtil.rand(groupList.size())); + log.info("机器人{}使用宝箱分组{}", robotId, apiRobotFightGroupVO.getGroupId()); + // 创建对战 + CreateFightBody createFightBody = new CreateFightBody(); + createFightBody.setModel(apiRobotFightGroupVO.getModel()); + createFightBody.setPlayerNumber(apiRobotFightGroupVO.getSeatNum()); + Map boxIdAndNumber = new HashMap<>(); + for (TtRobotFightGroupBox ttRobotFightGroupBox : apiRobotFightGroupVO.getTtRobotFightGroupBoxList()) { + boxIdAndNumber.put(ttRobotFightGroupBox.getBoxId(), ttRobotFightGroupBox.getBoxNum()); + } + createFightBody.setBoxIdAndNumber(boxIdAndNumber); + apiFightService.createFight(createFightBody, player); + } + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/RollTask.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/RollTask.java new file mode 100644 index 0000000..ce1c1f8 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/RollTask.java @@ -0,0 +1,130 @@ +package com.ruoyi.playingmethod.scheduled; + + +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtRollService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.entity.roll.TtRoll; +import com.ruoyi.playingmethod.service.ApiFightService; +import com.ruoyi.playingmethod.service.ApiRollService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON; + +@Slf4j +@Configuration // 1.主要用于标记配置类,兼备Component的效果。 +@EnableScheduling // 2.开启定时任务 +public class RollTask { + + @Value("${mkcsgo.fight.roundTime}") + private final Integer fightRoundTime = null; + + @Autowired + private ApiFightService fightService; + + @Autowired + private TtBoxRecordsMapper boxRecordsMapper; + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private TtRollService ttRollService; + + @Autowired + private Executor customThreadPoolExecutor; + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private ApiRollService apiRollService; + + // 定时检查即将开奖的roll房(临界区:1分钟) + @Scheduled(cron = "0 */1 * * * ?") + private void refreshDayTask() { + + // System.out.println("11111111111111111"); + + //System.out.println("roll 开奖临界区检查"); + + // 查询两分钟以内将要开奖的roll房 + Calendar c = Calendar.getInstance(); + c.add(Calendar.MINUTE, 2); + Timestamp criticalTime = new Timestamp(c.getTimeInMillis()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtRoll::getRollStatus, 0) + .eq(TtRoll::getDelFlag, 0) + .lt(TtRoll::getEndTime, criticalTime); + List criticalRoll = ttRollService.list(wrapper); + + if (criticalRoll.isEmpty()) return; + + log.info("roll开奖临界区检查,{}个roll房进入临界区", criticalRoll.size()); + + // 加入延时队列 + for (TtRoll roll : criticalRoll) { + + if (DateUtils.getNowDate().compareTo(roll.getEndTime()) > 0) { + + log.info("roll房{}已经超时,直接开奖。", roll.getId()); + + R r = apiRollService.endROLL(roll.getId()); + if (!r.getCode().equals(200)) { + log.error("roll开奖 消费失败,尝试消息补发再次消费!" + r.getMsg()); + } + if (ObjectUtil.isNotEmpty(r.getMsg())) log.info("{}", r.getMsg()); + // 已直接开奖,跳过后续延时队列入队,避免同一房间重复触发开奖 + continue; + } + + // 相差时间 + long betweenDay = DateUtil.between(DateUtils.getNowDate(), roll.getEndTime(), DateUnit.MS); + + System.out.println("设置消息超时时间"); + log.info("设置消息超时时间:{}",betweenDay); + MessageProperties messageProperties = new MessageProperties(); + messageProperties.setExpiration(String.valueOf(betweenDay)); // 消息的过期属性,单位 ms + Message message = new Message(String.valueOf(roll.getId()).getBytes(), messageProperties); + + // 延时队列实现开奖 + rabbitTemplate.convertAndSend( + DelayedQueueConfig.OPEN_ROLL_EXCHANGE, + DelayedQueueConfig.OPEN_ROLL_KEY, + message); + + log.info("Roll房{}-{}加入临界区,{}秒后开奖", roll.getId(), roll.getRollName(), betweenDay / 1000); + } + + } + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/WelfareBoxTask.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/WelfareBoxTask.java new file mode 100644 index 0000000..2ca2c09 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/WelfareBoxTask.java @@ -0,0 +1,60 @@ +package com.ruoyi.playingmethod.scheduled; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.playingmethod.service.ApiWelfareService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 每日福利箱子发放定时任务。 + *

+ * 注册到 RuoYi 系统监控-定时任务: + * - 任务名称:每日福利箱子发放 + * - 任务组名:WELFARE + * - 调用目标:welfareBoxTask.autoIssueDailyWelfareBox() + * - cron:0 0 0 * * ? (每天凌晨 0 点) + *

+ * 原 @Scheduled(cron = "0 0 0 * * ?") 迁移至此,由 sys_job 统一调度,请勿再加 @Scheduled。 + */ +@Slf4j +@Component("welfareBoxTask") +public class WelfareBoxTask { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private ApiWelfareService apiWelfareService; + + /** + * 每日0点自动为所有用户发放对应VIP等级的福利箱子 + */ + public void autoIssueDailyWelfareBox() { + log.info("[福利箱子] 开始自动发放每日福利箱子..."); + long start = System.currentTimeMillis(); + + // 1. 一次查出所有正常用户(排除机器人、已禁用) + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.ne(TtUser::getUserType, UserType.ROBOT_USER.getCode()) + .eq(TtUser::getStatus, "0"); + List users = ttUserService.list(query); + + if (users == null || users.isEmpty()) { + log.info("[福利箱子] 未查询到可发放用户,任务结束。"); + return; + } + + // 2. 批量发放(内部只有3次DB操作:查已领取、查福利配置、批量写入) + int[] result = apiWelfareService.batchAutoIssueDailyBoxes(users); + + long elapsed = System.currentTimeMillis() - start; + log.info("[福利箱子] 自动发放完成。共处理用户: {}, 成功: {}, 跳过: {}, 耗时: {}ms", + users.size(), result[0], result[1], elapsed); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/fightTask.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/fightTask.java new file mode 100644 index 0000000..f79410e --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/scheduled/fightTask.java @@ -0,0 +1,231 @@ +package com.ruoyi.playingmethod.scheduled; + + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.vo.fight.FightBoxVO; +import com.ruoyi.playingmethod.service.ApiFightService; +import com.ruoyi.framework.websocket.connector.PushService; +import com.ruoyi.framework.websocket.connector.SubscriptionManager; +import com.ruoyi.framework.websocket.connector.WsResult; +import com.ruoyi.playingmethod.websocket.constant.SMsgKey; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON; + +@Slf4j +@Configuration //1.主要用于标记配置类,兼备Component的效果。 +@EnableScheduling // 2.开启定时任务 +public class fightTask { + + @Value("${mkcsgo.fight.roundTime:6000}") + private int fightRoundTime; + + @Value("${mkcsgo.fight.startingDuration:3000}") + private int startingDuration; + + @Value("${mkcsgo.fight.revealDuration:4000}") + private int revealDuration; + + @Autowired + private ApiFightService fightService; + + @Autowired + private TtBoxRecordsMapper boxRecordsMapper; + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private PushService pushService; + + @Autowired + private TtBoxMapper ttBoxMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private Executor customThreadPoolExecutor; + + @Autowired + private com.ruoyi.common.core.redis.RedisCache redisCache; + + /** + * 定时清理超时未结束的对局(每分钟一次) + * 正常流程由 FightStateManager 管理对战生命周期; + * 此任务作为兜底,清理服务重启/推进线程丢失产生的僵尸对战(status=1 但无人推进), + * 统一标记为 status=3 并广播房间更新、清理 Redis 状态。 + */ + @Scheduled(cron = "0 * * * * ?") + public void refreshDayTask() { + log.info("定时更新超时未结束的对局"); + // Timestamp now = new Timestamp(System.currentTimeMillis()); + LambdaQueryWrapper fightQuery = new LambdaQueryWrapper<>(); + fightQuery + .eq(TtFight::getStatus,1) + .and(wrapper->{ + wrapper.isNull(TtFight::getEndTime); + }) + .and(wrapper->{ + wrapper.isNotNull(TtFight::getBeginTime); + }) + // TODO: 2024/4/13 优化sql + // 对局开始时间早于当前时间减去一定时间间隔(再减去6秒),就会被认为是超时的对局 + // begin_time < DATE_SUB(NOW(), INTERVAL 100 SECOND) + .last("AND begin_time < DATE_SUB(NOW(), INTERVAL " + fightRoundTime / 1000 + "* round_number + " + startingDuration / 1000 + " + " + revealDuration / 1000 + " + 15 second)"); + + List list = fightService.list(fightQuery); + + if (list.isEmpty()) return; + + List fightIds = new ArrayList<>(); + // 更新fight + list.stream().forEach(item->{ + item.setEndTime(new Timestamp(System.currentTimeMillis())); + item.setStatus(3); + item.setRemark("超时强制更新结束状态"); + fightIds.add(item.getId()); + + // 断开房间所有连接 + pushService.closeChannel(SubscriptionManager.fightChannel(item.getId().longValue())); + }); + fightService.updateBatchById(list); + + // 异步更新对局数据 + CompletableFuture.runAsync(() -> { + pushService.broadcastToChannel(SubscriptionManager.hallChannel(), WsResult.ok(SMsgKey.FIGHT_ROOM_UPDATE, buildRoomUpdateData(list), "对战房间最新信息")); + }, customThreadPoolExecutor); + + // 更新openBoxRecords(机器人持有的饰品不入背包,仅供前端展示) + new LambdaUpdateChainWrapper<>(boxRecordsMapper) + // .eq(TtBoxRecords::getStatus,IN_PACKSACK_ON.getCode()) + .in(TtBoxRecords::getFightId,fightIds) + .and(w -> w.isNull(TtBoxRecords::getHolderUserId) + .or().notInSql(TtBoxRecords::getHolderUserId, + "SELECT user_id FROM tt_user WHERE user_type = '" + UserType.ROBOT_USER.getCode() + "'")) + .set(TtBoxRecords::getStatus,IN_PACKSACK_ON.getCode()) + .update(); + + // 清理 Redis 状态 + fightIds.forEach(id -> redisCache.deleteObject("fight_state:" + id)); + + log.info("更新 {} 条超时未结束的对局记录",list.size()); + } + + private void enrichBoxPrices(List fights) { + List boxIds = new ArrayList<>(); + for (TtFight fight : fights) { + if (fight == null || fight.getBoxData() == null || fight.getBoxData().isEmpty()) continue; + for (String boxId : fight.getBoxData().keySet()) { + Object value = fight.getBoxData().get(boxId); + Integer id = extractBoxId(value); + if (id != null) boxIds.add(id); + } + } + if (boxIds.isEmpty()) return; + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxIds); + List boxes = ttBoxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + + for (TtFight fight : fights) { + if (fight == null || fight.getBoxData() == null) continue; + for (String boxId : fight.getBoxData().keySet()) { + Object value = fight.getBoxData().get(boxId); + FightBoxVO vo = resolveFightBoxVO(value); + if (vo != null) { + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), BigDecimal.ZERO)); + fight.getBoxData().put(boxId, vo); + } + } + } + } + + private Map buildRoomUpdateData(List fights) { + enrichBoxPrices(fights); + enrichOwnerNickNames(fights); + Map data = new HashMap<>(); + data.put("rows", fights); + return data; + } + + private void enrichOwnerNickNames(List fights) { + if (fights == null || fights.isEmpty()) return; + List ownerIds = fights.stream() + .map(TtFight::getUserId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (ownerIds.isEmpty()) return; + + List users = ttUserMapper.selectBatchIds(ownerIds); + Map nickMap = users.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(TtUser::getUserId, + u -> u.getNickName() != null ? u.getNickName() : "", (a, b) -> a)); + for (TtFight fight : fights) { + if (fight.getUserId() != null) { + // 用户存在时用实时昵称覆盖;用户被物理删除时保留入库的 ownerNickName + String liveNick = nickMap.get(fight.getUserId()); + if (liveNick != null) { + fight.setOwnerNickName(liveNick); + } + } + } + } + + private Integer extractBoxId(Object value) { + if (value == null) return null; + if (value instanceof FightBoxVO) return ((FightBoxVO) value).getBoxId(); + if (value instanceof LinkedHashMap) { + Object idField = ((LinkedHashMap) value).get("boxId"); + return idField != null ? Integer.valueOf(idField.toString()) : null; + } + return null; + } + + private FightBoxVO resolveFightBoxVO(Object value) { + if (value == null) return null; + if (value instanceof FightBoxVO) return (FightBoxVO) value; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + return new FightBoxVO( + map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + } + return null; + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBattleRoyaleService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBattleRoyaleService.java new file mode 100644 index 0000000..5c10a00 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBattleRoyaleService.java @@ -0,0 +1,4 @@ +package com.ruoyi.playingmethod.service; + +public interface ApiBattleRoyaleService { +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBindBoxService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBindBoxService.java new file mode 100644 index 0000000..a82d5ee --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBindBoxService.java @@ -0,0 +1,38 @@ +package com.ruoyi.playingmethod.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBoxA; +import com.ruoyi.domain.other.TtOrnamentsA; +import com.ruoyi.domain.other.TtBoxVO; +import com.ruoyi.playingmethod.domain.vo.CompValueVO; + +import java.util.List; + +public interface ApiBindBoxService { + + TtBoxA getBoxData(Integer boxId); + + /** 对战宝箱详情,概率使用 robot_odds */ + TtBoxA getFightBoxData(Integer boxId); + + List getCompBoxData(Integer boxId); + + List getBoxList(Integer boxTypeId, String homeFlag, String boxType, String isFight); + + List groupByBoxType(List boxData); + + List openBox(TtBox ttBox, Integer num, TtUser ttUser); + + List addBoxRecord(TtUser ttUser, TtBox ttBox, Integer num); + + List openBoxArithmetic(Integer fightId,TtUser ttUser, TtBox ttBox, Integer num); + + // List getBindBoxHistory(ApiBindBoxController.boxHistoryParam param); + + R blindBox(TtUser ttUser, TtBox ttBox, Integer num); + + CompValueVO getCompValue(TtUser ttUser, TtBox ttBox); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBoxOrnamentsService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBoxOrnamentsService.java new file mode 100644 index 0000000..4416e9f --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBoxOrnamentsService.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtBoxOrnaments; + +public interface ApiBoxOrnamentsService extends IService { + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBoxRecordsService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBoxRecordsService.java new file mode 100644 index 0000000..dc961e9 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiBoxRecordsService.java @@ -0,0 +1,18 @@ +package com.ruoyi.playingmethod.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.dto.boxRecords.TenTopQuery; +import com.ruoyi.domain.dto.boxRecords.queryCondition; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; + +import java.util.List; + +public interface ApiBoxRecordsService extends IService { + + List byCondition(queryCondition condition); + + List tenTopQuery(TenTopQuery query); + + List topHundredQuery(List status, List source, boolean todayOnly); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiCompoundService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiCompoundService.java new file mode 100644 index 0000000..754eee8 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiCompoundService.java @@ -0,0 +1,14 @@ +package com.ruoyi.playingmethod.service; + +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.vo.UserPackSackDataVO; + +import java.util.List; + +public interface ApiCompoundService { + + R compound(List packSackIds, TtUser ttUser); + + List getUserCompoundRecord(Integer userId); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiExponentService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiExponentService.java new file mode 100644 index 0000000..0e7dcf7 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiExponentService.java @@ -0,0 +1,38 @@ +package com.ruoyi.playingmethod.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.exponent.ExponentJoinParam; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; + +public interface ApiExponentService { + /** + * 加入指数 + * @param param + * @return + */ + R join(TtUser ttUser, ExponentJoinParam param); + + /** + * 宝箱信息 + */ + R box(TtUser ttUser, Integer id); + + /** + * 最近十期指数 + */ + R recenttenperiod(); + + /** + * 返回信息 + * @param ttUser + * @return + */ + R info(TtUser ttUser); + + R records(TtUser ttUser); + + R mybox(TtUser ttUser); + + R openbox(TtUser ttUser, TtBox ttBox, Integer num); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiFightService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiFightService.java new file mode 100644 index 0000000..5538430 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiFightService.java @@ -0,0 +1,80 @@ +package com.ruoyi.playingmethod.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.fight.FightDetailParam; +import com.ruoyi.domain.dto.fight.FightOnMyOwnParam; +import com.ruoyi.domain.other.CreateFightBody; +import com.ruoyi.domain.other.TtBoxVO; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.ApiFightListDataVO; +import com.ruoyi.domain.vo.FightResultDataVO; +import com.ruoyi.domain.vo.fight.FightResultVO; +import com.ruoyi.domain.vo.fight.TtFightVO; +import com.ruoyi.playingmethod.model.vo.ApiFightRankingVO; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +public interface ApiFightService extends IService { + + R createFight(CreateFightBody createFightParamVO, TtUser ttUser); + + /** + * 复刻房间:根据已有房间配置创建一个新房间,当前用户成为房主,其余座位为空 + */ + R copyFight(Integer sourceFightId, TtUser player); + + R joinFight(Integer fightId, TtUser player); + + /** 加入房间并指定座位(seatCode=null 时自动坐第一个空位) */ + R joinFight(Integer fightId, TtUser player, Integer seatCode); + + List getFightBoxList(Integer boxTypeId); + + List getFightList(String model, String status, Integer userId, Integer fightId); + + FightResultDataVO getFightRecord(Integer fightId, Integer round, Integer rounds); + + R fightBegin(Integer fightId, TtUser player); + + R fightEnd(Integer fightId, Long userId); + + R seatrReady(Integer fightId, TtUser player); + + R fightRoomExit(Integer fightId, TtUser player); + + List getFightList(FightOnMyOwnParam param); + + R fightDetail(FightDetailParam param); + + R earlierHistory(FightDetailParam param); + + // AjaxResult joinFightRoom(Integer userId,Integer fightId); + + List getFightRankingByDate(String date); + + Map> tenTopFight(Integer maxType, String model, Integer minPrice, Integer maxPrice, String keyword); + + /** + * 批量填充房主昵称(供 controller 中直接操作 TtFight 列表的场景使用) + */ + void enrichOwnerNickNames(List fights); + + /** + * 快速加入对战房间 + * 根据用户金额在本阵营等待中房间内,找到总价小于等于用户余额的最高价房间(不落座) + * 规则说明: + * 1. 只查询等待中房间(status = '0')且 model 与当前大厅阵营一致(不跨阵营匹配) + * 2. 找到所有房间价格 <= 用户余额的房间 + * 3. 在这些房间中选择价格最高的一个返回 + * 示例:用户有 10 金币,大厅有 1 元/2 元/9 元三个房间 → 选择 9 元的房间 + * + * @param userId 用户 ID + * @param model 房间模式(大厅阵营):0-欧皇 1-非酋 + * @return 房间 id(Long),本阵营无匹配房间时 data 为 null + */ + R quickJoinFightRoom(Integer userId, String model); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiFightUserService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiFightUserService.java new file mode 100644 index 0000000..65f8545 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiFightUserService.java @@ -0,0 +1,7 @@ +package com.ruoyi.playingmethod.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.other.TtFightUser; + +public interface ApiFightUserService extends IService { +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiLuckyUpgradeService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiLuckyUpgradeService.java new file mode 100644 index 0000000..ac69793 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiLuckyUpgradeService.java @@ -0,0 +1,18 @@ +package com.ruoyi.playingmethod.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.other.ApiLuckyUpgradeBody; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.UpgradeBodyA; +import com.ruoyi.domain.vo.ApiLuckyOrnamentsDataVO; + +import java.util.List; + +public interface ApiLuckyUpgradeService { + + List getOrnamentsList(ApiLuckyUpgradeBody apiLuckyUpgradeBody); + + R upgrade(TtUser ttUser, UpgradeBodyA upgradeBodyA); + + // List getUpgradeRecord(getUpgradeRecord param); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiRedPackService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiRedPackService.java new file mode 100644 index 0000000..40cb67a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiRedPackService.java @@ -0,0 +1,22 @@ +package com.ruoyi.playingmethod.service; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.domain.other.TtRedPack; +import com.ruoyi.domain.other.TtRedPacketRecord; +import com.ruoyi.thirdparty.wechat.domain.PtLoginUser; + +import java.util.List; + +public interface ApiRedPackService { + + List getRedPackList(PtLoginUser user, Integer type); + + AjaxResult getRedPack(Long userId, Long redPackId); + + List getActivityDataHis(PtLoginUser user); + + AjaxResult getRedPackDataDetail(PtLoginUser user, Long recordId); + + AjaxResult getRedPackByKey(LoginUser user, String redPackKey); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiRollService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiRollService.java new file mode 100644 index 0000000..247b7a5 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiRollService.java @@ -0,0 +1,37 @@ +package com.ruoyi.playingmethod.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.roll.GetRollOpenPrizeParam; +import com.ruoyi.domain.dto.roll.GetRollPlayersParam; +import com.ruoyi.domain.dto.roll.GetRollPrizePool; +import com.ruoyi.domain.entity.roll.TtRoll; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.RollDetailsDataVO; +import com.ruoyi.domain.vo.RollListDataVO; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsVO; +import com.ruoyi.domain.vo.roll.RollUserVO; +import com.ruoyi.playingmethod.contract.api.request.GetRollListParamRequest; +import com.ruoyi.playingmethod.controller.ApiRollController.JoinRollParam; + +import java.util.List; + +public interface ApiRollService extends IService { + + R joinRoll(JoinRollParam param, TtUser player); + + R endROLL(Integer rollId); + + // List getRollList(GetRollListParam param); + + List getRollList(GetRollListParamRequest param); + + R getRollDetails(Integer rollId, Long userId); + + R> getRollPlayers(GetRollPlayersParam param); + + R> getRollPrizePool(GetRollPrizePool param); + + R> getRollOpenPrize(GetRollOpenPrizeParam param); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiUpgradeRecordService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiUpgradeRecordService.java new file mode 100644 index 0000000..f701946 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiUpgradeRecordService.java @@ -0,0 +1,40 @@ +package com.ruoyi.playingmethod.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.upgrade.MyUpgradeRecordRequest; +import com.ruoyi.domain.dto.upgrade.UpgradeCondition; +import com.ruoyi.domain.dto.upgrade.UpgradeRecordPageRequest; +import com.ruoyi.domain.other.*; +import com.ruoyi.domain.vo.ApiLuckyUpgradeRecentPullVO; + + +import java.util.List; + +public interface ApiUpgradeRecordService extends IService { + + R historyDetail(UpgradeCondition param); + + R tenTopDetail(Boolean isVictory); + + /** + * 当前登录用户的幸运升级历史记录(高性能版) + *

仅按 user_id 单字段精准过滤 + open_time 倒序,依赖 (user_id, open_time) 复合索引, + * 同一用户记录避免按行回查用户表,最多查询一次用户信息。

+ * + * @param userId 当前登录用户ID(从Token获取,不信任前端入参) + * @param request 分页与可选过滤条件 + */ + R myHistoryRecord(Integer userId, MyUpgradeRecordRequest request); + + /** 最近100条拉取记录(按 upgradeId 查,所有用户) */ + List getRecentPulls(Integer upgradeId); + + /** + * 幸运升级记录分页(全服/我的 × 当前饰品/全部饰品) + * + * @param userId 我的记录=当前登录用户ID;全服记录传 null + * @param req 查询与分页条件 + */ + R recordPage(Integer userId, UpgradeRecordPageRequest req); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiUserBlendErcashService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiUserBlendErcashService.java new file mode 100644 index 0000000..a21f7e0 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiUserBlendErcashService.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.entity.TtUserBlendErcash; + +public interface ApiUserBlendErcashService extends IService { + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiWelfareMonthlyRechargesService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiWelfareMonthlyRechargesService.java new file mode 100644 index 0000000..1627aa4 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiWelfareMonthlyRechargesService.java @@ -0,0 +1,30 @@ +package com.ruoyi.playingmethod.service; + +import com.ruoyi.domain.vo.OpenBoxVO; +import com.ruoyi.playingmethod.model.ApiWelfare; +import com.ruoyi.playingmethod.model.vo.ApiWelfareMonthlyRechargesVO; + +import java.util.List; + +public interface ApiWelfareMonthlyRechargesService { + + /** + * 获取福利列表 + */ + List getWelfareList(Long userId); + + /** + * 领取福利 + */ + OpenBoxVO claimWelfare(Integer welfareId, Long userId); + + /** + * 检查是否符合领取条件 + */ + boolean checkEligible(Integer welfareId, Long userId); + + /** + * 检查是否已领取 + */ + boolean checkClaimed(Integer welfareId, Long userId); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiWelfareService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiWelfareService.java new file mode 100644 index 0000000..8ff087c --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/ApiWelfareService.java @@ -0,0 +1,55 @@ +package com.ruoyi.playingmethod.service; + +import com.ruoyi.domain.entity.TtWelfareBoxInventory; +import com.ruoyi.domain.vo.OpenBoxVO; +import com.ruoyi.playingmethod.model.ApiWelfare; + +import java.util.List; + +public interface ApiWelfareService { + + /** + * 获取福利列表 + */ + List getWelfareList(Long userId); + + /** + * 领取福利 + */ + OpenBoxVO claimWelfare(Integer welfareId, Long userId); + + /** + * 检查是否不符合领取条件 + */ + boolean checkNotEligible(Integer welfareId, Long userId); + + /** + * 检查是否已领取 + */ + boolean checkClaimed(Integer welfareId, Long userId); + + /** + * 每日领取福利箱子(不开箱,放入库存) + */ + TtWelfareBoxInventory claimDailyBox(Long userId); + + /** + * 定时任务批量发放每日福利箱子(无循环DB操作) + * + * @param users 待发放的用户列表 + * @return [成功数, 跳过数] + */ + int[] batchAutoIssueDailyBoxes(List users); + + /** + * 查询用户未开启的福利箱子列表 + */ + List getMyWelfareBoxes(Long userId); + + /** + * 批量开箱(开箱结果默认放入库存) + * @param boxInventoryIds 福利箱子库存ID列表 + * @return 开箱结果列表 + */ + List openWelfareBoxes(List boxInventoryIds, Long userId); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/FightStateManager.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/FightStateManager.java new file mode 100644 index 0000000..d5b6c86 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/FightStateManager.java @@ -0,0 +1,379 @@ +package com.ruoyi.playingmethod.service; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtFightMapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.vo.fight.FightBoxVO; +import com.ruoyi.domain.vo.fight.FightBrief; +import com.ruoyi.domain.vo.fight.FightState; +import com.ruoyi.framework.websocket.connector.PushService; +import com.ruoyi.framework.websocket.connector.SubscriptionManager; +import com.ruoyi.framework.websocket.connector.WsResult; +import com.ruoyi.playingmethod.websocket.constant.SMsgKey; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON; + +import com.ruoyi.domain.common.constant.UserType; + +@Slf4j +@Component +public class FightStateManager { + + @Autowired + private RedisCache redisCache; + + @Autowired + private PushService pushService; + + @Autowired + private TtFightMapper fightMapper; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Value("${mkcsgo.fight.roundTime:6000}") + private int fightRoundTime; + + @Value("${mkcsgo.fight.startingDuration:3000}") + private int startingDuration; + + @Value("${mkcsgo.fight.revealDuration:4000}") + private int revealDuration; + + @Value("${mkcsgo.fight.settledDuration:1000}") + private int settledDuration; + + private static final String STATE_KEY_PREFIX = "fight_state:"; + private static final int STATE_TTL_MINUTES = 10; + + public static final String PHASE_WAITING = "WAITING"; + public static final String PHASE_STARTING = "STARTING"; + public static final String PHASE_ROUND_OPENING = "ROUND_OPENING"; + public static final String PHASE_REVEAL = "REVEAL"; + public static final String PHASE_SETTLED = "SETTLED"; + + private final Executor stateExecutor = Executors.newVirtualThreadPerTaskExecutor(); + + public void startFightProgress(TtFight fight, List allRecords, + List winnerIds, List fightBoxVOList) { + // 磨损度不持久化到 tt_box_records,统一从 tt_ornament 实时查询填充 + fillExteriorNames(allRecords); + + Map> roundMap = allRecords.stream() + .collect(Collectors.groupingBy(r -> + r.getFightRoundNumber() != null ? r.getFightRoundNumber() : 0)); + + FightState fullState = FightState.builder() + .phase(PHASE_STARTING) + .currentRound(0) + .totalRounds(fight.getRoundNumber()) + .fight(FightBrief.from(fight)) + .revealedRounds(new ArrayList<>()) + .winnerIds(winnerIds) + .fightBoxVOList(fightBoxVOList) + .build(); + saveState(fight.getId(), fullState); + + FightState slimStart = FightState.builder() + .phase(PHASE_STARTING) + .totalRounds(fight.getRoundNumber()) + .build(); + broadcastState(fight.getId(), slimStart, "对战开始"); + + stateExecutor.execute(() -> runFightProgression(fight, roundMap, winnerIds, allRecords)); + } + + /** + * 从 tt_ornament 查询磨损度并回填到对战开箱记录(tt_box_records 不持久化该字段)。 + */ + private void fillExteriorNames(List records) { + if (records == null || records.isEmpty()) { + return; + } + List ornamentIds = records.stream() + .map(TtBoxRecords::getOrnamentId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (ornamentIds.isEmpty()) { + return; + } + Map exteriorMap = new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .in(TtOrnament::getId, ornamentIds) + .list() + .stream() + // 印花胶囊/武器箱/安慰奖等无磨损度饰品 exterior_name 为 null,必须过滤: + // 否则 Collectors.toMap 底层 Map.merge 对 null 值抛 NPE,导致 startFightProgress + // 在广播 STARTING 前中断,对局 status 已置 1 但前端永远卡在准备中 (WAITING) + // 非标准磨损度(如"普通")统一替换为"崭新出厂" + .collect(Collectors.toMap( + TtOrnament::getId, + o -> { + String exterior = o.getExteriorName(); + // 标准 5 种磨损度才保留原值,其他统一为崭新出厂 + if (exterior == null) return "崭新出厂"; + boolean isStandard = "崭新出厂".equals(exterior) + || "略有磨损".equals(exterior) + || "久经沙场".equals(exterior) + || "破损不堪".equals(exterior) + || "战痕累累".equals(exterior); + return isStandard ? exterior : "崭新出厂"; + }, + (a, b) -> a + )); + for (TtBoxRecords r : records) { + if (r.getOrnamentId() != null) { + r.setExteriorName(exteriorMap.get(r.getOrnamentId())); + } + } + } + + private void runFightProgression(TtFight fight, + Map> roundMap, + List winnerIds, + List allRecords) { + Integer fightId = fight.getId(); + String channel = SubscriptionManager.fightChannel(fightId.longValue()); + int totalRounds = fight.getRoundNumber(); + + try { + Thread.sleep(startingDuration); + playRounds(fightId, roundMap, totalRounds); + playReveal(fightId, allRecords, winnerIds); + playSettled(fightId, channel); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("对战{}进度推进被中断", fightId); + } catch (Exception e) { + log.error("对战{}进度推进异常", fightId, e); + // 兜底:异常时强制结算,避免产生僵尸对战(settleFight 内部 CAS eq(status,1),幂等安全) + try { + settleFight(fightId); + } catch (Exception ex) { + log.error("对战{}异常兜底结算失败", fightId, ex); + } + try { + pushService.closeChannel(channel); + } catch (Exception ignored) {} + } finally { + stateExecutor.execute(() -> { + try { Thread.sleep(30000); } catch (InterruptedException ignored) {} + redisCache.deleteObject(STATE_KEY_PREFIX + fightId); + }); + } + } + + private void playRounds(Integer fightId, Map> roundMap, int totalRounds) throws InterruptedException { + for (int round = 1; round <= totalRounds; round++) { + final int currentRound = round; + List records = roundMap.get(round); + if (records == null) records = Collections.emptyList(); + final List currentRecords = records; + + updateState(fightId, state -> { + state.setPhase(PHASE_ROUND_OPENING); + state.setCurrentRound(currentRound); + state.getRevealedRounds().add( + FightState.RoundResult.builder() + .round(currentRound).records(currentRecords).build()); + }); + + List roundResults = currentRecords.stream() + .map(r -> FightState.RoundRecord.builder() + .recordId(r.getId()) + .userId(r.getUserId()) + .ornamentId(r.getOrnamentId()) + .boxId(r.getBoxId()) + .ornamentName(r.getOrnamentName()) + .ornamentsPrice(r.getOrnamentsPrice()) + .imageUrl(r.getImageUrl()) + .ornamentsLevelId(r.getOrnamentsLevelId()) + .ornamentLevelImg(r.getOrnamentLevelImg()) + .exteriorName(r.getExteriorName()) + .build()) + .collect(Collectors.toList()); + + FightState slimRound = FightState.builder() + .phase(PHASE_ROUND_OPENING) + .currentRound(currentRound) + .totalRounds(totalRounds) + .roundResults(roundResults) + .build(); + broadcastState(fightId, slimRound, "第" + currentRound + "回合"); + + // 最后一回合无需等待,直接进入 REVEAL(前端自行控制最后一回合动画节奏) + if (round < totalRounds) { + Thread.sleep(fightRoundTime); + } + } + + List consolationRecords = roundMap.get(0); + if (consolationRecords != null && !consolationRecords.isEmpty()) { + updateState(fightId, state -> { + state.getRevealedRounds().add( + FightState.RoundResult.builder() + .round(0).records(consolationRecords).build()); + }); + } + } + + private void playReveal(Integer fightId, List allRecords, List winnerIds) throws InterruptedException { + FightState revealState = buildRevealState(allRecords, winnerIds); + updateState(fightId, state -> { + state.setPhase(PHASE_REVEAL); + state.setCurrentRound(null); + state.setWinnerIds(winnerIds); + state.setSettlement(revealState.getSettlement()); + }); + // DB 结算提前到最后一回合推送后立即执行:HTTP 拉结果即刻可用(观战/参与者均可提前拉取数据) + try { + settleFight(fightId); + } catch (Exception e) { + log.error("对战{}提前结算失败", fightId, e); + } + // 等待前端播放最后一回合开箱动画,再广播 REVEAL(避免动画未播完就触发前端弹窗) + Thread.sleep(fightRoundTime); + broadcastState(fightId, revealState, "对战结果揭示"); + } + + private void playSettled(Integer fightId, String channel) throws InterruptedException { + FightState slimSettled = FightState.builder().phase(PHASE_SETTLED).build(); + broadcastState(fightId, slimSettled, "对战已结算"); + Thread.sleep(settledDuration); + + pushService.closeChannel(channel); + log.info("对战{}状态同步完成,频道已关闭", fightId); + } + + private FightState buildRevealState(List allRecords, List winnerIds) { + List roundAssignments = new ArrayList<>(); + List prizeRecords = new ArrayList<>(); + + for (TtBoxRecords r : allRecords) { + if (r.getFightRoundNumber() != null) { + roundAssignments.add(FightState.RoundAssignment.builder() + .recordId(r.getId()) + .userId(r.getUserId()) + .ornamentId(r.getOrnamentId()) + .boxId(r.getBoxId()) + .ornamentsLevelId(r.getOrnamentsLevelId()) + .ornamentLevelImg(r.getOrnamentLevelImg()) + .exteriorName(r.getExteriorName()) + .holderUserId(r.getHolderUserId()) + .build()); + } else { + prizeRecords.add(FightState.PrizeRecord.builder() + .recordId(r.getId()) + .userId(r.getUserId()) + .ornamentId(r.getOrnamentId()) + .holderUserId(r.getHolderUserId()) + .ornamentName(r.getOrnamentName()) + .ornamentsPrice(r.getOrnamentsPrice()) + .imageUrl(r.getImageUrl()) + .ornamentsLevelId(r.getOrnamentsLevelId()) + .ornamentLevelImg(r.getOrnamentLevelImg()) + .exteriorName(r.getExteriorName()) + .build()); + } + } + + FightState.Settlement settlement = FightState.Settlement.builder() + .roundAssignments(roundAssignments) + .prizeRecords(prizeRecords) + .build(); + + return FightState.builder() + .phase(PHASE_REVEAL) + .winnerIds(winnerIds) + .settlement(settlement) + .build(); + } + + private void settleFight(Integer fightId) { + new LambdaUpdateChainWrapper<>(fightMapper) + .eq(TtFight::getId, fightId) + .eq(TtFight::getStatus, 1) + .set(TtFight::getStatus, 2) + .set(TtFight::getEndTime, new Timestamp(System.currentTimeMillis())) + .update(); + + LambdaUpdateWrapper update = new LambdaUpdateWrapper<>(); + update.eq(TtBoxRecords::getFightId, fightId) + // 机器人持有的饰品不入背包(仅供前端展示),保持"背包不显示"状态 + .and(w -> w.isNull(TtBoxRecords::getHolderUserId) + .or().notInSql(TtBoxRecords::getHolderUserId, + "SELECT user_id FROM tt_user WHERE user_type = '" + UserType.ROBOT_USER.getCode() + "'")) + .set(TtBoxRecords::getStatus, IN_PACKSACK_ON.getCode()); + boxRecordsService.update(update); + + TtFight fight = fightMapper.selectById(fightId); + Map data = new HashMap<>(); + data.put("rows", Arrays.asList(fight)); + log.info("对局{}结算, 推送HALL频道: key={}, data.rows.size={}, fight.status={}", + fightId, SMsgKey.FIGHT_ROOM_UPDATE, 1, fight.getStatus()); + pushService.broadcastToChannel( + SubscriptionManager.hallChannel(), + WsResult.ok(SMsgKey.FIGHT_ROOM_UPDATE, data, "对战结束")); + } + + public FightState getStateSnapshot(Integer fightId) { + return redisCache.getCacheObject(STATE_KEY_PREFIX + fightId); + } + + public FightState buildWaitingState(TtFight fight, List fightBoxVOList) { + return FightState.builder() + .phase(PHASE_WAITING) + .totalRounds(fight.getRoundNumber()) + .fight(FightBrief.from(fight)) + .fightBoxVOList(fightBoxVOList) + .build(); + } + + private void saveState(Integer fightId, FightState state) { + redisCache.setCacheObject(STATE_KEY_PREFIX + fightId, state, STATE_TTL_MINUTES, TimeUnit.MINUTES); + } + + private void updateState(Integer fightId, Consumer updater) { + FightState state = redisCache.getCacheObject(STATE_KEY_PREFIX + fightId); + if (state == null) { + log.warn("对战{}状态已过期", fightId); + return; + } + updater.accept(state); + saveState(fightId, state); + } + + private void broadcastState(Integer fightId, FightState state, String msg) { + pushService.broadcastToChannel( + SubscriptionManager.fightChannel(fightId.longValue()), + WsResult.ok(SMsgKey.FIGHT_STATE, state, msg)); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/IApiAttendanceRecordService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/IApiAttendanceRecordService.java new file mode 100644 index 0000000..c58f01c --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/IApiAttendanceRecordService.java @@ -0,0 +1,14 @@ +package com.ruoyi.playingmethod.service; + +import com.ruoyi.playingmethod.entity.TtAttendanceRecord; + +import java.util.List; + +public interface IApiAttendanceRecordService { + + TtAttendanceRecord selectByUid(int uid); + + int insert(int uid); + + List selectSevenAttendance(int uid); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/IApiReplacementRecordService.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/IApiReplacementRecordService.java new file mode 100644 index 0000000..e9711d4 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/IApiReplacementRecordService.java @@ -0,0 +1,11 @@ +package com.ruoyi.playingmethod.service; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; + +import java.util.List; + +public interface IApiReplacementRecordService { + + AjaxResult synthesizeItems(LoginUser user, List itemIds); +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiAttendanceRecordServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiAttendanceRecordServiceImpl.java new file mode 100644 index 0000000..44c39c2 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiAttendanceRecordServiceImpl.java @@ -0,0 +1,109 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import com.ruoyi.playingmethod.entity.TtAttendanceRecord; +import com.ruoyi.playingmethod.mapper.ApiAttendanceRecordMapper; +import com.ruoyi.playingmethod.service.IApiAttendanceRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.text.SimpleDateFormat; +import java.util.*; + +@Service +@Slf4j +public class ApiAttendanceRecordServiceImpl implements IApiAttendanceRecordService{ + private final ApiAttendanceRecordMapper apiTtAttendanceRecordMapper; + + public ApiAttendanceRecordServiceImpl(ApiAttendanceRecordMapper apiTtAttendanceRecordMapper) { + this.apiTtAttendanceRecordMapper = apiTtAttendanceRecordMapper; + } + + + @Override + public TtAttendanceRecord selectByUid(int uid) { + return apiTtAttendanceRecordMapper.selectByUid(uid); + } + + @Override + public int insert(int uid) { + TtAttendanceRecord ttAttendanceRecord = new TtAttendanceRecord(); + ttAttendanceRecord.setUserId(uid); + ttAttendanceRecord.setCreateTime(new Date()); + ttAttendanceRecord.setCoin(1); + return apiTtAttendanceRecordMapper.insertAttendanceRecord(ttAttendanceRecord); + + } + + @Override + public List selectSevenAttendance(int uid) { + List ttAttendanceRecords = new ArrayList<>(); + //周一 + DateTime dateTime = DateUtil.beginOfWeek(new Date()); + System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(dateTime)); + TtAttendanceRecord zhouYittAttendanceRecor = new TtAttendanceRecord(); + + if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(dateTime)) != null )zhouYittAttendanceRecor.setIsStatus("1"); + zhouYittAttendanceRecor.setCreateTime(dateTime); + ttAttendanceRecords.add(zhouYittAttendanceRecor); + //周二 + Calendar calendar = new GregorianCalendar(); + calendar.setTime(dateTime); + calendar.add(Calendar.DATE, 1); + System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())); + + TtAttendanceRecord zhouErttAttendanceRecord = new TtAttendanceRecord(); + if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null)zhouErttAttendanceRecord.setIsStatus("1"); + zhouErttAttendanceRecord.setCreateTime(calendar.getTime()); + ttAttendanceRecords.add(zhouErttAttendanceRecord); + + //周三 + calendar.add(Calendar.DATE, 1); + System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())); + + TtAttendanceRecord zhouSanttAttendanceRecord = new TtAttendanceRecord(); + if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouSanttAttendanceRecord.setIsStatus("1"); + zhouSanttAttendanceRecord.setCreateTime(calendar.getTime()); + ttAttendanceRecords.add(zhouSanttAttendanceRecord); + + //周四 + calendar.add(Calendar.DATE, 1); + System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())); + + TtAttendanceRecord zhouSittAttendanceRecord = new TtAttendanceRecord(); + if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouSittAttendanceRecord.setIsStatus("1"); + zhouSittAttendanceRecord.setCreateTime(calendar.getTime()); + ttAttendanceRecords.add(zhouSittAttendanceRecord); + + //周五 + calendar.add(Calendar.DATE, 1); + System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())); + + TtAttendanceRecord zhouWuttAttendanceRecord = new TtAttendanceRecord(); + if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouWuttAttendanceRecord.setIsStatus("1"); + zhouWuttAttendanceRecord.setCreateTime(calendar.getTime()); + ttAttendanceRecords.add(zhouWuttAttendanceRecord); + + //周六 + calendar.add(Calendar.DATE, 1); + System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())); + + TtAttendanceRecord zhouLiuttAttendanceRecord = new TtAttendanceRecord(); + if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouLiuttAttendanceRecord.setIsStatus("1"); + zhouLiuttAttendanceRecord.setCreateTime(calendar.getTime()); + ttAttendanceRecords.add(zhouLiuttAttendanceRecord); + + //周天 + calendar.add(Calendar.DATE, 1); + System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())); + + TtAttendanceRecord zhouRittAttendanceRecord = new TtAttendanceRecord(); + if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouRittAttendanceRecord.setIsStatus("1"); + zhouRittAttendanceRecord.setCreateTime(calendar.getTime()); + ttAttendanceRecords.add(zhouRittAttendanceRecord); + + return ttAttendanceRecords; + } + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBattleRoyaleServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBattleRoyaleServiceImpl.java new file mode 100644 index 0000000..502d8c9 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBattleRoyaleServiceImpl.java @@ -0,0 +1,8 @@ +package com.ruoyi.playingmethod.service.impl; + +import com.ruoyi.playingmethod.service.ApiBattleRoyaleService; +import org.springframework.stereotype.Service; + +@Service +public class ApiBattleRoyaleServiceImpl implements ApiBattleRoyaleService { +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBindBoxServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBindBoxServiceImpl.java new file mode 100644 index 0000000..8142d0a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBindBoxServiceImpl.java @@ -0,0 +1,508 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper; +import com.ruoyi.admin.mapper.TtBoxTypeMapper; +import com.ruoyi.admin.mapper.TtCompRecordMapper; +import com.ruoyi.admin.model.UpdateUserAccountBo; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.admin.service.TtOrnamentsLevelService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.*; +import com.ruoyi.domain.vo.OpenBoxVO; +import com.ruoyi.playingmethod.domain.vo.CompValueVO; +import com.ruoyi.playingmethod.mapper.ApiBindBoxMapper; +import com.ruoyi.playingmethod.mapper.ApiTtUserBlendErcashMapper; +import com.ruoyi.playingmethod.service.ApiBindBoxService; +import com.ruoyi.playingmethod.utils.customException.OrnamentNullException; +import com.ruoyi.framework.websocket.connector.PushService; +import com.ruoyi.framework.websocket.connector.SubscriptionManager; +import com.ruoyi.framework.websocket.connector.WsResult; +import com.ruoyi.playingmethod.websocket.constant.SMsgKey; +import com.ruoyi.message.service.MessageService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ObjectUtils; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON; +import static java.math.BigDecimal.ROUND_HALF_UP; + +@Service +@Slf4j +public class ApiBindBoxServiceImpl implements ApiBindBoxService { + + private final TtUserService userService; + private final TtBoxService boxService; + private final TtBoxTypeMapper boxTypeMapper; + private final ApiBindBoxMapper bindBoxMapper; + private final TtBoxOrnamentsMapper boxOrnamentsMapper; + private final TtBoxRecordsService boxRecordsService; + private final RedisCache redisCache; + private final Executor customThreadPoolExecutor; + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private LotteryMachine lotteryMachine; + + @Autowired + private TtCompRecordMapper ttCompRecordMapper; + + @Autowired + private ApiTtUserBlendErcashMapper apiTtUserBlendErcashMapper; + + @Autowired + private TtOrnamentsLevelService ornamentsLevelService; + + @Autowired + private PushService pushService; + + @Autowired + private MessageService messageService; + + public ApiBindBoxServiceImpl(TtUserService userService, + TtBoxService boxService, + TtBoxTypeMapper boxTypeMapper, + ApiBindBoxMapper bindBoxMapper, + TtBoxOrnamentsMapper boxOrnamentsMapper, + TtBoxRecordsService boxRecordsService, + RedisCache redisCache, + Executor customThreadPoolExecutor) { + this.userService = userService; + this.boxService = boxService; + this.boxTypeMapper = boxTypeMapper; + this.bindBoxMapper = bindBoxMapper; + this.boxOrnamentsMapper = boxOrnamentsMapper; + this.boxRecordsService = boxRecordsService; + this.redisCache = redisCache; + this.customThreadPoolExecutor = customThreadPoolExecutor; + } + + //@PostConstruct + public void postConstruct() { + List boxIds = boxOrnamentsMapper.selectBoxIdList(); + if (ObjectUtils.isEmpty(boxIds)) return; + for (Integer boxId : boxIds) { + boxService.isReplenishment(boxId); + } + } + + @Override + public TtBoxA getBoxData(Integer boxId) { + + TtBoxA boxData = bindBoxMapper.getBoxData(boxId); + if (ObjectUtils.isEmpty(boxData)) return null; + + List boxOrnamentsList = bindBoxMapper.getBoxOrnamentsList(boxId); + if (ObjectUtils.isEmpty(boxOrnamentsList)) return null; + + // 计算概率(仅展示爆率,不计算真实开奖概率) + BigDecimal sum = new BigDecimal(boxOrnamentsList.stream().mapToInt(TtOrnamentsA::getOdds).sum()); + BigDecimal oneHundred = new BigDecimal("100").setScale(2); + BigDecimal max = BigDecimal.ZERO; + Long maxId = null; + BigDecimal add = BigDecimal.ZERO; + for (TtOrnamentsA vo : boxOrnamentsList) { + Integer odds = vo.getOdds(); + BigDecimal oddsResult = new BigDecimal(odds).divide(sum, 4, ROUND_HALF_UP).multiply(oneHundred).setScale(2); + add = oddsResult.add(add); + if (max.compareTo(oddsResult) <= 0) { + max = oddsResult; + maxId = vo.getOrnamentId(); + } + vo.setOddsResult(oddsResult); + } + + // 平差 + BigDecimal erroValue = oneHundred.subtract(add); + for (TtOrnamentsA vo : boxOrnamentsList) { + if (vo.getOrnamentId().equals(maxId)) { + vo.setOddsResult(vo.getOddsResult().add(erroValue)); + break; + } + } + + if (ObjectUtils.isEmpty(boxOrnamentsList)) return null; + boxOrnamentsList = boxOrnamentsList.stream().peek(boxOrnamentsDataVO -> boxOrnamentsDataVO.setOdds(null)).collect(Collectors.toList()); + List probabilityDistribution = bindBoxMapper.getProbabilityDistribution(boxId); + boxOrnamentsList.sort((o1, o2) -> o2.getUsePrice().compareTo(o1.getUsePrice())); + boxData.setBoxOrnamentsList(boxOrnamentsList); + boxData.setProbabilityDistribution(probabilityDistribution); + return boxData; + } + + @Override + public List getCompBoxData(Integer boxId) { + List boxOrnamentsList = bindBoxMapper.getBoxOrnamentsList(boxId); + if (ObjectUtils.isEmpty(boxOrnamentsList)) + return null; + // 剪除非补偿物品 + boxOrnamentsList.removeIf(ttOrnamentsA -> ttOrnamentsA.getCompOdds() == 0); + // 计算概率 + BigDecimal sum = new BigDecimal(boxOrnamentsList.stream().mapToInt(TtOrnamentsA::getCompOdds).sum()); + BigDecimal oneHundred = new BigDecimal("100").setScale(2); + BigDecimal max = BigDecimal.ZERO; + Long maxId = null; + BigDecimal add = BigDecimal.ZERO; + for (TtOrnamentsA ttOrnamentsA : boxOrnamentsList) { + Integer compOdds = ttOrnamentsA.getCompOdds(); + BigDecimal compOddsResult = new BigDecimal(compOdds).divide(sum, 4, ROUND_HALF_UP).multiply(oneHundred).setScale(2); + add = compOddsResult.add(add); + if (max.compareTo(compOddsResult) <= 0) { + max = compOddsResult; + maxId = ttOrnamentsA.getOrnamentId(); + } + ttOrnamentsA.setCompOddsResult(compOddsResult); + } + // 平差 + BigDecimal errorValue = oneHundred.subtract(add); + for (TtOrnamentsA vo : boxOrnamentsList) { + if (vo.getOrnamentId().equals(maxId)) { + vo.setOddsResult(vo.getCompOddsResult().add(errorValue)); + break; + } + } + return boxOrnamentsList; + } + + @Override + public TtBoxA getFightBoxData(Integer boxId) { + TtBoxA boxData = bindBoxMapper.getBoxData(boxId); + if (ObjectUtils.isEmpty(boxData)) return null; + + // 对战宝箱用 robot_odds 作为展示概率 + List boxOrnamentsList = bindBoxMapper.getFightBoxOrnamentsList(boxId); + if (ObjectUtils.isEmpty(boxOrnamentsList)) return null; + + // 过滤掉 robot_odds = 0 的饰品(对战奖池中实际不存在的饰品) + boxOrnamentsList.removeIf(v -> v.getOdds() == null || v.getOdds() == 0); + if (ObjectUtils.isEmpty(boxOrnamentsList)) return null; + + BigDecimal sum = new BigDecimal(boxOrnamentsList.stream().mapToInt(TtOrnamentsA::getOdds).sum()); + BigDecimal oneHundred = new BigDecimal("100").setScale(2); + BigDecimal max = BigDecimal.ZERO; + Long maxId = null; + BigDecimal add = BigDecimal.ZERO; + for (TtOrnamentsA vo : boxOrnamentsList) { + Integer odds = vo.getOdds(); + BigDecimal oddsResult = new BigDecimal(odds).divide(sum, 4, ROUND_HALF_UP).multiply(oneHundred).setScale(2); + add = oddsResult.add(add); + if (max.compareTo(oddsResult) <= 0) { + max = oddsResult; + maxId = vo.getOrnamentId(); + } + vo.setOddsResult(oddsResult); + } + // 平差 + BigDecimal erroValue = oneHundred.subtract(add); + for (TtOrnamentsA vo : boxOrnamentsList) { + if (vo.getOrnamentId().equals(maxId)) { + vo.setOddsResult(vo.getOddsResult().add(erroValue)); + break; + } + } + + boxOrnamentsList = boxOrnamentsList.stream().peek(v -> v.setOdds(null)).collect(Collectors.toList()); + List probabilityDistribution = bindBoxMapper.getProbabilityDistribution(boxId); + boxOrnamentsList.sort((o1, o2) -> o2.getUsePrice().compareTo(o1.getUsePrice())); + boxData.setBoxOrnamentsList(boxOrnamentsList); + boxData.setProbabilityDistribution(probabilityDistribution); + return boxData; + } + + @Override + public List getBoxList(Integer boxTypeId, String homeFlag, String boxType, String isFight) { + + List boxList = bindBoxMapper.getBoxList(boxTypeId, homeFlag, boxType, isFight); + if (CollectionUtils.isEmpty(boxList)) { + return new ArrayList<>(); + } + + // 性能优化:一次 SQL 批量加载所有 boxId 的 box_ornaments,避免 N+1 + List boxIds = boxList.stream() + .map(TtBoxA::getBoxId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + Map> boxOrnamentsMap = new HashMap<>(); + if (!boxIds.isEmpty()) { + List all = new LambdaQueryChainWrapper<>(boxOrnamentsMapper) + .in(TtBoxOrnaments::getBoxId, boxIds) + .list(); + boxOrnamentsMap = all.stream() + .filter(o -> o != null && o.getBoxId() != null) + .collect(Collectors.groupingBy(TtBoxOrnaments::getBoxId)); + } + + // 内存过滤空箱子(保持原语义:存在且至少一个 realOdds > 0) + Map> finalBoxOrnamentsMap = boxOrnamentsMap; + List collect = boxList.stream().filter(item -> { + List list = finalBoxOrnamentsMap.get(item.getBoxId()); + if (list == null || list.isEmpty()) return false; + for (TtBoxOrnaments ornaments : list) { + if (ornaments.getRealOdds() != null && ornaments.getRealOdds() > 0) { + return true; + } + } + return false; + }).collect(Collectors.toList()); + + return groupByBoxType(collect); + } + + @Override + public List groupByBoxType(List boxData) { + Map> collect = boxData.stream().collect(Collectors.groupingBy(TtBoxA::getBoxTypeId)); + if (collect.isEmpty()) { + return new ArrayList<>(); + } + + // 性能优化:一次 SQL 批量加载所有 boxType,避免 N+1 + List typeIds = new ArrayList<>(collect.keySet()); + Map typeMap = boxTypeMapper.selectBatchIds(typeIds).stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(TtBoxType::getId, t -> t, (a, b) -> a)); + + List resultList = new ArrayList<>(); + for (Map.Entry> entry : collect.entrySet()) { + TtBoxType ttBoxType = typeMap.get(entry.getKey()); + if (StringUtils.isNull(ttBoxType)) continue; + // 过滤掉前端隐藏的分类 + if ("1".equals(ttBoxType.getIsShow())) continue; + TtBoxVO boxTypeA = TtBoxVO.builder().build(); + boxTypeA.setBoxTypeId(ttBoxType.getId()); + boxTypeA.setBoxTypeName(ttBoxType.getName()); + boxTypeA.setIcon(ttBoxType.getIcon()); + boxTypeA.setSort(ttBoxType.getSort()); + boxTypeA.setBoxList(entry.getValue()); + resultList.add(boxTypeA); + } + resultList.sort(Comparator.comparingInt(a -> a.getSort() == null ? Integer.MAX_VALUE : a.getSort())); + return resultList; + } + + @Override + public List openBox(TtBox ttBox, Integer num, TtUser ttUser) { + return null; + } + + @Override + public List addBoxRecord(TtUser ttUser, TtBox ttBox, Integer num) { + return null; + } + + @Override + public List openBoxArithmetic(Integer fightId, TtUser ttUser, TtBox ttBox, Integer num) { + return null; + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public R blindBox(TtUser user, TtBox box, Integer num) { + + // 同步扣款 + BigDecimal totalmoney = box.getPrice().multiply(new BigDecimal(num)); + R r = upgradeAccounting(totalmoney, user); + if (R.isError(r)) { + // 透传原始 code(如余额不足 702),避免被默认覆盖为 500 + return R.fail(r.getCode().intValue(), r.getMsg()); + } + + // 抽奖 + List boxRecords = new ArrayList<>(); + Map boxToOrnament = new HashMap<>(); + for (int i = 0; i < num; i++) { + + String ornamentId = lotteryMachine.singleLottery(user, box); + + if (StringUtils.isBlank(ornamentId)) { + throw new OrnamentNullException("单次抽奖无结果#"+box.getBoxId()+"#"+ornamentId); + //return R.fail("系统繁忙,请稍后重试。"); + } + + // 宝箱详细信息 + TtOrnamentsA ornamentsData = bindBoxMapper.ornamentsInfo(box.getBoxId(), ornamentId); + + if (ObjectUtil.isEmpty(ornamentsData)){ + throw new OrnamentNullException("没有符合条件的宝箱信息#"+box.getBoxId()+"#"+ornamentId); + } + + // 比对补偿箱类型开箱价格和奖品价值,记录补偿值。如果饰品大于等于门票,清空补偿值 + if ("3".equals(box.getBoxType()) && ornamentsData.getUsePrice().compareTo(box.getPrice()) < 0) { + compRecord(user.getUserId(), box.getBoxId()); + } else { + ttCompRecordMapper.deleteTtCompRecord(user.getUserId(), box.getBoxId()); + } + + // 抽奖成功,构建开箱记录数据 + TtBoxRecords boxRecord = TtBoxRecords.builder() + .userId(user.getUserId()) + + .boxId(box.getBoxId()) + .boxName(box.getBoxName()) + .boxPrice(box.getPrice()) + + .ornamentId(Long.valueOf(ornamentId)) + // .marketHashName(ornamentsData.) + .ornamentName(ObjectUtil.isNotEmpty(ornamentsData.getName()) ? ornamentsData.getName() : ObjectUtil.isNotEmpty(ornamentsData.getShortName()) ? ornamentsData.getShortName() : "无名称") + .imageUrl(ornamentsData.getImageUrl()) + .ornamentsPrice(ornamentsData.getUsePrice()) + .ornamentsLevelId(ornamentsData.getOrnamentsLevelId()) + .ornamentLevelImg(ornamentsData.getLevelImg()) + + .holderUserId(user.getUserId()) + + .source(TtboxRecordSource.BLIND_BOX.getCode()) + .status(IN_PACKSACK_ON.getCode()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + boxRecords.add(boxRecord); + boxToOrnament.put(Long.valueOf(ornamentId), ornamentsData); + } + + // 同步保存游戏记录 + boxRecordsService.saveBatch(boxRecords); + + // 发送邮件通知 + for (TtBoxRecords record : boxRecords) { + String priceStr = record.getOrnamentsPrice() == null ? "0" + : record.getOrnamentsPrice().stripTrailingZeros().toPlainString(); + String content = "恭喜您在常规补给中获得饰品【" + record.getOrnamentName() + "】(价值 ¥" + priceStr + ")。"; + messageService.sendNotice( + Long.valueOf(user.getUserId()), + MessageCategoryEnum.GAME_BOX.getTitle(), + content, + MessageCategoryEnum.GAME_BOX.getCode()); + } + + // 返回值 + List openBoxVOs = boxRecords.stream().map(item -> { + OpenBoxVO openBoxVO = new OpenBoxVO(); + BeanUtil.copyProperties(item, openBoxVO); + TtOrnamentsA ornamentsData = boxToOrnament.get(item.getOrnamentId()); + if (ornamentsData != null) { + openBoxVO.setShortName(ornamentsData.getShortName()); + openBoxVO.setExteriorName(ornamentsData.getExteriorName()); + } + openBoxVO.setUsePrice(openBoxVO.getOrnamentsPrice()); + openBoxVO.setLevelImg(item.getOrnamentLevelImg()); + return openBoxVO; + }).collect(Collectors.toList()); + + // 异步任务 广播开箱消息 + broadcastToBoxRoom(boxRecords); + + return R.ok(openBoxVOs); + } + + @Override + public CompValueVO getCompValue(TtUser ttUser, TtBox ttBox) { + CompValueVO compValueVO = new CompValueVO(); + // 查询宝箱设定的补偿值 + compValueVO.setDesiredCompValue(ttBox.getCompAmount()); + // 查询用户当前补偿值 + TtCompRecord ttCompRecord = ttCompRecordMapper.selectTtCompRecord(ttUser.getUserId(), ttBox.getBoxId()); + if (Objects.isNull(ttCompRecord)) + compValueVO.setCurrentCompValue(0); + else + compValueVO.setCurrentCompValue(ttCompRecord.getCompAmount()); + return compValueVO; + } + + // 扣款(经典开箱:差额规则——金币优先扣到与积分相等,超出部分金币与积分同步扣减) + public R upgradeAccounting(BigDecimal consumption, TtUser player) { + + // 再次检查余额:与本次开箱所需金额比较 + player = userService.getById(player.getUserId()); + if (player.getAccountAmount() == null || player.getAccountAmount().compareTo(consumption) < 0) { + return R.fail(702, "账户余额不足"); + } + return userService.updateUserAccount(player.getUserId(), consumption.negate(), TtAccountRecordSource.GAME_TYPE_01); + } + + // 异步任务 广播开箱消息 + public void broadcastToBoxRoom(List boxRecords) { + + CompletableFuture.runAsync(() -> { + + // 数据按boxId排序 + Collections.sort(boxRecords, new Comparator() { + @Override + public int compare(TtBoxRecords o1, TtBoxRecords o2) { + return o1.getBoxId().compareTo(o2.getBoxId()); + } + }); + + // 缓冲区 + List cache = new ArrayList<>(); + + Integer flagBoxId = -1; + for (TtBoxRecords record : boxRecords) { + + if (cache.isEmpty()) { + // 缓冲区为空,直接加入 + cache.add(record); + flagBoxId = record.getBoxId(); + continue; + } else { + // 不为空 + if (record.getBoxId().equals(flagBoxId)) { + cache.add(record); + } else { + Integer bid = cache.get(0).getBoxId(); + // 广播最新数据 + pushService.broadcastToChannel(SubscriptionManager.bindboxChannel(bid.longValue()), WsResult.ok(SMsgKey.Blind_Box_Current_Data, cache, "开盒子更新。")); + cache.clear(); + } + } + } + Integer bid = cache.get(0).getBoxId(); + // 广播最新数据 + pushService.broadcastToChannel(SubscriptionManager.bindboxChannel(bid.longValue()), WsResult.ok(SMsgKey.Blind_Box_Current_Data, cache, "开盒子更新。")); + + }, customThreadPoolExecutor); + } + + // 记录补偿值 + public void compRecord(Integer userId, Integer boxId) { + // 查询当前补偿值 + TtCompRecord ttCompRecord = ttCompRecordMapper.selectTtCompRecord(userId, boxId); + if (!Objects.isNull(ttCompRecord)) { + // 补偿值+1 + ttCompRecord.setCompAmount(ttCompRecord.getCompAmount() + 1); + ttCompRecordMapper.updateTtCompRecord(ttCompRecord); + } + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBoxOrnamentsServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBoxOrnamentsServiceImpl.java new file mode 100644 index 0000000..5157564 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBoxOrnamentsServiceImpl.java @@ -0,0 +1,11 @@ +package com.ruoyi.playingmethod.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.other.TtBoxOrnaments; +import com.ruoyi.playingmethod.mapper.ApiBoxOrnamentsMapper; +import com.ruoyi.playingmethod.service.ApiBoxOrnamentsService; +import org.springframework.stereotype.Service; + +@Service +public class ApiBoxOrnamentsServiceImpl extends ServiceImpl implements ApiBoxOrnamentsService { +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBoxRecordsServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBoxRecordsServiceImpl.java new file mode 100644 index 0000000..388d43e --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiBoxRecordsServiceImpl.java @@ -0,0 +1,65 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.domain.dto.boxRecords.TenTopQuery; +import com.ruoyi.domain.dto.boxRecords.queryCondition; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.playingmethod.service.ApiBoxRecordsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +@Slf4j +public class ApiBoxRecordsServiceImpl extends ServiceImpl implements ApiBoxRecordsService { + + @Override + public List byCondition(queryCondition condition) { + + condition.setLimit((condition.getPage() - 1) * condition.getSize()); + if (ObjectUtil.isNull(condition.getOrderByFie())) condition.setOrderByFie(1); + + List list = this.baseMapper.byCondition( + condition.getUserId(), + condition.getLimit(), + condition.getSize(), + condition.getOrderByFie(), + condition.getBoxRecordId(), + condition.getBoxId(), + condition.getUserType(), + condition.getSource(), + condition.getStatus(), + condition.getOrnamentPriceMin(), + condition.getOrnamentPriceMax(), + condition.getOrnamentLevelIds() + ); + + return list; + + } + + @Override + public List tenTopQuery(TenTopQuery query) { + query.setLimit((query.getPage() - 1) * query.getSize()); + + return this.baseMapper.tenTopQuery( + query.getLimit(), + query.getSize(), + query.getSource(), + query.getStatus() + ); + } + + /** + * 「今日百佳 / 历史百佳」不缓存,直接查库。 + * 索引已建好、查询为索引 top100,直接查库也很快、彻底无延迟,仅并发略有上升。 + */ + @Override + public List topHundredQuery(List status, List source, boolean todayOnly) { + return this.baseMapper.topHundredQuery(100, status, source, todayOnly); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiCompoundServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiCompoundServiceImpl.java new file mode 100644 index 0000000..1ff03a9 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiCompoundServiceImpl.java @@ -0,0 +1,121 @@ +package com.ruoyi.playingmethod.service.impl; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtOrnamentsLevelService; +import com.ruoyi.admin.service.WebsitePropertyService; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.domain.vo.WebsitePropertyDataVO; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.playingmethod.mapper.ApiCompoundMapper; +import com.ruoyi.playingmethod.service.ApiCompoundService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.service.ApiUserPackSackService; +import com.ruoyi.domain.vo.UserPackSackDataVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordSource.MALL_EXCHANGE; + +@Service +@Slf4j +public class ApiCompoundServiceImpl implements ApiCompoundService { + + private final ISysConfigService configService; + private final ApiUserPackSackService userPackSackService; + private final WebsitePropertyService websitePropertyService; + private final TtBoxRecordsService boxRecordsService; + private final TtOrnamentMapper ornamentsMapper; + private final TtOrnamentsLevelService ornamentsLevelService; + private final ApiCompoundMapper apiCompoundMapper; + + public ApiCompoundServiceImpl(ISysConfigService configService, + ApiUserPackSackService userPackSackService, + WebsitePropertyService websitePropertyService, + TtBoxRecordsService boxRecordsService, + TtOrnamentMapper ornamentsMapper, + TtOrnamentsLevelService ornamentsLevelService, + ApiCompoundMapper apiCompoundMapper) { + this.configService = configService; + this.userPackSackService = userPackSackService; + this.websitePropertyService = websitePropertyService; + this.boxRecordsService = boxRecordsService; + this.ornamentsMapper = ornamentsMapper; + this.ornamentsLevelService = ornamentsLevelService; + this.apiCompoundMapper = apiCompoundMapper; + } + + @Override + public R compound(List packSackIds, TtUser ttUser) { + List boxRecordsList = userPackSackService.packSackHandle(packSackIds, ttUser, 3); + if (ObjectUtils.isEmpty(boxRecordsList)) return R.fail("请选择饰品后再进行合成!"); + List ornamentsIds = boxRecordsList.stream().map(TtBoxRecords::getOrnamentId).collect(Collectors.toList()); + List websitePropertyIds = websitePropertyService.list().stream().map(WebsitePropertyDataVO::getId).collect(Collectors.toList()); + websitePropertyIds.retainAll(ornamentsIds); + if (!websitePropertyIds.isEmpty()) return R.fail("选择的饰品中存在网站专属道具,请重新选择!"); + String maxCompoundNumStr = configService.selectConfigByKey("maxCompoundNum"); + int maxCompoundNum = Integer.parseInt(maxCompoundNumStr); + if (boxRecordsList.size() > maxCompoundNum) return R.fail("最多选择" + maxCompoundNum + "个饰品!"); + BigDecimal compoundBeanTotal = boxRecordsList.stream().map(TtBoxRecords::getOrnamentsPrice).reduce(BigDecimal.ZERO, BigDecimal::add); + String compoundMinPriceStr = configService.selectConfigByKey("compoundMinPrice"); // 获取配置中的汰换最低价 + BigDecimal compoundMinPrice = new BigDecimal(compoundMinPriceStr); + if (compoundBeanTotal.compareTo(compoundMinPrice) < 0) + return R.fail("选择的饰品总价必须大于" + compoundMinPrice + "游戏币才可以汰换哦!"); + Optional maxPriceOptional = boxRecordsList.stream().map(TtBoxRecords::getOrnamentsPrice).max(Comparator.naturalOrder()); + if (!maxPriceOptional.isPresent()) return null; + try { + String compoundMinPremiumRateStr = configService.selectConfigByKey("compoundMinPremiumRate"); + BigDecimal minPremiumRatePrice = new BigDecimal(compoundMinPremiumRateStr).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP) + .multiply(maxPriceOptional.get()); + String compoundMaxPremiumRateStr = configService.selectConfigByKey("compoundMaxPremiumRate"); + BigDecimal maxPremiumRatePrice = new BigDecimal(compoundMaxPremiumRateStr).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP) + .multiply(compoundBeanTotal); + BigDecimal minPrice = maxPriceOptional.get().subtract(minPremiumRatePrice); + BigDecimal maxPrice = compoundBeanTotal.subtract(maxPremiumRatePrice); + List ornamentsList = new LambdaQueryChainWrapper<>(ornamentsMapper).in(TtOrnament::getType, 2, 3, 4, 5) + .between(TtOrnament::getUsePrice, minPrice, maxPrice).list(); + if (StringUtils.isNull(ornamentsList) || ornamentsList.isEmpty()) + return R.fail("汰换失败,饰品池中未筛选出符合您价格区间的饰品!"); + int randomIndex = RandomUtils.getRandomIndex(ornamentsList.size()); + TtOrnament ttOrnament = ornamentsList.get(randomIndex); + List levelIds = ornamentsLevelService.list().stream().map(TtOrnamentsLevel::getId).collect(Collectors.toList()); + Integer ornamentsLevelId = levelIds.get(RandomUtils.getRandomIndex(levelIds.size())); + boxRecordsService.updateBatchById(boxRecordsList, 1); + TtBoxRecords boxRecords = TtBoxRecords.builder().build(); + boxRecords.setUserId(ttUser.getUserId()); + boxRecords.setOrnamentId(ttOrnament.getId()); + boxRecords.setOrnamentsPrice(ttOrnament.getUsePrice()); + boxRecords.setOrnamentsLevelId(ornamentsLevelId); + boxRecords.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); + boxRecords.setCreateTime(new Date()); + boxRecords.setSource(MALL_EXCHANGE.getCode()); //// TODO: 2024/4/1 类型不对 + boxRecords.setHolderUserId(ttUser.getUserId()); + boxRecordsService.save(boxRecords); + UserPackSackDataVO compoundData = apiCompoundMapper.selectCompoundDataById(boxRecords.getId()); + return R.ok(compoundData); + } catch (Exception e) { + return R.fail("数据异常!"); + } + } + + @Override + public List getUserCompoundRecord(Integer userId) { + return apiCompoundMapper.selectCompoundRecordByUserId(userId); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiExponentServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiExponentServiceImpl.java new file mode 100644 index 0000000..8742622 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiExponentServiceImpl.java @@ -0,0 +1,555 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtExponentMapper; +import com.ruoyi.admin.mapper.TtExponentUserBoxMapper; +import com.ruoyi.admin.mapper.TtExponentUserMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.dto.exponent.*; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.exponent.TtExponent; +import com.ruoyi.domain.entity.exponent.TtExponentUser; +import com.ruoyi.domain.entity.exponent.TtExponentUserBox; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtBoxA; +import com.ruoyi.domain.other.TtOrnamentsA; +import com.ruoyi.domain.vo.OpenBoxVO; +import com.ruoyi.playingmethod.mapper.ApiBindBoxMapper; +import com.ruoyi.playingmethod.mapper.ApiTtUserBlendErcashMapper; +import com.ruoyi.playingmethod.service.ApiExponentService; +import com.ruoyi.playingmethod.utils.customException.OrnamentNullException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON; + +@Service +@Slf4j +public class ApiExponentServiceImpl implements ApiExponentService { + + @Autowired + private TtExponentMapper ttExponentMapper; + + @Autowired + private TtExponentUserMapper ttExponentUserMapper; + + @Autowired + private TtBoxService boxService; + + @Autowired + private TtUserService userService; + + @Autowired + private LotteryMachine lotteryMachine; + + @Autowired + private ApiBindBoxMapper bindBoxMapper; + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private ApiTtUserBlendErcashMapper apiTtUserBlendErcashMapper; + + @Autowired + private TtBoxMapper ttBoxMapper; + + @Autowired + private TtExponentUserBoxMapper ttExponentUserBoxMapper; + + @Override + public R join(TtUser ttUser, ExponentJoinParam param) { + if (param == null || param.getId() == null || param.getBoxId() == null || param.getForward() == null) { + return R.fail("参数不合法"); + } + TtBox ttBox = boxService.getById(param.getBoxId()); + if (ttBox == null) { + return R.fail("宝箱参数不合法"); + } + // 同步扣款 + R> mapR = upgradeAccounting(ttBox.getPrice(), ttUser); + if (!mapR.getCode().equals(200)) { + return mapR; + } + TtExponentUser exponentUser = new TtExponentUser(); + exponentUser.setExponentId(param.getId()); + exponentUser.setBoxId(param.getBoxId()); + exponentUser.setUserId(ttUser.getUserId()); + exponentUser.setPrice(ttBox.getPrice()); + exponentUser.setForward(param.getForward()); + exponentUser.setStatus(0); + exponentUser.setBoxRecordId(0L); + exponentUser.setCreateTime(new Date()); + exponentUser.setUpdateTime(new Date()); + ttExponentUserMapper.insert(exponentUser); + return R.ok(); + } + + // 扣款 + public R> upgradeAccounting(BigDecimal consumption, TtUser player) { + // 再次检查余额 + player = userService.getById(player.getUserId()); + if (player.getAccountAmount() == null || player.getAccountAmount().compareTo(consumption) < 0) { + return R.fail("余额不足"); + } + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate.eq(TtUser::getUserId, player.getUserId()); + Map map; + if (player.getAccountAmount().compareTo(consumption) >= 0) { + userUpdate.set(TtUser::getAccountAmount, player.getAccountAmount().subtract(consumption)); + map = MapUtil.builder("Amount", consumption).map(); + } else { + BigDecimal subtract = consumption.subtract(player.getAccountAmount()); + userUpdate + .set(TtUser::getAccountAmount, 0) + .set(TtUser::getAccountCredits, player.getAccountCredits().subtract(subtract)); + map = MapUtil.builder("Amount", player.getAccountAmount()).map(); + map.put("Credits", subtract); + } + userService.update(userUpdate); + + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(player.getUserId()) + .amount(null) + .finalAmount(null) + .credits(null) + .finalCredits(null) + .total(consumption.negate()) // 收支合计 + .type(TtAccountRecordType.OUTPUT.getCode()) + .source(TtAccountRecordSource.EXPONENT_GAME_TYPE_01.getCode()) + .remark(TtAccountRecordSource.EXPONENT_GAME_TYPE_01.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + apiTtUserBlendErcashMapper.insert(blendErcash); + + return R.ok(map); + } + + @Override + public R box(TtUser ttUser, Integer id) { + List boxList = bindBoxMapper.getBoxList(null, null, "5", null); + if (boxList.isEmpty()) { + return R.ok(new ArrayList<>(), "没有匹配的数据。"); + } + List boxVos = new ArrayList<>(); + List boxIds = new ArrayList<>(); + for (TtBoxA ttBoxA : boxList) { + ExponentBoxVo boxVo = new ExponentBoxVo(); + BeanUtils.copyProperties(ttBoxA, boxVo); + boxVo.setBoxId(ttBoxA.getBoxId()); + boxIds.add(ttBoxA.getBoxId()); + boxVos.add(boxVo); + } + + // 查看当前登录者是否购买本期指数 + LambdaQueryWrapper wrapper1 = Wrappers.lambdaQuery(); + wrapper1.in(TtExponentUser::getBoxId, boxIds); + wrapper1.eq(TtExponentUser::getExponentId, id); + wrapper1.eq(TtExponentUser::getUserId, ttUser.getUserId()); + List exponentUsers = ttExponentUserMapper.selectList(wrapper1); + Map boxIdToUp = new HashMap<>(); + Map boxIdToDown = new HashMap<>(); + if (CollectionUtils.isNotEmpty(exponentUsers)) { + for (TtExponentUser user : exponentUsers) { + if (user == null) { + continue; + } + if (Objects.equals(user.getForward(), 1)) { + int num = boxIdToUp.getOrDefault(user.getBoxId(), 0); + boxIdToUp.put(user.getBoxId(), num + 1); + } + if (Objects.equals(user.getForward(), 2)) { + int num = boxIdToDown.getOrDefault(user.getBoxId(), 0); + boxIdToDown.put(user.getBoxId(), num + 1); + } + } + } + for (ExponentBoxVo vo : boxVos) { + vo.setBuy_forward_up(boxIdToUp.getOrDefault(vo.getBoxId(), 0)); + vo.setBuy_forward_down(boxIdToDown.getOrDefault(vo.getBoxId(), 0)); + } + return R.ok(boxVos); + } + + @Override + public R recenttenperiod() { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.orderByDesc(TtExponent::getId); + wrapper.last("limit 10"); + List db = ttExponentMapper.selectList(wrapper); + return R.ok(db); + } + + /** + * + */ + @Override + public R info(TtUser ttUser) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.orderByDesc(TtExponent::getId); + wrapper.last("limit 1"); + TtExponent db = ttExponentMapper.selectOne(wrapper); + if (db == null) { + return R.fail("指数信息不存在"); + } + ExponentInfoVo vo = new ExponentInfoVo(); + Integer id = db.getId() + 1; + vo.setId(id); + vo.setExponent(0); + // 创建日期格式化对象,指定日期格式 + SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd"); + // 获取Calendar实例 + Calendar calendar = Calendar.getInstance(); + // 设置时间为今天凌晨 + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + // 获取今天凌晨时间 + Date todayMidnight = calendar.getTime(); + String period = ""; + if (db.getCreateTime().after(todayMidnight)) { + period = formatter.format(new Date()) + String.format("%04d", (db.getNumber() + 1)); + } else { + period = formatter.format(new Date()) + "0001"; + } + vo.setPeriod(period); + int createTime = (int) (db.getCreateTime().getTime() / 1000); + vo.setEnd_time(createTime + 60); + vo.setPoints(""); + vo.setState(0); + vo.setNumber(db.getNumber() + 1); + vo.setForward(0); + vo.setCreatetime(createTime + 30); + vo.setUpdatetime(createTime + 30); + int nowTime = (int) (System.currentTimeMillis() / 1000); + vo.setTimer(vo.getCreatetime() - nowTime); + + // 查看当前登录者是否购买本期指数 + LambdaQueryWrapper wrapper2 = Wrappers.lambdaQuery(); + wrapper2.eq(TtExponentUser::getExponentId, id); + wrapper2.eq(TtExponentUser::getUserId, ttUser.getUserId()); + List exponentUsers2 = ttExponentUserMapper.selectList(wrapper2); + // 若购买 + if (CollectionUtils.isNotEmpty(exponentUsers2)) { + vo.setBuy(exponentUsers2.size()); + BigDecimal up = BigDecimal.ZERO; + BigDecimal down = BigDecimal.ZERO; + for (TtExponentUser exponentUser : exponentUsers2) { + if (exponentUser == null) { + continue; + } + if (Objects.equals(exponentUser.getForward(), 1)) { + up = up.add(exponentUser.getPrice()); + } + if (Objects.equals(exponentUser.getForward(), 2)) { + down = down.add(exponentUser.getPrice()); + } + } + vo.setBuy_forward_up(up); + vo.setBuy_forward_down(down); + } + + ExponentInfoVo last = new ExponentInfoVo(); + last.setId(db.getId()); + last.setExponent(db.getExponent()); + period = formatter.format(db.getCreateTime()) + String.format("%04d", db.getNumber()); + last.setPeriod(period); + int lastCreateTime = (int) (db.getCreateTime().getTime() / 1000); + last.setEnd_time(lastCreateTime + 30); + last.setPoints(db.getPoints()); + last.setState(2); + last.setNumber(db.getNumber()); + last.setForward(db.getForward()); + last.setCreatetime(lastCreateTime); + last.setUpdatetime(lastCreateTime); + + vo.setLast(last); + + // 查看当前登录者是否购买本期指数 + LambdaQueryWrapper wrapper1 = Wrappers.lambdaQuery(); + wrapper1.eq(TtExponentUser::getExponentId, db.getId()); + wrapper1.eq(TtExponentUser::getUserId, ttUser.getUserId()); + List exponentUsers = ttExponentUserMapper.selectList(wrapper1); + ExponentJieguoVo jieguo = new ExponentJieguoVo(); + jieguo.setPeriod(period); + jieguo.setExponent(db.getExponent()); + jieguo.setForward(db.getForward()); + // 若购买 + if (CollectionUtils.isNotEmpty(exponentUsers)) { + jieguo.setSkinarr(openExponent(db.getForward(), ttUser, exponentUsers)); + jieguo.setBuy(exponentUsers.size()); + BigDecimal up = BigDecimal.ZERO; + BigDecimal down = BigDecimal.ZERO; + BigDecimal price = BigDecimal.ZERO; + for (TtExponentUser exponentUser : exponentUsers) { + if (exponentUser == null) { + continue; + } + price = price.add(exponentUser.getPrice()); + if (Objects.equals(exponentUser.getForward(), 1)) { + up = up.add(exponentUser.getPrice()); + } + if (Objects.equals(exponentUser.getForward(), 2)) { + down = down.add(exponentUser.getPrice()); + } + } + jieguo.setBuy_money(price); + jieguo.setBuy_forward_up(up); + jieguo.setBuy_forward_down(down); + } + vo.setJieguo(jieguo); + return R.ok(vo); + } + + /** + * 指数开箱 + */ + private List openExponent(int forward, TtUser ttUser, List exponentUsers) { + Map boxIdToNum = new HashMap<>(); + // 判断箱子是否已判定结果 + Map boxIdToOpen = new HashMap<>(); + for (TtExponentUser user : exponentUsers) { + if (user == null) { + continue; + } + // 等于1,则代表指数结果已判定结果 + if (Objects.equals(user.getStatus(), 1)) { + boxIdToOpen.put(user.getBoxId(), true); + int num = boxIdToNum.getOrDefault(user.getBoxId(), 0); + boxIdToNum.put(user.getBoxId(), num + 1); + continue; + } + // 等于2,则代表指数结果已判定结果 + if (Objects.equals(user.getStatus(), 2)) { + boxIdToOpen.put(user.getBoxId(), true); + boxIdToNum.put(user.getBoxId(), 0); + continue; + } + if (Objects.equals(user.getForward(), forward)) { + int num = boxIdToNum.getOrDefault(user.getBoxId(), 0); + boxIdToNum.put(user.getBoxId(), num + 1); + user.setStatus(1); + } else { + user.setStatus(2); + } + user.setUpdateTime(new Date()); + ttExponentUserMapper.updateById(user); + } + if (MapUtils.isEmpty(boxIdToNum)) { + return new ArrayList<>(); + } + // 查询箱子信息 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(TtBox::getBoxId, boxIdToNum.keySet()); + List ttBoxes = ttBoxMapper.selectList(wrapper); + Map ttBoxMap = ttBoxes.stream().collect(Collectors.toMap(TtBox::getBoxId, Function.identity())); + + // 查询该用户已拥有的箱子信息 + LambdaQueryWrapper wrapper1 = Wrappers.lambdaQuery(); + wrapper1.in(TtExponentUserBox::getBoxId, boxIdToNum.keySet()); + wrapper1.eq(TtExponentUserBox::getUserId, ttUser.getUserId()); + List userBoxes = ttExponentUserBoxMapper.selectList(wrapper1); + Map userBoxeMap = userBoxes.stream().collect(Collectors.toMap(TtExponentUserBox::getBoxId, Function.identity())); + + List vos = new ArrayList<>(); + for (Map.Entry entry : boxIdToNum.entrySet()) { + ExponentSkinarrVo vo = new ExponentSkinarrVo(); + vo.setId(entry.getKey()); + vo.setCount(entry.getValue()); + TtBox ttBox = ttBoxMap.get(entry.getKey()); + if (ttBox != null) { + vo.setImage(ttBox.getBoxImg01()); + vo.setImage2(ttBox.getBoxImg02()); + } + vos.add(vo); + // 若指数没有判定过,则进行放入箱子数量 + if (!boxIdToOpen.getOrDefault(entry.getKey(), false)) { + TtExponentUserBox userBox = userBoxeMap.get(entry.getKey()); + if (userBox == null) { + userBox = new TtExponentUserBox(); + userBox.setBoxId(entry.getKey()); + userBox.setUserId(ttUser.getUserId()); + userBox.setNumber(entry.getValue()); + userBox.setCreateTime(new Date()); + userBox.setUpdateTime(new Date()); + ttExponentUserBoxMapper.insert(userBox); + } else { + userBox.setNumber(userBox.getNumber() + entry.getValue()); + userBox.setUpdateTime(new Date()); + ttExponentUserBoxMapper.updateById(userBox); + } + } + } + return vos; + } + + @Override + public R records(TtUser ttUser) { + List vos = ttExponentMapper.exponentOpenBox(ttUser.getUserId()); + if (CollectionUtils.isEmpty(vos)) { + return R.ok(new ArrayList<>()); + } + List boxIds = new ArrayList<>(); + for (ExponentOpenBoxVo vo : vos) { + boxIds.add(vo.getBoxId()); + if (vo.getFailed() != null && vo.getFailed() > 0) { + vo.setState(2); + } + if (vo.getSuccess() != null && vo.getSuccess() > 0) { + vo.setState(1); + } + } + + // 查询箱子信息 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(TtBox::getBoxId, boxIds); + List ttBoxes = ttBoxMapper.selectList(wrapper); + Map ttBoxMap = ttBoxes.stream().collect(Collectors.toMap(TtBox::getBoxId, Function.identity())); + for (ExponentOpenBoxVo vo : vos) { + TtBox ttBox = ttBoxMap.get(vo.getBoxId()); + if (ttBox != null) { + vo.setImage(ttBox.getBoxImg01()); + vo.setImage2(ttBox.getBoxImg02()); + } + } + return R.ok(vos); + } + + @Override + public R mybox(TtUser ttUser) { + // 查询该用户已拥有的箱子信息 + LambdaQueryWrapper wrapper1 = Wrappers.lambdaQuery(); + wrapper1.eq(TtExponentUserBox::getUserId, ttUser.getUserId()); + List userBoxes = ttExponentUserBoxMapper.selectList(wrapper1); + if (CollectionUtils.isEmpty(userBoxes)) { + return R.ok(new ArrayList<>()); + } + List vos = new ArrayList<>(); + List boxIds = new ArrayList<>(); + for (TtExponentUserBox userBox : userBoxes) { + ExponentSkinarrVo vo = new ExponentSkinarrVo(); + vo.setId(userBox.getBoxId()); + boxIds.add(userBox.getBoxId()); + vo.setCount(userBox.getNumber()); + vos.add(vo); + } + // 查询箱子信息 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(TtBox::getBoxId, boxIds); + List ttBoxes = ttBoxMapper.selectList(wrapper); + Map ttBoxMap = ttBoxes.stream().collect(Collectors.toMap(TtBox::getBoxId, Function.identity())); + for (ExponentSkinarrVo vo : vos) { + TtBox ttBox = ttBoxMap.get(vo.getId()); + if (ttBox != null) { + vo.setImage(ttBox.getBoxImg01()); + vo.setImage2(ttBox.getBoxImg02()); + } + } + return R.ok(vos); + } + + @Override + public R openbox(TtUser user, TtBox box, Integer num) { + // 查询该用户已拥有的箱子信息 + LambdaQueryWrapper wrapper1 = Wrappers.lambdaQuery(); + wrapper1.eq(TtExponentUserBox::getUserId, user.getUserId()); + wrapper1.eq(TtExponentUserBox::getBoxId, box.getBoxId()); + List userBoxes = ttExponentUserBoxMapper.selectList(wrapper1); + if (CollectionUtils.isEmpty(userBoxes) || userBoxes.get(0) == null || userBoxes.get(0).getNumber() < num) { + return R.ok(new ArrayList<>()); + } + TtExponentUserBox userBox = userBoxes.get(0); + // 抽奖 + List boxRecords = new ArrayList<>(); + Map boxToOrnament = new HashMap<>(); + for (int i = 0; i < num; i++) { + String ornamentId = lotteryMachine.singleLottery(user, box); + if (StringUtils.isBlank(ornamentId)) { + throw new OrnamentNullException("单次抽奖无结果#"+box.getBoxId()+"#"+ornamentId); + //return R.fail("系统繁忙,请稍后重试。"); + } + // 宝箱详细信息 + TtOrnamentsA ornamentsData = bindBoxMapper.ornamentsInfo(box.getBoxId(), ornamentId); + if (ObjectUtil.isEmpty(ornamentsData)){ + throw new OrnamentNullException("没有符合条件的宝箱信息#"+box.getBoxId()+"#"+ornamentId); + } + // 抽奖成功,构建开箱记录数据 + TtBoxRecords boxRecord = TtBoxRecords.builder() + .userId(user.getUserId()) + + .boxId(box.getBoxId()) + .boxName(box.getBoxName()) + .boxPrice(box.getPrice()) + + .ornamentId(Long.valueOf(ornamentId)) + // .marketHashName(ornamentsData.) + .ornamentName(ObjectUtil.isNotEmpty(ornamentsData.getName()) ? ornamentsData.getName() : ObjectUtil.isNotEmpty(ornamentsData.getShortName()) ? ornamentsData.getShortName() : "无名称") + .imageUrl(ornamentsData.getImageUrl()) + .ornamentsPrice(ornamentsData.getUsePrice()) + .ornamentsLevelId(ornamentsData.getOrnamentsLevelId()) + .ornamentLevelImg(ornamentsData.getLevelImg()) + .holderUserId(user.getUserId()) + .source(TtboxRecordSource.EXPONENT_BOX.getCode()) + .status(IN_PACKSACK_ON.getCode()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + boxRecords.add(boxRecord); + boxToOrnament.put(Long.valueOf(ornamentId), ornamentsData); + } + // 同步保存游戏记录 + boxRecordsService.saveBatch(boxRecords); + // 返回值 + List openBoxVOs = boxRecords.stream().map(item -> { + OpenBoxVO openBoxVO = new OpenBoxVO(); + BeanUtil.copyProperties(item, openBoxVO); + TtOrnamentsA ornamentsData = boxToOrnament.get(item.getOrnamentId()); + if (ornamentsData != null) { + openBoxVO.setShortName(ornamentsData.getShortName()); + openBoxVO.setExteriorName(ornamentsData.getExteriorName()); + } + openBoxVO.setUsePrice(openBoxVO.getOrnamentsPrice()); + openBoxVO.setLevelImg(item.getOrnamentLevelImg()); + return openBoxVO; + }).collect(Collectors.toList()); + + userBox.setNumber(userBox.getNumber() - num); + userBox.setUpdateTime(new Date()); + ttExponentUserBoxMapper.updateById(userBox); + + return R.ok(openBoxVOs); + } + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiFightServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiFightServiceImpl.java new file mode 100644 index 0000000..9b6fe5a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiFightServiceImpl.java @@ -0,0 +1,2698 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.model.UpdateUserAccountBo; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtFightResultService; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.admin.service.TtOrnamentsLevelService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.common.constant.ErrorCode; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.dto.fight.FightDetailParam; +import com.ruoyi.domain.dto.fight.FightOnMyOwnParam; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.fight.FightSeat; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.BoxDataBodyA; +import com.ruoyi.domain.other.CreateFightBody; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtBoxA; +import com.ruoyi.domain.other.TtBoxOrnaments; +import com.ruoyi.domain.other.TtBoxVO; +import com.ruoyi.domain.other.TtFightResult; +import com.ruoyi.domain.other.TtFightUser; +import com.ruoyi.domain.other.TtOrnamentsA; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import com.ruoyi.domain.vo.ApiFightListDataVO; +import com.ruoyi.domain.vo.FightResultDataVO; +import com.ruoyi.domain.vo.PlayerGainsOrnamentsDataVO; +import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO; +import com.ruoyi.domain.vo.UserPackSackDataVO; +import com.ruoyi.domain.vo.fight.AudienceVO; +import com.ruoyi.domain.vo.fight.FightBoxVO; +import com.ruoyi.domain.vo.fight.FightBrief; +import com.ruoyi.domain.vo.fight.FightResultVO; +import com.ruoyi.domain.vo.fight.FightState; +import com.ruoyi.domain.vo.fight.TtFightVO; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.websocket.connector.PushService; +import com.ruoyi.framework.websocket.connector.SubscriptionManager; +import com.ruoyi.framework.websocket.connector.WsResult; +import com.ruoyi.playingmethod.mapper.ApiBindBoxMapper; +import com.ruoyi.playingmethod.mapper.ApiFightMapper; +import com.ruoyi.playingmethod.mapper.ApiTtUserBlendErcashMapper; +import com.ruoyi.playingmethod.model.vo.ApiFightRankingVO; +import com.ruoyi.playingmethod.model.vo.ApiFightTenTopVo; +import com.ruoyi.playingmethod.service.ApiBindBoxService; +import com.ruoyi.playingmethod.service.ApiFightService; +import com.ruoyi.playingmethod.service.ApiFightUserService; +import com.ruoyi.playingmethod.service.FightStateManager; +import com.ruoyi.playingmethod.websocket.constant.SMsgKey; +import com.ruoyi.reward.service.GlobalRewardPoolService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.message.service.MessageService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.ObjectUtils; + +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.ruoyi.admin.config.RedisConstants.JOIN_FIGHT_BEGIN_LOCK; +import static com.ruoyi.admin.config.RedisConstants.JOIN_FIGHT_END_LOCK; +import static com.ruoyi.admin.config.RedisConstants.JOIN_FIGHT_LOCK; +import static com.ruoyi.admin.config.RedisConstants.JOIN_FIGHT_SEAT_READY_LOCK; +import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_OFF; +import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON; +import static com.ruoyi.domain.common.constant.TtboxRecordStatus.RESOLVE; + +@Service +@Slf4j +public class ApiFightServiceImpl extends ServiceImpl implements ApiFightService { + + @Value("${mkcsgo.fight.roundTime}") + private final Integer fightRoundTime = null; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Autowired + private PushService pushService; + + @Autowired + private TtBoxRecordsMapper boxRecordsMapper; + + @Autowired + private TtBoxOrnamentsMapper boxOrnamentsMapper; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ApiBindBoxMapper bindBoxMapper; + + @Autowired + private LotteryMachine lotteryMachine; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private TtOrnamentsYYMapper ttOrnamentsYYMapper; + + @Autowired + private TtOrnamentsLevelMapper ornamentsLevelMapper; + + @Autowired + private ApiTtUserBlendErcashMapper apiTtUserBlendErcashMapper; + + @Autowired + private ApiFightUserService fightUserService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private TtFightResultService ttFightResultService; + + @Autowired + private MessageService messageService; + + private final TtBoxMapper boxMapper; + private final TtBoxRecordsService ttBoxRecordsService; + @Autowired + private TtBoxRecordsMapper ttBoxRecordsMapper; + private final TtUserService userService; + + private final RabbitTemplate rabbitTemplate; + private final TtBoxRecordsService boxRecordsService; + private final TtFightMapper fightMapper; + private final ApiFightMapper apiFightMapper; + private final TtFightUserMapper fightUserMapper; + private final ApiBindBoxService apiBindBoxService; + private final TtFightResultMapper fightResultMapper; + private final RedisLock redisLock; + private final Executor customThreadPoolExecutor; + private final TtOrnamentService ttOrnamentsZBTService; + + + private final TtOrnamentsLevelService ttOrnamentsLevelService; + @Autowired + private TtUserMapper ttUserMapper; + @Autowired + private GlobalRewardPoolService globalRewardPoolService; + + @Autowired + private FightStateManager fightStateManager; + + public ApiFightServiceImpl(TtBoxMapper boxMapper, + TtUserService userService, + TtBoxRecordsService ttBoxRecordsService, + TtBoxRecordsService boxRecordsService, + TtFightMapper fightMapper, + RabbitTemplate rabbitTemplate, + TtOrnamentsLevelService ttOrnamentsLevelService, + TtOrnamentService ttOrnamentsZBTService, + ApiFightMapper apiFightMapper, + TtFightUserMapper fightUserMapper, + ApiBindBoxService apiBindBoxService, + TtFightResultMapper fightResultMapper, + RedisLock redisLock, + Executor customThreadPoolExecutor) { + this.boxMapper = boxMapper; + this.ttOrnamentsLevelService = ttOrnamentsLevelService; + this.userService = userService; + this.ttBoxRecordsService = ttBoxRecordsService; + this.boxRecordsService = boxRecordsService; + this.fightMapper = fightMapper; + this.rabbitTemplate = rabbitTemplate; + this.ttOrnamentsZBTService = ttOrnamentsZBTService; + this.apiFightMapper = apiFightMapper; + this.fightUserMapper = fightUserMapper; + this.apiBindBoxService = apiBindBoxService; + this.fightResultMapper = fightResultMapper; + this.redisLock = redisLock; + this.customThreadPoolExecutor = customThreadPoolExecutor; + } + + + private AjaxResult joinFightRoomCheck(Integer userId, Integer fightId) { + + TtUser player = userService.getById(userId); + TtFight ttFight = new LambdaQueryChainWrapper<>(fightMapper) + .eq(TtFight::getId, fightId) + .eq(TtFight::getStatus, "0") + .one(); + List roomPlayers = new LambdaQueryChainWrapper<>(fightUserMapper) + .eq(TtFightUser::getFightId, fightId) + .eq(TtFightUser::getUserId, player.getUserId()) + .list(); + + if (player.getAccountAmount().compareTo(ttFight.getBoxPriceTotal()) < 0) + return AjaxResult.error(702, "加入房间失败,您的账户余额不足!"); + if (ObjectUtils.isEmpty(ttFight)) return AjaxResult.error("加入对战失败,该房间已结束!"); + if (roomPlayers.size() >= 1) return AjaxResult.error("您已成功加入该房间,请勿重复加入!"); + + return AjaxResult.success(); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public R createFight(CreateFightBody createFightParam, TtUser player) { + + // log.info("初始化对局---------------------------------------------------------------------- 1"); + // 检查数据 + R fightCheck = createFightCheck(createFightParam, player); + if (!fightCheck.getCode().equals(200)) return fightCheck; + + TtFight fight = (TtFight) fightCheck.getData(); + + // 账户结算 + R r = readyFightAccounting(fight, player); + if (R.isError(r)) { + return R.fail(r.getMsg()); + } + + // 初始化对局 + fight.setModel(createFightParam.getModel()); + // 直接加入:房主固定坐在 code=0 的位置,其余座位保持 1/2/3 顺序 + List seats = fight.getSeats(); + FightSeat ready = seats.remove(0).sitDown(player.getUserId()).ready(); + // 补充用户信息 + ready.setAvatar(player.getAvatar()); + ready.setNickName(player.getNickName()); + seats.add(0, ready); + + save(fight); + + // 同步保存参与人员信息 + TtFightUser fightUser = TtFightUser.builder() + .fightId(fight.getId()) + .userId(player.getUserId()) + .joinPrice(fight.getBoxPriceTotal()) + .createTime(new Date()) + .build(); + fightUserService.save(fightUser); + + + // ws广播最新对局(异步) + CompletableFuture.runAsync(() -> { + pushService.broadcastToChannel(SubscriptionManager.hallChannel(), WsResult.ok(SMsgKey.FIGHT_ROOM_UPDATE, buildRoomUpdateData(Arrays.asList(fight)), "对战房间最新信息")); + log.info("用户{}【创建房间】{}", player.getUserId(), fight.getId()); + }, customThreadPoolExecutor); + + return R.ok(fight, "创建成功,可以建立 fightRoom ws"); + } + + public R createFightCheck(CreateFightBody createFightParam, TtUser player) { + + if (ObjectUtil.isEmpty(createFightParam.getModel())) { + return R.fail("模式不能为空"); + } + if (!createFightParam.getModel().equals("0") && !createFightParam.getModel().equals("1")) { + return R.fail("非法的模式类型"); + } + + // 组队模式校验:目前仅支持2v2,必须4人房 + boolean isTeamMode = Integer.valueOf(1).equals(createFightParam.getTeamMode()); + if (isTeamMode && !Integer.valueOf(4).equals(createFightParam.getPlayerNumber())) { + return R.fail("组队对战仅支持2v2,玩家人数必须为4!"); + } + + Map boxIdAndNumber = createFightParam.getBoxIdAndNumber(); + + // 本局箱子总数 + Integer boxNum = 0; + LinkedHashMap boxMap = new LinkedHashMap<>(); + // 计算整局游戏箱子总价 + BigDecimal boxTotalPrice = BigDecimal.ZERO; + for (Integer boxId : boxIdAndNumber.keySet()) { + TtBox box = new LambdaQueryChainWrapper<>(boxMapper) + .eq(TtBox::getBoxId, boxId) + .eq(TtBox::getIsFight, "0") + .eq(TtBox::getStatus, "0") + .one(); + if (ObjectUtils.isEmpty(box)) { + return R.fail("不存在的箱子!"); + } + + boxMap.put(String.valueOf(boxId), new FightBoxVO(boxId, boxIdAndNumber.get(boxId), box.getBoxImg01(), box.getBoxImg02())); + + BigDecimal multiply = BigDecimal.valueOf(boxIdAndNumber.get(boxId)).multiply(box.getPrice()); + boxTotalPrice = boxTotalPrice.add(multiply); + boxNum = boxNum + boxIdAndNumber.get(boxId); + } + + // 回合数最多为15回合 + if (boxNum > 15 || boxNum <= 0) return R.fail("非法参数值 boxNum【" + boxNum + "】,宝箱数量范围1-15个!"); + if (player.getAccountAmount() == null || player.getAccountAmount().compareTo(boxTotalPrice) < 0) + return R.fail(702, "您的账户余额不足!"); + + // ObjectMapper objectMapper = new ObjectMapper(); + + // 初始化座位信息 + ArrayList seats = new ArrayList<>(); + for (int i = 0; i < createFightParam.getPlayerNumber(); i++) { + FightSeat seat = FightSeat.builder() + .code(i) + .status(0) + .awardTotalPrices(BigDecimal.ZERO) + .build(); + seats.add(seat); + } + + TtFight fight = TtFight.builder() + .boxData(boxMap) + .boxPriceTotal(boxTotalPrice) + .status(0) + .roundNumber(boxNum) + .userId(player.getUserId()) + .ownerNickName(player.getNickName()) + .playerNum(createFightParam.getPlayerNumber()) + .teamMode(isTeamMode ? 1 : 0) + .seats(seats) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + + return R.ok(fight); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R copyFight(Integer sourceFightId, TtUser player) { + TtFight sourceFight = getById(sourceFightId); + if (ObjectUtil.isEmpty(sourceFight)) { + return R.fail("原房间不存在。"); + } + if (MapUtils.isEmpty(sourceFight.getBoxData())) { + return R.fail("原房间宝箱配置为空,无法复刻。"); + } + if (ObjectUtil.isEmpty(sourceFight.getPlayerNum()) || sourceFight.getPlayerNum() <= 0) { + return R.fail("原房间人数配置异常,无法复刻。"); + } + if (ObjectUtil.isEmpty(sourceFight.getModel()) + || (!"0".equals(sourceFight.getModel()) && !"1".equals(sourceFight.getModel()))) { + return R.fail("原房间模式配置异常,无法复刻。"); + } + + // 将数据库中序列化的 boxData 统一转换为 FightBoxVO,兼容 JacksonTypeHandler 反序列化后的实际类型 + ObjectMapper objectMapper = new ObjectMapper(); + Map boxData = objectMapper.convertValue(sourceFight.getBoxData(), + new com.fasterxml.jackson.core.type.TypeReference>() { + }); + + // 使用 LinkedHashMap 保持原房间宝箱顺序 + Map boxIdAndNumber = new LinkedHashMap<>(); + for (Map.Entry entry : boxData.entrySet()) { + FightBoxVO vo = entry.getValue(); + if (vo == null || vo.getBoxId() == null || vo.getNumber() == null) { + continue; + } + boxIdAndNumber.put(vo.getBoxId(), vo.getNumber()); + } + + if (boxIdAndNumber.isEmpty()) { + return R.fail("原房间宝箱配置为空,无法复刻。"); + } + + CreateFightBody createFightParam = new CreateFightBody(); + createFightParam.setModel(sourceFight.getModel()); + createFightParam.setPlayerNumber(sourceFight.getPlayerNum()); + createFightParam.setTeamMode(sourceFight.getTeamMode()); + createFightParam.setBoxIdAndNumber(boxIdAndNumber); + + // 复用 createFight:扣款、初始化座位、房主就座、广播大厅等逻辑完全一致 + return createFight(createFightParam, player); + } + + private R joinFightCheck(Integer fightId, TtUser player, Integer seatCode) { + + ObjectMapper objectMapper = new ObjectMapper(); + + // 查询对局信息(把mybatis一级缓存关掉) + TtFight fight = null; + fight = new LambdaQueryChainWrapper<>(fightMapper) + .eq(TtFight::getId, fightId) + .eq(TtFight::getStatus, "0") + .one(); + + if (ObjectUtils.isEmpty(fight)) return R.fail("此对局已结束!"); + + // 检查主播不能进入普通玩家房间 + /*if ("02".equals(createFightUser.getUserType()) && "01".equals(player.getUserType())) { + return R.fail("主播不可进入普通玩家房间"); + }*/ + + // 如果已报名,直接进入房间 + List list = JSONUtil.toList(JSON.toJSONString(fight.getSeats()), FightSeat.class); + for (FightSeat seat : list) { + if (ObjectUtil.isNotEmpty(seat.getPlayerId()) && seat.getPlayerId().equals(player.getUserId())) { + return R.ok(0, "已报名,再次进入房间。"); + } + } + + // 检查余额(机器人跳过) + boolean isRobot = UserType.ROBOT_USER.getCode().equals(player.getUserType()); + if (!isRobot && (player.getAccountAmount() == null || player.getAccountAmount().compareTo(fight.getBoxPriceTotal()) < 0)) + return R.fail(702, "加入房间失败,您的账户余额不足!"); + + // 座位规整(原逻辑逐个 convertValue 保证类型一致) + List seats = fight.getSeats(); + for (int i = 0; i < seats.size(); i++) { + seats.set(i, objectMapper.convertValue(seats.get(i), FightSeat.class)); + } + + if (ObjectUtil.isNotEmpty(seatCode)) { + // 指定座位入座:校验座位存在且为空位(已报名场景在方法开头已返回,此处占用必为他人占用) + FightSeat target = null; + for (FightSeat seat : seats) { + if (seatCode.equals(seat.getCode())) { + target = seat; + break; + } + } + if (ObjectUtil.isEmpty(target)) return R.fail("座位不存在。"); + if (!target.getStatus().equals(0)) return R.fail("该座位已被占用,请选择其他座位。"); + target.sitDown(player.getUserId()); + target.setAvatar(player.getAvatar()); + target.setNickName(player.getNickName()); + } else { + // 自动坐第一个空位(旧逻辑,兼容机器人/未选座场景) + boolean f = false; + for (FightSeat seat : seats) { + if (seat.getStatus().equals(0) && !f) { + seat.sitDown(player.getUserId()); + seat.setAvatar(player.getAvatar()); + seat.setNickName(player.getNickName()); + f = true; + } + } + if (!f) return R.fail("对局人满。"); + } + + try { + // 返回新的座位信息 + return R.ok(objectMapper.writeValueAsString(seats)); + } catch (JsonProcessingException e) { + log.warn("加入对局失败,请稍后重试。"); + return R.fail("加入对局失败,请稍后重试。"); + } + } + + // 加入房间(兼容旧调用:自动坐第一个空位,供机器人/未选座场景使用) + @Override + @Transactional + public R joinFight(Integer fightId, TtUser player) { + return joinFight(fightId, player, null); + } + + // 加入房间(支持指定座位;seatCode=null 时自动坐第一个空位) + @Override + @Transactional + public R joinFight(Integer fightId, TtUser player, Integer seatCode) { + // 加入 + Boolean lock = false; + for (int i = 0; i < 10; i++) { + lock = redisLock.tryLock(JOIN_FIGHT_LOCK + fightId, 3L, 7L, TimeUnit.SECONDS); + if (lock) { + break; + } + } + + if (!lock) { + return R.fail("服务器繁忙,稍后重试。"); + } + + try { + // 复查 + R check = joinFightCheck(fightId, player, seatCode); + if (!check.getCode().equals(200)) { + redisLock.unlock(JOIN_FIGHT_LOCK + fightId); + return check; + } + if (ObjectUtil.isNotEmpty(check.getData()) && check.getData().equals(0)) { + return check; + } + + ObjectMapper objectMapper = new ObjectMapper(); + List seats = objectMapper.readValue((String) check.getData(), new com.fasterxml.jackson.core.type.TypeReference>() { + }); + LambdaUpdateWrapper fightUpdate = new LambdaUpdateWrapper<>(); + fightUpdate + .eq(TtFight::getId, fightId) + .eq(TtFight::getStatus, 0) + .set(TtFight::getSeats, check.getData()); + if (seatsIsFull(seats)) { + System.out.println("人满"); + // fightUpdate.set(TtFight::getStatus, 1); + } + update(fightUpdate); + log.info("玩家{}加入对局{}", player.getUserId(), fightId); + + TtFight fight = getById(fightId); + + if (MapUtils.isNotEmpty(fight.getBoxData())) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, fight.getBoxData().keySet()); + List boxes = boxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + fight.getBoxData().keySet().forEach(boxId -> { + Object value = fight.getBoxData().get(boxId); + FightBoxVO a = null; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + a = new FightBoxVO(map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO)); + } else { + a = (FightBoxVO) value; + a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO)); + } + fight.getBoxData().put(boxId, a); + }); + } + + // ws广播参加对局玩家信息(异步) + CompletableFuture.runAsync(() -> { + ArrayList fightBoxVOList = enrichFightBoxVOList(fight); + FightState waiting = fightStateManager.buildWaitingState(fight, fightBoxVOList); + pushService.broadcastToChannel(SubscriptionManager.fightChannel(fight.getId().longValue()), + WsResult.ok(SMsgKey.FIGHT_STATE, waiting, "对战房间最新信息")); + }, customThreadPoolExecutor); + + if (seatsIsFull(seats)) { + return R.ok(1, "加入对战成功。房间已满人"); + } + return R.ok(0, "加入对战成功。"); + + } catch (Exception e) { + e.printStackTrace(); + return R.fail("加入对战失败!"); + } finally { + redisLock.unlock(JOIN_FIGHT_LOCK + fightId); + } + } + + public R fightBeginCheck(Integer fightId, TtUser player) { + + TtFight fight = new LambdaQueryChainWrapper(fightMapper) + .eq(TtFight::getId, fightId) + // .eq(TtFight::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(fight)) return R.fail(null, "不存在的对局房间"); + if (fight.getStatus().equals(1)) return R.fail(fight, "对局已开始"); + if (fight.getStatus().equals(2) || fight.getStatus().equals(3)) return R.fail(fight, "对局已结束"); + + ObjectMapper objectMapper = new ObjectMapper(); + Map boxData = objectMapper.convertValue(fight.getBoxData(), new com.fasterxml.jackson.core.type.TypeReference>() { + }); + List seats = objectMapper.convertValue(fight.getSeats(), new com.fasterxml.jackson.core.type.TypeReference>() { + }); + + for (FightSeat seat : seats) { + if (!seat.getStatus().equals(2)) return R.fail("用户" + seat.getPlayerId() + "未准备。"); + } + + fight.setBoxData(boxData); + fight.setSeats(seats); + return R.ok(fight); + + } + + // 开始游戏 + @Override + public R fightBegin(Integer fightId, TtUser player) { + + Boolean lock = redisLock.tryLock(JOIN_FIGHT_BEGIN_LOCK + fightId, 15L, 7L, TimeUnit.SECONDS); + + ObjectMapper objectMapper = new ObjectMapper(); + + // 多次尝试失败,检查一下房间状态 + if (!lock) { + TtFight fight = new LambdaQueryChainWrapper<>(fightMapper) + .eq(TtFight::getId, fightId) + .one(); + if (fight.getStatus().equals(0)) return R.fail("系统繁忙,请稍后重试。"); + return R.ok(null, "游戏已经开始"); + } + + // 抢到锁 + try { + + // 检查对局信息 + R check = fightBeginCheck(fightId, player); + if (!check.getCode().equals(200)) { + if (ObjectUtil.isEmpty(check.getData())) return check; + TtFight fight = objectMapper.convertValue(check.getData(), TtFight.class); + if (fight.getStatus().equals(1)) return R.ok(fight, "对局已开始"); + if (fight.getStatus().equals(2) || fight.getStatus().equals(3)) return R.ok(fight, "对局已结束"); + return check; + } + + TtFight fight = objectMapper.convertValue(check.getData(), TtFight.class); + + // 抽奖 计算开箱结果 + List result = newComputerFight(fight); + + // 计算胜负(组队模式或2v2按队伍总价值判胜,胜方两人均为赢家) + List winnerIds; + if (Integer.valueOf(1).equals(fight.getTeamMode())) { + winnerIds = computerTeamWinner(fight.getModel(), fight.getSeats(), result); + } else { + winnerIds = computerWinner(fight.getModel(), fight.getSeats(), result); + } + + List fightResult = new ArrayList<>(); + // 分配战利品 + // 新逻辑, 若多个胜利者,且胜利者个数小于总玩家,代表着剩余失败的奖励要平分 + if (fight.getSeats().size() > winnerIds.size() && winnerIds.size() > 1) { + BigDecimal total = new BigDecimal(0); + for (TtBoxRecords item : result) { + if (ObjectUtil.isEmpty(item.getOrnamentId())) { + // 过滤掉抽空的情况 + item.setHolderUserId(null); + fightResult.add(item); + continue; + } + // 胜利者的钱包分配给胜利者 + if (winnerIds.contains(item.getUserId())) { + item.setHolderUserId(item.getUserId()); + fightResult.add(item); + continue; + } + item.setHolderUserId(null); + fightResult.add(item); + // 失败者的钱要加起来平分 + total = total.add(item.getOrnamentsPrice()); + } + BigDecimal bisect = total.divide(new BigDecimal(winnerIds.size()), 2, RoundingMode.HALF_UP); + fightResult.addAll(bisectPrize(fight, bisect, winnerIds)); + } else { + // 旧逻辑 + int count = 0; + for (TtBoxRecords item : result) { + if (ObjectUtil.isEmpty(item.getOrnamentId())) { + // 过滤掉抽空的情况 + item.setHolderUserId(null); + fightResult.add(item); + continue; + } + + if (winnerIds.contains(item.getUserId())) { + item.setHolderUserId(item.getUserId()); + } else { + int i = count % winnerIds.size(); + item.setHolderUserId(winnerIds.get(i)); + count++; + } + fightResult.add(item); + } + } + + // 分配失败者奖励 + List losePrize = loserPrize(fight, winnerIds); + + // 合并所有对战出货 + fightResult.addAll(losePrize); + + // 将对战结果临时存入Redis供进行中查询(写入数据库耗时长导致刚开始对战时进入的用户查询到空数据) + String key = "fight_result:fight_" + fightId; + redisCache.setCacheObject(key, fightResult, 3, TimeUnit.MINUTES); + + // 同步保存游戏出货结果 + boxRecordsService.saveBatch(fightResult); + + // ===== 新增:对战结果通知(过滤机器人用户)===== + AsyncManager.me().run(() -> sendFightResultNotice(fight, fightResult, winnerIds)); + // ===== 新增对战结果通知 结束 ===== + + // 同步更新对局状态 + LambdaUpdateWrapper fightUpdate = new LambdaUpdateWrapper<>(); + fightUpdate + .eq(TtFight::getId, fightId) + .eq(TtFight::getStatus, 0) + .set(TtFight::getWinnerIds, JSON.toJSONString(winnerIds)) + // 持久化 computerWinner 填充的座位 awardTotalPrices,供观战/历史查询渲染胜负徽章 + .set(TtFight::getSeats, JSON.toJSONString(fight.getSeats())) + .set(TtFight::getStatus, 1) + .set(TtFight::getBeginTime, new Timestamp(System.currentTimeMillis())); + boolean updated = update(fightUpdate); + if (!updated) { + log.warn("对战{} fightBegin条件更新未生效(status可能已非0),winner_ids可能未写入", fightId); + } + + // 构建宝箱详情列表(同步执行) + Map boxData = fight.getBoxData(); + ArrayList fightBoxVOList = new ArrayList<>(); + LambdaQueryWrapper boxQuery = new LambdaQueryWrapper<>(); + boxQuery.in(TtBox::getBoxId, boxData.keySet()); + List boxes = boxMapper.selectList(boxQuery); + Map boxMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + Map boxNameMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, b -> b.getBoxName() != null ? b.getBoxName() : "")); + boxData.keySet().forEach(boxId -> { + List boxOrnamentsVOS = boxOrnamentsMapper.selectTtBoxOrnamentsList(Integer.valueOf(boxId)); + FightBoxVO vo = resolveFightBoxVO(boxData.get(boxId)); + if (vo != null) { + vo.setOrnaments(boxOrnamentsVOS); + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), BigDecimal.ZERO)); + vo.setBoxName(boxNameMap.getOrDefault(vo.getBoxId(), "")); + fightBoxVOList.add(vo); + boxData.put(boxId, vo); + } + }); + + // 重新查询 status=1 的 fight + TtFight newFight = new LambdaQueryChainWrapper<>(fightMapper) + .eq(TtFight::getId, fightId) + .eq(TtFight::getStatus, 1) + .one(); + + // 推送大厅最新的房间信息 + pushService.broadcastToChannel(SubscriptionManager.hallChannel(), WsResult.ok(SMsgKey.FIGHT_ROOM_UPDATE, buildRoomUpdateData(Arrays.asList(newFight)), "对战房间最新信息")); + + // 委托 FightStateManager 驱动状态同步 + fightStateManager.startFightProgress(newFight, fightResult, winnerIds, fightBoxVOList); + + return R.ok(null, "游戏开始。"); + + } catch (Exception e) { + e.printStackTrace(); + log.error("开始游戏失败,对战房间号{} 异常信息:{}", fightId, e.getMessage(), e); + return R.fail("系统繁忙,请稍后重试。"); + } finally { + redisLock.unlock(JOIN_FIGHT_BEGIN_LOCK + fightId); + } + } + + /** + * 对战结果通知(过滤机器人用户) + * 示例:对战结果:张三 胜利,最高价值饰品:AK-47(价值 ¥350),本局共获得价值 ¥820 的饰品。 + */ + private void sendFightResultNotice(TtFight fight, List fightResult, List winnerIds) { + try { + List allPlayerIds = fight.getSeats().stream() + .map(FightSeat::getPlayerId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (allPlayerIds.isEmpty()) return; + + // 查询真实用户,过滤机器人 + List realPlayers = new LambdaQueryChainWrapper<>(ttUserMapper) + .in(TtUser::getUserId, allPlayerIds) + .ne(TtUser::getUserType, UserType.ROBOT_USER.getCode()) + .list(); + + // 按持有人统计获得饰品 + Map> holderMap = fightResult.stream() + .filter(r -> r.getHolderUserId() != null && r.getOrnamentId() != null) + .collect(Collectors.groupingBy(r -> r.getHolderUserId().intValue())); + + // 0=欧皇(天选之战) 1=非酋(逆袭之战) + boolean isTopRank = "0".equals(fight.getModel()); + String category = isTopRank + ? MessageCategoryEnum.GAME_PVP_TOP.getCode() + : MessageCategoryEnum.GAME_PVP_LOW.getCode(); + String title = isTopRank + ? MessageCategoryEnum.GAME_PVP_TOP.getTitle() + : MessageCategoryEnum.GAME_PVP_LOW.getTitle(); + + for (TtUser realUser : realPlayers) { + Integer playerId = realUser.getUserId(); + boolean isWinner = winnerIds.contains(playerId); + // 仅给胜利玩家发送对战结果通知 + if (!isWinner) { + continue; + } + List myItems = holderMap.getOrDefault(playerId, Collections.emptyList()); + + // 计算总获得价值 + BigDecimal totalValue = myItems.stream() + .map(TtBoxRecords::getOrnamentsPrice) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 找最高价值物品 + TtBoxRecords bestItem = myItems.stream() + .filter(r -> r.getOrnamentsPrice() != null) + .max(Comparator.comparing(TtBoxRecords::getOrnamentsPrice)) + .orElse(null); + + // 拼接通知内容 + StringBuilder content = new StringBuilder(); + content.append("对战结果:") + .append(realUser.getNickName()) + .append("胜利") + .append(","); + if (bestItem != null) { + String priceStr = bestItem.getOrnamentsPrice().stripTrailingZeros().toPlainString(); + content.append("最高价值饰品:") + .append(bestItem.getOrnamentName()) + .append("(价值 ¥").append(priceStr).append("),"); + } + content.append("本局共获得价值 ¥").append(totalValue.stripTrailingZeros().toPlainString()).append(" 的饰品。"); + messageService.sendNotice(Long.valueOf(playerId), title, content.toString(), category); + } + } catch (Exception e) { + log.error("对战通知推送失败,fightId={}, error={}", fight.getId(), e.getMessage(), e); + } + } + + private List bisectPrize(TtFight fight, BigDecimal bisect, List winnerIds) { + + List boxRecords = new ArrayList<>(); + + // 获取平分奖专属配置(与安慰奖 fightLoserPrize 独立) + String bisectPrize = configService.selectConfigByKey("fightBisectPrize"); + // 兜底:若未配置平分奖饰品,则复用安慰奖配置 + if (StrUtil.isBlank(bisectPrize)) { + bisectPrize = configService.selectConfigByKey("fightLoserPrize"); + } + + List ornamentIds = JSONUtil.parseArray(bisectPrize).toList(Long.class); + + if (ornamentIds.isEmpty()) return boxRecords; + + List loserPrizeList = new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .in(TtOrnament::getId, ornamentIds) + .list(); + if (ObjectUtil.isEmpty(loserPrizeList) || loserPrizeList.isEmpty()) return boxRecords; + + // 批量获取品质等级信息 + Set levelIds = loserPrizeList.stream() + .map(TtOrnament::getQuality) + .filter(Objects::nonNull) + .map(Integer::valueOf) + .collect(Collectors.toSet()); + Map levelImgMap = new HashMap<>(); + if (!levelIds.isEmpty()) { + List levels = new LambdaQueryChainWrapper<>(ornamentsLevelMapper) + .in(TtOrnamentsLevel::getId, levelIds) + .list(); + for (TtOrnamentsLevel level : levels) { + levelImgMap.put(level.getId(), level.getLevelImg()); + } + } + + for (Integer winnerId : winnerIds) { + + List records = loserPrizeList.stream().map(ornament -> { + Integer levelId = ornament.getQuality() != null ? Integer.valueOf(ornament.getQuality()) : null; + return TtBoxRecords.builder() + .userId(winnerId) + + .ornamentId(ornament.getId()) + .marketHashName("对战平分奖") + .ornamentName("对战平分奖") + .imageUrl(ornament.getImageUrl()) + .ornamentsPrice(bisect) + .ornamentsLevelId(levelId) + .ornamentLevelImg(levelId != null ? levelImgMap.get(levelId) : null) + + .holderUserId(winnerId) + + .source(TtboxRecordSource.FIGHT.getCode()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .fightId(fight.getId()) + // .isShow(0) + .status(IN_PACKSACK_OFF.getCode()) + .build(); + + }).collect(Collectors.toList()); + + boxRecords.addAll(records); + + } + + return boxRecords; + } + + private List loserPrize(TtFight fight, List winnerIds) { + + List loserBoxRecords = new ArrayList<>(); + + List seats = fight.getSeats(); + + List losers = new ArrayList<>(); + for (FightSeat seat : seats) { + if (winnerIds.contains(seat.getPlayerId())) continue; + losers.add(seat.getPlayerId()); + } + + // 获取失败奖励 + String loserPrize = configService.selectConfigByKey("fightLoserPrize"); + + List ornamentIds = JSONUtil.parseArray(loserPrize).toList(Long.class); + + if (ornamentIds.isEmpty()) return loserBoxRecords; + + List loserPrizeList = new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .in(TtOrnament::getId, ornamentIds) + .list(); + if (ObjectUtil.isEmpty(loserPrizeList) || loserPrizeList.isEmpty()) return loserBoxRecords; + + // 批量获取品质等级信息 + Set levelIds = loserPrizeList.stream() + .map(TtOrnament::getQuality) + .filter(Objects::nonNull) + .map(Integer::valueOf) + .collect(Collectors.toSet()); + Map levelImgMap = new HashMap<>(); + if (!levelIds.isEmpty()) { + List levels = new LambdaQueryChainWrapper<>(ornamentsLevelMapper) + .in(TtOrnamentsLevel::getId, levelIds) + .list(); + for (TtOrnamentsLevel level : levels) { + levelImgMap.put(level.getId(), level.getLevelImg()); + } + } + + for (Integer loserId : losers) { + + List records = loserPrizeList.stream().map(ornament -> { + Integer levelId = ornament.getQuality() != null ? Integer.valueOf(ornament.getQuality()) : null; + return TtBoxRecords.builder() + .userId(loserId) + + .ornamentId(ornament.getId()) + .marketHashName(ornament.getMarketHashName()) + .ornamentName(ornament.getShortName()) + .imageUrl(ornament.getImageUrl()) + .ornamentsPrice(ornament.getUsePrice()) + .ornamentsLevelId(levelId) + .ornamentLevelImg(levelId != null ? levelImgMap.get(levelId) : null) + + .holderUserId(loserId) + + .source(TtboxRecordSource.FIGHT.getCode()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .fightId(fight.getId()) + // .isShow(0) + .status(IN_PACKSACK_OFF.getCode()) + .build(); + + }).collect(Collectors.toList()); + + loserBoxRecords.addAll(records); + + } + + return loserBoxRecords; + } + + public List computerWinner(String model, List seats, List fightResult) { + + // 参数校验:fightResult 为空直接返回空列表 + if (CollectionUtils.isEmpty(fightResult)) { + log.warn("computerWinner: fightResult 为空,model={}", model); + return new ArrayList<>(); + } + + List winnerIds = new ArrayList<>(); + if (model.equals("0")) { + BigDecimal max = BigDecimal.ZERO; + for (FightSeat seat : seats) { + for (TtBoxRecords record : fightResult) { + if (record.getHolderUserId() != null && record.getHolderUserId().equals(seat.getPlayerId())) { + BigDecimal price = record.getOrnamentsPrice() != null ? record.getOrnamentsPrice() : BigDecimal.ZERO; + seat.setAwardTotalPrices(seat.getAwardTotalPrices().add(price)); + } + } + if (max.compareTo(seat.getAwardTotalPrices()) < 0) { + max = seat.getAwardTotalPrices(); + winnerIds.clear(); + winnerIds.add(seat.getPlayerId()); + } else if (max.compareTo(seat.getAwardTotalPrices()) == 0) { + winnerIds.add(seat.getPlayerId()); + } + } + } else if (model.equals("1")) { + + log.info("计算胜负,非酋模式。"); + + BigDecimal min = null; + for (FightSeat seat : seats) { + for (TtBoxRecords record : fightResult) { + if (record.getHolderUserId().equals(seat.getPlayerId())) { + seat.setAwardTotalPrices(seat.getAwardTotalPrices() + .add(ObjectUtil.isNotEmpty(record.getOrnamentsPrice()) ? record.getOrnamentsPrice() : BigDecimal.ZERO)); + } + } + if (ObjectUtil.isEmpty(min) || min.compareTo(BigDecimal.ZERO) == 0) { + min = seat.getAwardTotalPrices(); + } + if (min.compareTo(seat.getAwardTotalPrices()) > 0) { + min = seat.getAwardTotalPrices(); + winnerIds.clear(); + winnerIds.add(seat.getPlayerId()); + } else if (min.compareTo(seat.getAwardTotalPrices()) == 0) { + winnerIds.add(seat.getPlayerId()); + } + } + + log.info("赢家{}", winnerIds); + + } else { + log.warn("非法的对战模式编号"); + return winnerIds; + } + + return winnerIds; + } + + /** + * 2v2组队判胜:座位0/1为一队、座位2/3为二队,按队伍累计掉落总价值判胜负。 + * 欧皇(model=0):队伍总价值高者胜;非酋(model=1):队伍总价值低者胜;平局时双方均为赢家(各自保留掉落)。 + * 胜方两人均进入 winnerIds,fightBegin 中现有平分逻辑(胜者保留自己掉落 + 败方总价值平分)自动生效。 + */ + public List computerTeamWinner(String model, List seats, List fightResult) { + + List winnerIds = new ArrayList<>(); + if (!"0".equals(model) && !"1".equals(model)) { + log.warn("非法的对战模式编号"); + return winnerIds; + } + + // 累计每个座位的个人掉落价值(供前端展示),并按队伍汇总 + // 队伍划分:座位code 0/1 -> 队0,座位code 2/3 -> 队1 + BigDecimal[] teamTotals = {BigDecimal.ZERO, BigDecimal.ZERO}; + List> teamPlayers = new ArrayList<>(); + teamPlayers.add(new ArrayList<>()); + teamPlayers.add(new ArrayList<>()); + + for (FightSeat seat : seats) { + for (TtBoxRecords record : fightResult) { + if (record.getHolderUserId() != null && record.getHolderUserId().equals(seat.getPlayerId())) { + seat.setAwardTotalPrices(seat.getAwardTotalPrices() + .add(ObjectUtil.isNotEmpty(record.getOrnamentsPrice()) ? record.getOrnamentsPrice() : BigDecimal.ZERO)); + } + } + int teamIndex = (seat.getCode() != null && seat.getCode() >= 2) ? 1 : 0; + teamTotals[teamIndex] = teamTotals[teamIndex].add(seat.getAwardTotalPrices()); + teamPlayers.get(teamIndex).add(seat.getPlayerId()); + } + + int compare = teamTotals[0].compareTo(teamTotals[1]); + if (compare == 0) { + // 平局:双方均为赢家,各自保留掉落,不产生饰品转移 + winnerIds.addAll(teamPlayers.get(0)); + winnerIds.addAll(teamPlayers.get(1)); + } else if ("0".equals(model)) { + // 欧皇:队伍总价值高者胜 + winnerIds.addAll(compare > 0 ? teamPlayers.get(0) : teamPlayers.get(1)); + } else { + // 非酋:队伍总价值低者胜 + winnerIds.addAll(compare < 0 ? teamPlayers.get(0) : teamPlayers.get(1)); + } + + log.info("2v2组队对战判胜,模式{},队0总价值{},队1总价值{},赢家{}", model, teamTotals[0], teamTotals[1], winnerIds); + + return winnerIds; + } + + @Override + public R fightEnd(Integer fightId, Long userId) { + + TtFight fight = new LambdaQueryChainWrapper<>(fightMapper) + .eq(TtFight::getId, fightId) + .one(); + if (ObjectUtil.isEmpty(fight)) { + return R.ok(null, "不存在的对局"); + } + if (fight.getStatus().equals(0)) { + return R.ok(null, "对局未开始"); + } + // P1#14: 校验调用者是否为参战者或创建者 + boolean isParticipant = fight.getUserId().equals(userId.intValue()); + if (!isParticipant && fight.getSeats() != null) { + isParticipant = fight.getSeats().stream() + .anyMatch(seat -> seat.getPlayerId() != null && seat.getPlayerId().equals(userId.intValue())); + } + if (!isParticipant) return R.fail("您不是该对局的参与者"); + if (fight.getStatus().equals(2) || fight.getStatus().equals(3)) { + return R.ok(fight, "对局已结束"); + } + + Boolean lock = false; + for (int i = 0; i < 4; i++) { + + lock = redisLock.tryLock(JOIN_FIGHT_END_LOCK + fightId, 2L, 7L, TimeUnit.SECONDS); + + if (!lock) { + continue; + } + break; + } + + if (!lock) return R.ok("系统繁忙。请稍后查看对战结果"); + + try { + // 校验时间 + LocalDateTime now = LocalDateTime.now(); + Timestamp beginTime = fight.getBeginTime(); + LocalDateTime beginTime1 = beginTime.toLocalDateTime(); + Duration duration = Duration.between(beginTime1, now); + Long second = duration.toMillis() / 1000; + Integer roundTime = fightRoundTime / 1000; + // if (fight.getRoundNumber() * roundTime > second.intValue()) return R.fail("对局尚未结束,请稍后重试。"); + + // 更新游戏状态 + Timestamp endTime = new Timestamp(System.currentTimeMillis()); + new LambdaUpdateChainWrapper<>(fightMapper) + .eq(TtFight::getId, fightId) + .eq(TtFight::getStatus, 1) + .set(TtFight::getStatus, 2) + .set(TtFight::getEndTime, endTime) + .update(); + + // 更新背包(机器人持有的饰品不入背包,仅供前端展示) + LambdaUpdateWrapper boxRecordsUpdate = new LambdaUpdateWrapper<>(); + boxRecordsUpdate + .eq(TtBoxRecords::getFightId, fightId) + .and(w -> w.isNull(TtBoxRecords::getHolderUserId) + .or().notInSql(TtBoxRecords::getHolderUserId, + "SELECT user_id FROM tt_user WHERE user_type = '" + UserType.ROBOT_USER.getCode() + "'")) + // .set(TtBoxRecords::getIsShow, 1); + .set(TtBoxRecords::getStatus, IN_PACKSACK_ON.getCode()); + boxRecordsService.update(boxRecordsUpdate); + + fight.setStatus(2); + fight.setEndTime(endTime); + + // 异步更新对局数据 + CompletableFuture.runAsync(() -> { + pushService.broadcastToChannel(SubscriptionManager.hallChannel(), WsResult.ok(SMsgKey.FIGHT_ROOM_UPDATE, buildRoomUpdateData(Arrays.asList(fight)), "对战房间最新信息")); + }, customThreadPoolExecutor); + + return R.ok(fight); + + } catch (Exception e) { + e.printStackTrace(); + log.warn("客户端【主动结束】游戏异常。"); + return R.ok(fight, "服务器繁忙。"); + } finally { + redisLock.unlock(JOIN_FIGHT_END_LOCK + fightId); + } + } + + public R seatrReadyCheck(Integer fightId, TtUser player) { + + TtFight fight = new LambdaQueryChainWrapper<>(fightMapper) + .eq(TtFight::getId, fightId) + // .eq(TtFight::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(fight)) return R.fail("不存在的对局。"); + if (fight.getStatus().equals(1)) return R.fail("对局进行中。"); + if (fight.getStatus().equals(2) || fight.getStatus().equals(3)) return R.fail("对局已结束。"); + + ObjectMapper objectMapper = new ObjectMapper(); + List seats = objectMapper.convertValue(fight.getSeats(), new com.fasterxml.jackson.core.type.TypeReference>() { + @Override + public Type getType() { + return super.getType(); + } + }); + fight.setSeats(seats); + + Map boxData = objectMapper.convertValue(fight.getBoxData(), new com.fasterxml.jackson.core.type.TypeReference>() { + @Override + public Type getType() { + return super.getType(); + } + }); + fight.setBoxData(boxData); + + return R.ok(fight); + } + + // 玩家准备游戏 + @Transactional(rollbackFor = Exception.class) + @Override + public R seatrReady(Integer fightId, TtUser player) { + + // 检查 + R check = seatrReadyCheck(fightId, player); + if (!check.getCode().equals(200)) return check; + + // fight + ObjectMapper objectMapper = new ObjectMapper(); + TtFight fight = objectMapper.convertValue(check.getData(), TtFight.class); + + if (MapUtils.isNotEmpty(fight.getBoxData())) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, fight.getBoxData().keySet()); + List boxes = boxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + fight.getBoxData().keySet().forEach(boxId -> { + Object value = fight.getBoxData().get(boxId); + FightBoxVO a = null; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + a = new FightBoxVO(map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO)); + } else { + a = (FightBoxVO) value; + a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO)); + } + fight.getBoxData().put(boxId, a); + }); + } + + // 更新座位状态 + boolean flag = false; + for (FightSeat seat : fight.getSeats()) { + + if (flag) break; + + // 空座位跳过,避免 NPE + if (seat.getPlayerId() == null) { + continue; + } + + //找到自己的座位 + if (seat.getPlayerId().equals(player.getUserId())) { + + String lock = JOIN_FIGHT_SEAT_READY_LOCK + fightId + ":" + seat.getCode(); + + Boolean tryLock = false; + for (int i = 0; i < 2; i++) { + tryLock = redisLock.tryLock(lock, 2L, 7L, TimeUnit.SECONDS); + if (tryLock) break; + } + if (!tryLock) return R.fail("系统繁忙,请稍后重试。"); + + // if (ObjectUtil.isEmpty(seat.ready())) return R.fail("座位状态异常。"); + // updateById(fight); + + try { + //座位准备 + // if (ObjectUtil.isEmpty(seat.ready())) return R.fail("座位状态异常。"); + if (seat.getStatus().equals(2)) return R.fail("已准备,请勿重复操作。"); + if (ObjectUtil.isEmpty(seat.ready())) return R.fail("座位状态(0)异常。"); + updateById(fight); + + // 账户结算 + R mapR = readyFightAccounting(fight, player); + if (R.isError(mapR)) { + seat.readyCancel().sitUp(); + updateById(fight); + log.info("对战{},用户{}加入扣款异常,踢出座位{}。", fightId, player.getUserId(), seat.getCode()); + return mapR; + } + + // 同步保存参与人员信息 + TtFightUser build = TtFightUser.builder() + .fightId(fightId) + .userId(player.getUserId()) + .joinPrice(fight.getBoxPriceTotal()) + .createTime(new Date()) + .updateTime(new Date()) + .build(); + fightUserService.save(build); + } catch (Exception e) { + log.error("fight happen exception is {}", e.getMessage(), e); + seat.readyCancel().sitUp(); + updateById(fight); + e.printStackTrace(); + } finally { + redisLock.unlock(lock); + } + + // 广播房间消息 + CompletableFuture.runAsync(() -> { + ArrayList fightBoxVOList = enrichFightBoxVOList(fight); + FightState waiting = fightStateManager.buildWaitingState(fight, fightBoxVOList); + pushService.broadcastToChannel(SubscriptionManager.fightChannel(fightId.longValue()), + WsResult.ok(SMsgKey.FIGHT_STATE, waiting, "对战房间最新信息")); + }, customThreadPoolExecutor); + + // 服务端兜底:全员已准备则事务提交后直接触发开局,不再依赖前端满员请求 + boolean allReady = fight.getSeats().stream() + .allMatch(s -> s.getStatus() != null && s.getStatus().equals(2)); + if (allReady) { + triggerFightBeginAfterCommit(fightId, player); + } + + flag = true; + return R.ok(); + } + } + + return R.fail("你不在此对局座位中"); + } + + /** + * 全员就绪后由服务端触发开局(事务提交后异步执行)。 + * fightBegin 内部有分布式锁与 status=0 条件更新,与前端满员请求重复触发也是幂等的。 + */ + private void triggerFightBeginAfterCommit(Integer fightId, TtUser player) { + Runnable task = () -> CompletableFuture.runAsync(() -> { + try { + R r = fightBegin(fightId, player); + log.info("对战{}全员就绪,服务端触发开局结果:{}", fightId, r == null ? "" : r.getMsg()); + } catch (Exception e) { + log.error("对战{}服务端触发开局异常:{}", fightId, e.getMessage(), e); + } + }, customThreadPoolExecutor); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + task.run(); + } + }); + } else { + task.run(); + } + } + + @Override + public R fightRoomExit(Integer fightId, TtUser player) { + + TtFight fight = new LambdaQueryChainWrapper<>(fightMapper) + .eq(TtFight::getId, fightId) + .eq(TtFight::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(fight)) return R.fail("对局已开始,不能退出。"); + + List seats = JSONUtil.toList(JSONUtil.toJsonStr(fight.getSeats()), FightSeat.class); + for (int i = 0; i < seats.size(); i++) { + + Integer playerId = seats.get(i).getPlayerId(); + if (ObjectUtil.isEmpty(playerId)) { + continue; + } + + if (playerId.equals(player.getUserId())) { + if (seats.get(i).getStatus().equals(2)) { + // seat.sitUp(); + // updateById(fight); + return R.ok("退出成功,消费不退回。"); + } else if (seats.get(i).getStatus().equals(1)) { + if (!seats.get(i).sitUp()) return R.fail("退出失败,请稍后重试。"); + fight.setSeats(seats); + updateById(fight); + // 广播房间消息 + CompletableFuture.runAsync(() -> { + List list = new LambdaQueryChainWrapper<>(fightMapper) + .eq(TtFight::getStatus, 0) + .orderByDesc(TtFight::getCreateTime) + .list(); + pushService.broadcastToChannel(SubscriptionManager.hallChannel(), WsResult.ok(SMsgKey.FIGHT_ROOM_UPDATE, buildRoomUpdateData(list), "对战房间最新信息")); + ArrayList fightBoxVOList = enrichFightBoxVOList(fight); + FightState waiting = fightStateManager.buildWaitingState(fight, fightBoxVOList); + pushService.broadcastToChannel(SubscriptionManager.fightChannel(fight.getId().longValue()), + WsResult.ok(SMsgKey.FIGHT_STATE, waiting, "对战房间最新信息")); + }, customThreadPoolExecutor); + return R.ok("退出成功。"); + } else { + return R.fail("异常的座位状态0"); + } + } + } + + return R.fail("状态异常,你不在本对局中。"); + + } + + @Override + public List getFightList(FightOnMyOwnParam param) { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(ObjectUtil.isNotEmpty(param.getFightId()), TtFight::getId, param.getFightId()) + .eq(ObjectUtil.isNotEmpty(param.getModel()), TtFight::getModel, param.getModel()) + .ge(ObjectUtil.isNotNull(param.getMinPrice()), TtFight::getBoxPriceTotal, param.getMinPrice()) + .le(ObjectUtil.isNotNull(param.getMaxPrice()), TtFight::getBoxPriceTotal, param.getMaxPrice()) + .in(ObjectUtil.isNotEmpty(param.getStatusList()), TtFight::getStatus, param.getStatusList()); + // 我参与的 + if (ObjectUtil.isNotNull(param.getPlayerId())) { + List myOwnFights = fightUserMapper.myOwnFights(param.getPlayerId()); + if (myOwnFights.size() > 0) { + wrapper.in(TtFight::getId, myOwnFights); + } else { + wrapper.eq(TtFight::getId, "non_existing_value"); + } + } + // 房间号 id 精确匹配(keyword 仅支持对战房间号) + if (ObjectUtil.isNotEmpty(param.getKeyword())) { + try { + wrapper.eq(TtFight::getId, Integer.valueOf(param.getKeyword().trim())); + } catch (NumberFormatException e) { + // keyword 非纯数字,直接返回空结果 + return Collections.emptyList(); + } + } + wrapper.orderByDesc(TtFight::getCreateTime); + + // 分页查询,默认第1页每页20条 + int pageNum = (param.getPage() != null && param.getPage() > 0) ? param.getPage() : 1; + int pageSize = (param.getSize() != null && param.getSize() > 0) ? param.getSize() : 20; + Page pageInfo = new Page<>(pageNum, pageSize); + pageInfo.setOptimizeCountSql(false); + + List page = this.page(pageInfo, wrapper).getRecords(); + if (CollectionUtils.isEmpty(page)) { + return Collections.emptyList(); + } + + List boxIds = new ArrayList<>(); + for (TtFight fight : page) { + if (fight == null || MapUtils.isEmpty(fight.getBoxData())) { + continue; + } + for (Map.Entry entry : fight.getBoxData().entrySet()) { + Object value = entry.getValue(); + if (value == null) { + continue; + } + Integer boxId; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + boxId = map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null; + } else { + boxId = ((FightBoxVO) value).getBoxId(); + } + if (boxId == null) { + continue; + } + boxIds.add(boxId); + } + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxIds); + List boxes = boxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + Map boxNameMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, b -> b.getBoxName() != null ? b.getBoxName() : "")); + for (TtFight fight : page) { + if (fight == null || MapUtils.isEmpty(fight.getBoxData())) { + continue; + } + for (Map.Entry entry : fight.getBoxData().entrySet()) { + Object value = entry.getValue(); + if (value == null) { + continue; + } + FightBoxVO vo; + Integer boxId; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + vo = new FightBoxVO(map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), new BigDecimal(0))); + vo.setBoxName(boxNameMap.getOrDefault(vo.getBoxId(), "")); + } else { + vo = (FightBoxVO) value; + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), new BigDecimal(0))); + vo.setBoxName(boxNameMap.getOrDefault(vo.getBoxId(), "")); + } + fight.getBoxData().put(vo.getBoxId().toString(), vo); + } + } + + var result = page.stream().map(fight -> { + ApiFightListDataVO build = ApiFightListDataVO.builder().build(); + BeanUtil.copyProperties(fight, build); + build.setBoxData(JSONUtil.toJsonStr(fight.getBoxData())); + build.setSeats(fight.getSeats()); + build.setRoundNumber(fight.getRoundNumber()); + // 设置 winnerIdList 以便 getWinnerIds() getter 正确返回 + if (fight.getWinnerIds() != null && !fight.getWinnerIds().isEmpty()) { + build.setWinnerIdList(JSONUtil.toJsonStr(fight.getWinnerIds())); + } + return build; + }).collect(Collectors.toList()); + + List fightIds = result.stream().filter(Objects::nonNull).map(ApiFightListDataVO::getId).collect(Collectors.toList()); + List voList = apiFightMapper.fightOrnamentsPrice(fightIds); + Map fightIdToPrice = voList.stream() + .filter(Objects::nonNull) // 过滤掉空值 + .collect(Collectors.toMap( + ApiFightTenTopVo::getId, + ApiFightTenTopVo::getTotalOrnamentsPrice + )); + for (ApiFightListDataVO vo : result) { + vo.setTotalOrnamentsPrice(fightIdToPrice.getOrDefault(vo.getId(), BigDecimal.ZERO)); + } + + // 批量填充每个玩家开出的饰品总和(seats[].awardTotalPrices) + List holderPriceList = apiFightMapper.holderOrnamentsPrice(fightIds); + // 构建 fightId:holderUserId -> totalOrnamentsPrice 映射 + Map holderPriceMap = new HashMap<>(); + for (ApiFightTenTopVo hp : holderPriceList) { + if (hp != null && hp.getId() != null && hp.getUserId() != null) { + holderPriceMap.put(hp.getId() + ":" + hp.getUserId(), hp.getTotalOrnamentsPrice()); + } + } + for (ApiFightListDataVO vo : result) { + if (vo.getSeats() == null) continue; + List winIds = vo.getWinnerIds() != null ? vo.getWinnerIds() : Collections.emptyList(); + BigDecimal winTotal = BigDecimal.ZERO; + BigDecimal loseTotal = BigDecimal.ZERO; + for (FightSeat seat : vo.getSeats()) { + if (seat.getPlayerId() != null) { + BigDecimal playerPrice = holderPriceMap.getOrDefault(vo.getId() + ":" + seat.getPlayerId(), BigDecimal.ZERO); + seat.setAwardTotalPrices(playerPrice); + if (winIds.contains(seat.getPlayerId())) { + winTotal = winTotal.add(playerPrice); + } else { + loseTotal = loseTotal.add(playerPrice); + } + } + } + vo.setWinnerTotalPrice(winTotal); + vo.setLoserTotalPrice(loseTotal); + } + + enrichOwnerNickNamesForVO(result); + + // 批量填充 seats 中的 vipLevel + List allPlayerIds = result.stream() + .filter(v -> v.getSeats() != null) + .flatMap(v -> v.getSeats().stream()) + .map(FightSeat::getPlayerId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (!allPlayerIds.isEmpty()) { + List users = new LambdaQueryChainWrapper<>(ttUserMapper) + .in(TtUser::getUserId, allPlayerIds) + .select(TtUser::getUserId, TtUser::getVipLevel) + .list(); + Map vipMap = users.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtUser::getUserId, u -> u.getVipLevel() != null && u.getVipLevel() > 0 ? u.getVipLevel() - 1 : 0, (a, b) -> a)); + for (ApiFightListDataVO vo : result) { + if (vo.getSeats() == null) continue; + for (FightSeat seat : vo.getSeats()) { + if (seat.getPlayerId() != null) { + seat.setVipLevel(vipMap.getOrDefault(seat.getPlayerId(), 0)); + } + } + } + } + + return result; + } + + @Override + public R fightDetail(FightDetailParam param) { + + TtFight fight = this.getById(param.getFightId()); + if (!fight.getStatus().equals(2) && !fight.getStatus().equals(3)) { + return R.fail("对局未结束"); + } + + TtFightVO vo = new TtFightVO(); + BeanUtil.copyProperties(fight, vo); + Map boxData = fight.getBoxData(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxData.keySet()); + List boxes = boxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + boxData.keySet().forEach(boxId -> { + Object value = boxData.get(boxId); + FightBoxVO a = null; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + a = new FightBoxVO(map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO)); + } else { + a = (FightBoxVO) value; + a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO)); + } + boxData.put(boxId, a); + }); + vo.setBoxData(boxData); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtBoxRecords::getFightId, param.getFightId()); + + List list = ttBoxRecordsService.list(wrapper); + vo.setFightResult(list); + + return R.ok(vo); + } + + @Override + public R earlierHistory(FightDetailParam param) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TtFight::getId, param.getFightId()); + + TtFight one = this.getOne(queryWrapper); + Date createTime = one.getCreateTime(); + + Page pageInfo = new Page<>(param.getPage(), param.getSize()); + pageInfo.setOptimizeCountSql(false); + queryWrapper.clear(); + queryWrapper + .le(TtFight::getCreateTime, createTime) + // .and(qw->qw.eq(TtFight::getStatus,0)) + .eq(TtFight::getStatus, 0); + + pageInfo = this.page(pageInfo, queryWrapper); + + return R.ok(pageInfo.getRecords()); + } + + @Override + public List getFightRankingByDate(String date) { + return apiFightMapper.getFightRankingByDate(date); + } + + // 加入对战账户结算 + public R readyFightAccounting(TtFight fight, TtUser player) { + // 扣款 + BigDecimal totalMoney = fight.getBoxPriceTotal(); + globalRewardPoolService.incrementalBet(totalMoney); + R r = userService.updateUserAccount(player.getUserId(), totalMoney.negate(), TtAccountRecordSource.GAME_TYPE_02); + if (R.isError(r)) { + return R.fail(r.getMsg()); + } + + return R.ok(); + } + + public List computerFight(TtFight fight, TtBox box, Integer number) { + + // 计算结果 + ArrayList result = new ArrayList<>(); + List seats = fight.getSeats(); + for (FightSeat seat : seats) { + TtUser player = userService.getById(seat.getPlayerId()); + List ttBoxRecords = apiBindBoxService.openBoxArithmetic(fight.getId(), player, box, number); + // 补充图片 + ttBoxRecords.stream().forEach(boxRecord -> { + TtOrnament ornament = ttOrnamentsZBTService.getById(boxRecord.getOrnamentId()); + boxRecord.setImageUrl(ornament.getImageUrl()); + }); + result.addAll(ttBoxRecords); + } + return result; + } + + public List computerFight1(TtFight fight) { + + List result = new ArrayList<>(); + + List seats = fight.getSeats(); + Map boxs = fight.getBoxData(); + for (FightSeat seat : seats) { + Integer roundNumber = 1; + for (String boxId : boxs.keySet()) { + TtBox box = boxMapper.selectById(boxId); + // 一种箱子一个人的所有结果 + TtUser player = userService.getById(seat.getPlayerId()); + List ttBoxRecords = apiBindBoxService.openBoxArithmetic(fight.getId(), player, box, boxs.get(boxId).getNumber()); + // 补充图片 + for (TtBoxRecords boxRecord : ttBoxRecords) { + TtOrnament ornament = ttOrnamentsZBTService.getById(boxRecord.getOrnamentId()); + boxRecord.setImageUrl(ornament.getImageUrl()); + boxRecord.setFightRoundNumber(roundNumber); + roundNumber++; + } + result.addAll(ttBoxRecords); + } + } + + return result; + } + + // 新开箱算法 + public List newComputerFight(TtFight fight) { + + List result = new ArrayList<>(); + + Map boxs = fight.getBoxData(); + // 抽奖人列表 + ArrayList userList = new ArrayList<>(); + for (FightSeat seat : fight.getSeats()) { + userList.add(seat.getPlayerId()); + } + + Integer round = 1; + for (String boxId : boxs.keySet()) { + // 获取宝箱详细信息 + TtBox box = boxMapper.selectById(boxId); + for (int i = 0; i < boxs.get(boxId).getNumber(); i++) { + // 一个宝箱一回合(传入对战模式:机器人按模式选欧皇/非酋池,真实玩家/主播不受影响) + List roundResult = roundLottery(round, box, userList, fight.getModel()); + if (ObjectUtil.isEmpty(roundResult) || roundResult.isEmpty()) return null; + result.addAll(roundResult); + round++; + } + + // 统计宝箱历史开箱数量 + new LambdaUpdateChainWrapper<>(boxMapper) + .eq(TtBox::getBoxId, boxId) + .set(TtBox::getOpenNum, box.getOpenNum() + boxs.get(boxId).getNumber()) + .update(); + } + + // 补充信息 + result.stream().forEach(item -> { + item.setFightId(fight.getId()); + }); + + // 保存信息 + return result; + } + + // 抽奖(一回合),fightModel: "0"=欧皇 "1"=非酋,仅影响机器人选池(LotteryMachine 内部判断) + public List roundLottery(Integer round, TtBox box, ArrayList userList, String fightModel) { + + ArrayList result = new ArrayList<>(); + + // 循环所有人 + for (Integer uid : userList) { + + // 空座位(playerId 为 null)跳过,避免 getById(null) 及抽奖 NPE + if (uid == null) { + log.warn("对战抽奖跳过空座位"); + continue; + } + TtUser user = userService.getById(uid); + if (user == null) { + log.warn("对战抽奖跳过无效用户{}", uid); + continue; + } + + // 抽奖 + String ornamentId = lotteryMachine.singleLottery(user, box, fightModel); + if (StringUtils.isBlank(ornamentId)) { + // 奖池为空/未配置对应爆率池(如机器人欧皇 robot_odds_lucky 未配置)时, + // 记为“抽空”(ornamentId=null),fightBegin 已处理抽空场景; + // 若整局返回 null 会导致 fightBegin 抛 NPE、房间永久卡在准备中 + log.warn("用户{}在宝箱{}抽奖为空(奖池可能未配置对应爆率),记为抽空", + user.getUserId(), box.getBoxId()); + result.add(TtBoxRecords.builder() + .userId(user.getUserId()) + .boxId(box.getBoxId()) + .boxName(box.getBoxName()) + .boxPrice(box.getPrice()) + .ornamentId(null) + .ornamentsPrice(BigDecimal.ZERO) + .holderUserId(user.getUserId()) + .source(TtboxRecordSource.FIGHT.getCode()) + .fightRoundNumber(round) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .status(IN_PACKSACK_OFF.getCode()) + .build()); + continue; + } + + // 饰品详情 + TtOrnamentsA ornamentsData = bindBoxMapper.ornamentsInfo(box.getBoxId(), ornamentId); + + // 抽奖成功,构建开箱记录数据 + TtBoxRecords boxRecord = TtBoxRecords.builder() + .userId(user.getUserId()) + .boxId(box.getBoxId()) + .boxName(box.getBoxName()) + .boxPrice(box.getPrice()) + .ornamentId(Long.valueOf(ornamentId)) + // .marketHashName(ornamentsData.) + .ornamentName(ornamentsData.getShortName()) + .imageUrl(ornamentsData.getImageUrl()) + .ornamentsPrice(ornamentsData.getUsePrice()) + .ornamentsLevelId(ornamentsData.getOrnamentsLevelId()) + .ornamentLevelImg(ornamentsData.getLevelImg()) + .holderUserId(user.getUserId()) + .source(TtboxRecordSource.FIGHT.getCode()) + .status(IN_PACKSACK_ON.getCode()) + .fightRoundNumber(round) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + // .fightId() 后续补充 + // .isShow(0) + .status(IN_PACKSACK_OFF.getCode()) + .build(); + result.add(boxRecord); + + } + + return result; + } + + // 座位是否人满 + public Boolean seatsIsFull(List seats) { + boolean f = true; + for (FightSeat seat : seats) { + if (seat.getStatus().equals(0)) return !f; + } + return f; + } + + + @Override + public List getFightBoxList(Integer boxTypeId) { + + List boxList = apiFightMapper.getFightBoxList(boxTypeId); + + // 补充物品信息 + // for (TtBoxA box : boxList){ + // List list = new LambdaQueryChainWrapper<>(boxOrnamentsMapper) + // .eq(TtBoxOrnaments::getBoxId, box.getBoxId()) + // .list(); + // } + + // 过滤空箱子 + List collect = boxList.stream().filter(item -> { + + List list = new LambdaQueryChainWrapper<>(boxOrnamentsMapper) + .eq(TtBoxOrnaments::getBoxId, item.getBoxId()) + .list(); + if (ObjectUtil.isEmpty(list) || list.isEmpty()) return false; + + boolean flag = false; + for (TtBoxOrnaments ornaments : list) { + if (ornaments.getRealOdds() > 0) { + return !flag; + } + } + return flag; + + }).collect(Collectors.toList()); + + List ttBoxVOS = apiBindBoxService.groupByBoxType(collect); + + return ttBoxVOS; + } + + @Override + public List getFightList(String model, String status, Integer userId, Integer fightId) { + List vos = apiFightMapper.getFightList(model, status, userId, fightId); + if (CollectionUtils.isEmpty(vos)) { + return vos; + } + + List fightIds = vos.stream().filter(Objects::nonNull).map(ApiFightListDataVO::getId).collect(Collectors.toList()); + List voList = apiFightMapper.fightOrnamentsPrice(fightIds); + Map fightIdToPrice = voList.stream() + .filter(Objects::nonNull) // 过滤掉空值 + .collect(Collectors.toMap( + ApiFightTenTopVo::getId, + ApiFightTenTopVo::getTotalOrnamentsPrice + )); + Set boxIds = new HashSet<>(); + for (ApiFightListDataVO vo : vos) { + Map boxData = JSONUtil.toBean(vo.getBoxData(), new cn.hutool.core.lang.TypeReference>() { + }, false); + if (MapUtils.isEmpty(boxData)) { + continue; + } + for (Map.Entry entry : boxData.entrySet()) { + if (entry == null || entry.getValue() == null || entry.getValue().getBoxId() == null) { + continue; + } + boxIds.add(entry.getValue().getBoxId()); + } + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxIds); + List boxes = boxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + Map boxNameMap2 = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, b -> b.getBoxName() != null ? b.getBoxName() : "")); + for (ApiFightListDataVO vo : vos) { + Map boxData = JSONUtil.toBean(vo.getBoxData(), new cn.hutool.core.lang.TypeReference>() { + }, false); + if (MapUtils.isNotEmpty(boxData)) { + for (Map.Entry entry : boxData.entrySet()) { + if (entry == null || entry.getValue() == null || entry.getValue().getBoxId() == null) { + continue; + } + entry.getValue().setPrice(boxMap.getOrDefault(entry.getValue().getBoxId(), new BigDecimal(0))); + entry.getValue().setBoxName(boxNameMap2.getOrDefault(entry.getValue().getBoxId(), "")); + } + } + vo.setBoxData(JSONUtil.toJsonStr(boxData)); + vo.setTotalOrnamentsPrice(fightIdToPrice.getOrDefault(vo.getId(), BigDecimal.ZERO)); + } + + enrichOwnerNickNamesForVO(vos); + + return vos; + } + + @Override + public FightResultDataVO getFightRecord(Integer fightId, Integer round, Integer rounds) { + FightResultDataVO fightResultDataVO = new FightResultDataVO(); + fightResultDataVO.setFightId(fightId); + + TtFightResult ttFightResult = fightResultMapper.selectOne(new QueryWrapper() + .eq("fight_id", fightId)); + if (ObjectUtil.isEmpty(ttFightResult)) return null; + // 对局正式开始时间 - 客户端获取本回合结果时间 + long hhsj = new Date().getTime() - ttFightResult.getCreateTime().getTime(); + + if (round != null) { + fightResultDataVO.setRound(round); + } else { + // 当前回合数 + round = (int) hhsj / 6000 + 1; + if (round > rounds) { + // 时间已过,房间关闭 + return fightResultDataVO; + } else { + fightResultDataVO.setRound(round); + } + } + List playerGainsOrnamentsDataVOList = new ArrayList<>(); + + // 计算当前回合数奖品 + List list = fightUserMapper + .selectList(new QueryWrapper() + .select("fight_id", "user_id", "join_seat_num") + .eq("fight_id", fightId) + .groupBy("fight_id", "user_id", "join_seat_num")); + + for (int i = 0; i < list.size(); i++) { + + PlayerGainsOrnamentsDataVO playerGainsOrnamentsDataVO = new PlayerGainsOrnamentsDataVO(); + // 获取奖品信息 + List ttBoxRecords = ttBoxRecordsService.list(new QueryWrapper() + .eq("user_id", list.get(i).getUserId()) + .eq("fight_id", list.get(i).getFightId())); + TtOrnament ttOrnament = ttOrnamentsZBTService.getById(ttBoxRecords.get(round - 1).getOrnamentId()); + + playerGainsOrnamentsDataVO.setUserId(ttBoxRecords.get(round - 1).getUserId()); + playerGainsOrnamentsDataVO.setJoinSeatNum(list.get(i).getJoinSeatNum()); + + UserPackSackDataVO userPackSackDataVO = new UserPackSackDataVO(); + userPackSackDataVO.setImageUrl(ttOrnament.getImageUrl()); + userPackSackDataVO.setOrnamentsPrice(ttOrnament.getPrice()); + userPackSackDataVO.setItemName(ttOrnament.getName()); + userPackSackDataVO.setShortName(ttOrnament.getShortName()); + + if (ttOrnament.getQuality() != null) ttOrnament.setQuality("1"); + + TtOrnamentsLevel ttOrnamentsLevel = ttOrnamentsLevelService.getById(ttOrnament.getQuality()); + userPackSackDataVO.setLevelImg(ttOrnamentsLevel.getLevelImg()); + + playerGainsOrnamentsDataVO.setOrnamentsData(userPackSackDataVO); + + playerGainsOrnamentsDataVOList.add(playerGainsOrnamentsDataVO); + } + fightResultDataVO.setPlayerGainsOrnamentsData(playerGainsOrnamentsDataVOList); + + return fightResultDataVO; + } + + private void sendResult(TtFight ttFight, Integer rounds) { + + log.info("sendResult"); + log.info("对局信息 " + ttFight.getPlayerNum()); + + int num = 25; + while (num > 0) { + try { + List ttBoxRecordsList = new LambdaQueryChainWrapper<>(boxRecordsService.getBaseMapper()) + .eq(TtBoxRecords::getStatus, "4") + .eq(TtBoxRecords::getSource, "1") + .eq(TtBoxRecords::getFightId, ttFight.getId()) + .list(); + + Map boxDataJsonString = ttFight.getBoxData(); + if (StringUtils.isEmpty(boxDataJsonString)) return; + + List boxDataList = JSONObject.parseObject(JSON.toJSONString(boxDataJsonString), new TypeReference>() { + }); + + int boxNumTotal = boxDataList.stream().mapToInt(BoxDataBodyA::getBoxNum).sum(); + + // 等待开箱结果 + if (boxNumTotal * ttFight.getPlayerNum() > ttBoxRecordsList.size()) { + log.info("等待开箱结果 " + ttBoxRecordsList.size()); + // TimeUnit.MILLISECONDS.sleep(1000); + TimeUnit.MILLISECONDS.sleep(400); + // TimeUnit.MILLISECONDS.sleep(100); + num--; + continue; + } + + Map> boxRecordsGroupingByUserId = ttBoxRecordsList.stream().collect(Collectors.groupingBy(TtBoxRecords::getUserId)); + List playerGainsOrnamentsDataList = new ArrayList<>(); + for (Integer userId : boxRecordsGroupingByUserId.keySet()) { + PlayerGainsOrnamentsDataVO playerGainsOrnamentsData = getPlayerGainsOrnamentsData(userId, ttFight.getId()); + playerGainsOrnamentsDataList.add(playerGainsOrnamentsData); + } + FightResultDataVO fightResultData = new FightResultDataVO(); + fightResultData.setFightId(ttFight.getId()); + fightResultData.setPlayerGainsOrnamentsData(playerGainsOrnamentsDataList); + List winnerGainsOrnamentsData = allotResult(boxRecordsGroupingByUserId, ttFight); + fightResultData.setWinnerGainsOrnamentsData(winnerGainsOrnamentsData); + new LambdaUpdateChainWrapper<>(fightMapper).eq(TtFight::getId, ttFight.getId()).set(TtFight::getStatus, "1") + .set(TtFight::getUpdateTime, new Date()).update(); + + // 关闭对战房间 + TtFightResult ttFightResult = fightResultMapper.selectOne(new QueryWrapper().eq("fight_id", ttFight.getId())); + if (ttFightResult != null) { + + // rabbitTemplate.convertAndSend(FightQueueConfig.FIGHT_EXCHANGE, FightQueueConfig.FIGHT_ROUTING_KEY, ttFight.getId(), + // message -> { + // // 注意这里时间可以使long,而且是设置header + // message.getMessageProperties().setDelay((rounds * 6000)); + // return message; + // } + // ); + + log.info(ttFight.getId() + "将在{}ms后执行", rounds * 6000); +// rabbitProduc.sendDelayMessage(ttRoll.getId().intValue() + "", difference); + + } + + + new LambdaUpdateChainWrapper<>(fightUserMapper).eq(TtFightUser::getFightId, ttFight.getId()).set(TtFightUser::getStatus, "2") + .set(TtFightUser::getUpdateTime, new Date()).update(); + pushService.broadcastToAll(WsResult.ok(SMsgKey.FIGHT_RESULT_DATA, fightResultData, "对战结果数据")); + TtFightResult fightResult = TtFightResult.builder().fightId(ttFight.getId()).createTime(new Date()).fightResult(JSON.toJSONString(fightResultData)).build(); + + log.info("=====================保存对战房间" + ttFight.getId() + "结果==================="); + fightResultMapper.insert(fightResult); + return; + } catch (Exception e) { + return; + } + } + } + + private List allotResult(Map> boxRecordsGroupingByUserId, TtFight ttFight) { + Map> dataA = new HashMap<>(); + for (Map.Entry> entry : boxRecordsGroupingByUserId.entrySet()) { + Integer userId = entry.getKey(); + List boxRecordsList = entry.getValue(); + TtFightUser ttFightUser = new LambdaQueryChainWrapper<>(fightUserMapper).eq(TtFightUser::getFightId, ttFight.getId()) + .eq(TtFightUser::getUserId, userId).eq(TtFightUser::getStatus, "0").one(); + PlayerGainsOrnamentsDataVO playerGainsOrnamentsData = getPlayerGainsOrnamentsData(userId, ttFight.getId()); + Map data = new HashMap<>(); + BigDecimal reduce = boxRecordsList.stream().map(TtBoxRecords::getOrnamentsPrice).reduce(BigDecimal.ZERO, BigDecimal::add); + data.put("priceTotal", JSON.toJSONString(reduce)); + data.put("userId", JSON.toJSONString(userId)); + data.put("playerGainsOrnamentsData", JSON.toJSONString(playerGainsOrnamentsData)); + data.put("boxRecordsList", JSON.toJSONString(boxRecordsList)); + dataA.put(ttFightUser.getJoinSeatNum(), data); + } + Map balanceMap = dataA.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> JSONObject + .parseObject(entry.getValue().get("priceTotal"), new TypeReference() { + }))); + BigDecimal balanceA = balanceMap.values().stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO); + List maxJoinSeatNumList = balanceMap.entrySet().stream().filter(entry -> entry.getValue().equals(balanceA)) + .map(Map.Entry::getKey).collect(Collectors.toList()); + BigDecimal balanceB = balanceMap.values().stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO); + List minJoinSeatNumList = balanceMap.entrySet().stream().filter(entry -> entry.getValue().equals(balanceB)) + .map(Map.Entry::getKey).collect(Collectors.toList()); + Map> collectA = dataA.entrySet().stream().filter(entry -> !maxJoinSeatNumList.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + Map> collectB = dataA.entrySet().stream().filter(entry -> !minJoinSeatNumList.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + if ("0".equals(ttFight.getModel())) { + return singlePlayer(ttFight, dataA, maxJoinSeatNumList, collectA); + } else if ("1".equals(ttFight.getModel())) { + return singlePlayer(ttFight, dataA, minJoinSeatNumList, collectB); + } else if ("2".equals(ttFight.getModel())) { + if (ttFight.getPlayerNum() == 4 && dataA.size() == 4) { + return null; + } + } + return null; + } + + private List singlePlayer(TtFight ttFight, Map> dataTabulate, + List joinSeatNumList, Map> collect) { + if (ttFight.getPlayerNum() == 2 && dataTabulate.size() == 2) { + if (joinSeatNumList.size() == 1) return onePersonWin(ttFight, dataTabulate, joinSeatNumList, collect); + else if (joinSeatNumList.size() == 2) return dogFall(ttFight, dataTabulate, joinSeatNumList); + } else if (ttFight.getPlayerNum() == 3 && dataTabulate.size() == 3) { + if (joinSeatNumList.size() == 1) return onePersonWin(ttFight, dataTabulate, joinSeatNumList, collect); + else if (joinSeatNumList.size() == 2) { + List playerGainsOrnamentsDataVOList = dogFall(ttFight, dataTabulate, joinSeatNumList); + divideEquallyLosePrice(collect, playerGainsOrnamentsDataVOList); + return playerGainsOrnamentsDataVOList; + } else if (joinSeatNumList.size() == 3) return dogFall(ttFight, dataTabulate, joinSeatNumList); + } else if (ttFight.getPlayerNum() == 4 && dataTabulate.size() == 4) { + if (joinSeatNumList.size() == 1) return onePersonWin(ttFight, dataTabulate, joinSeatNumList, collect); + else if (joinSeatNumList.size() == 2) { + List playerGainsOrnamentsDataVOList = dogFall(ttFight, dataTabulate, joinSeatNumList); + List losePriceList = collect.values().stream() + .map(stringStringMap -> JSONObject.parseObject(stringStringMap.get("priceTotal"), new TypeReference() { + })) + .collect(Collectors.toList()); + if (losePriceList.get(0).compareTo(losePriceList.get(1)) == 0) { + List playerGainsOrnamentsData = collect.values().stream() + .map(stringStringMap -> JSONObject.parseObject(stringStringMap.get("playerGainsOrnamentsData"), + new TypeReference() { + })) + .collect(Collectors.toList()); + for (int i = 0; i < playerGainsOrnamentsData.size(); i++) { + playerGainsOrnamentsDataVOList.get(i).getOrnamentsDataList().addAll(playerGainsOrnamentsData.get(i).getOrnamentsDataList()); + new LambdaUpdateChainWrapper<>(boxRecordsService.getBaseMapper()).eq(TtBoxRecords::getFightId, ttFight.getId()) + .eq(TtBoxRecords::getUserId, playerGainsOrnamentsData.get(i).getUserId()).set(TtBoxRecords::getStatus, "0") + .set(TtBoxRecords::getUpdateTime, new Date()).set(TtBoxRecords::getHolderUserId, playerGainsOrnamentsDataVOList.get(i) + .getUserId()).update(); + } + } else { + divideEquallyLosePrice(collect, playerGainsOrnamentsDataVOList); + } + return playerGainsOrnamentsDataVOList; + } else if (joinSeatNumList.size() == 3) { + List playerGainsOrnamentsDataVOList = dogFall(ttFight, dataTabulate, joinSeatNumList); + divideEquallyLosePrice(collect, playerGainsOrnamentsDataVOList); + return playerGainsOrnamentsDataVOList; + } else if (joinSeatNumList.size() == 4) return dogFall(ttFight, dataTabulate, joinSeatNumList); + } + return null; + } + + private void divideEquallyLosePrice(Map> collect, + List playerGainsOrnamentsDataVOList) { + BigDecimal reduce = collect.values().stream().map(stringStringMap -> JSONObject.parseObject(stringStringMap + .get("priceTotal"), new TypeReference() { + })).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal divide = reduce.divide(new BigDecimal(playerGainsOrnamentsDataVOList.size()), 2, RoundingMode.HALF_UP); + for (PlayerGainsOrnamentsDataVO playerGainsOrnamentsDataVO : playerGainsOrnamentsDataVOList) { + userService.updateUserAccount(playerGainsOrnamentsDataVO.getUserId(), divide, TtAccountRecordSource.GAME_TYPE_02_AWARD); + } + List boxRecordsList = new ArrayList<>(); + for (Map value : collect.values()) { + boxRecordsList.addAll(JSONObject.parseObject(value.get("boxRecordsList"), new TypeReference>() { + })); + } + boxRecordsList = boxRecordsList.stream().peek(ttBoxRecords -> { + ttBoxRecords.setStatus(RESOLVE.getCode()); + ttBoxRecords.setUpdateTime(new Date()); + }).collect(Collectors.toList()); + boxRecordsService.updateBatchById(boxRecordsList, 1); + } + + private List dogFall(TtFight ttFight, + Map> dataTabulate, + List joinSeatNumList) { + List result = new ArrayList<>(); + for (Integer i : joinSeatNumList) { + PlayerGainsOrnamentsDataVO playerGainsOrnamentsData = JSONObject.parseObject( + dataTabulate.get(i).get("playerGainsOrnamentsData"), + new TypeReference() { + }); + result.add(playerGainsOrnamentsData); + new LambdaUpdateChainWrapper<>(boxRecordsService.getBaseMapper()).eq(TtBoxRecords::getFightId, ttFight.getId()) + .eq(TtBoxRecords::getUserId, playerGainsOrnamentsData.getUserId()).set(TtBoxRecords::getStatus, "0") + .set(TtBoxRecords::getUpdateTime, new Date()).set(TtBoxRecords::getHolderUserId, playerGainsOrnamentsData.getUserId()).update(); + } + return result; + } + + private List onePersonWin(TtFight ttFight, + Map> dataTabulate, + List joinSeatNumList, + Map> collect) { + List result = new ArrayList<>(); + List losePackSackDataList = new ArrayList<>(); + List> loseOrnamentsDataList = collect.values().stream().map(stringStringMap -> JSONObject.parseObject( + stringStringMap.get("playerGainsOrnamentsData"), new TypeReference() { + }).getOrnamentsDataList()) + .collect(Collectors.toList()); + for (List loseOrnamentsData : loseOrnamentsDataList) { + losePackSackDataList.addAll(loseOrnamentsData); + } + PlayerGainsOrnamentsDataVO playerGainsOrnamentsData = JSONObject.parseObject(dataTabulate.get( + joinSeatNumList.get(0)).get("playerGainsOrnamentsData"), + new TypeReference() { + }); + playerGainsOrnamentsData.getOrnamentsDataList().addAll(losePackSackDataList); + result.add(playerGainsOrnamentsData); + for (Map.Entry> entry : dataTabulate.entrySet()) { + Integer userId = JSONObject.parseObject(entry.getValue().get("userId"), new TypeReference() { + }); + new LambdaUpdateChainWrapper<>(boxRecordsService.getBaseMapper()).eq(TtBoxRecords::getFightId, ttFight.getId()) + .eq(TtBoxRecords::getUserId, userId).set(TtBoxRecords::getStatus, "0").set(TtBoxRecords::getUpdateTime, new Date()) + .set(TtBoxRecords::getHolderUserId, playerGainsOrnamentsData.getUserId()).update(); + } + return result; + } + + private PlayerGainsOrnamentsDataVO getPlayerGainsOrnamentsData(Integer userId, Integer fightId) { + TtUser ttUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()).eq(TtUser::getUserId, userId).one(); + TtFightUser ttFightUser = new LambdaQueryChainWrapper<>(fightUserMapper).eq(TtFightUser::getFightId, fightId) + .eq(TtFightUser::getUserId, userId).eq(TtFightUser::getStatus, "0").one(); + List ornamentsDataList = apiFightMapper.selectOrnamentsDataListByUserIdAndFightId(userId, fightId); + PlayerGainsOrnamentsDataVO playerGainsOrnamentsData = new PlayerGainsOrnamentsDataVO(); + playerGainsOrnamentsData.setUserId(userId); + playerGainsOrnamentsData.setUserName(ttUser.getUserName()); + playerGainsOrnamentsData.setNickName(ttUser.getNickName()); + playerGainsOrnamentsData.setAvatar(ttUser.getAvatar()); + playerGainsOrnamentsData.setOrnamentsDataList(ornamentsDataList); + playerGainsOrnamentsData.setJoinSeatNum(ttFightUser.getJoinSeatNum()); + return playerGainsOrnamentsData; + } + + private void openBox(TtFight ttFight, TtUser ttUser) { + log.info("开箱"); + String boxDataJsonString = JSON.toJSONString(ttFight.getBoxData()); + List boxDataList = JSONObject.parseObject(boxDataJsonString, new TypeReference>() { + }); + List boxRecordsList = new ArrayList<>(); + + for (BoxDataBodyA boxData : boxDataList) { + TtBox ttBox = new LambdaQueryChainWrapper<>(boxMapper) + .eq(TtBox::getBoxId, boxData.getBoxId()) + .eq(TtBox::getIsFight, "0") + .eq(TtBox::getStatus, "0") + .one(); + if (StringUtils.isNull(ttBox)) throw new RuntimeException("数据异常!"); + + // 计算开箱结果、保存开箱记录、返回开箱记录 + List ttBoxRecords = apiBindBoxService.addBoxRecord(ttUser, ttBox, boxData.getBoxNum()); + boxRecordsList.addAll(ttBoxRecords); + + // 统计箱子的历史开箱次数 + ttBox.setOpenNum(ttBox.getOpenNum() + boxData.getBoxNum()); + boxMapper.updateById(ttBox); + } + + if (boxRecordsList.isEmpty()) throw new RuntimeException("数据异常!"); + + boxRecordsList = boxRecordsList.stream().peek(ttBoxRecords -> { + ttBoxRecords.setStatus(IN_PACKSACK_ON.getCode()); + ttBoxRecords.setSource(TtboxRecordSource.FIGHT.getCode()); + ttBoxRecords.setFightId(ttFight.getId()); + ttBoxRecords.setHolderUserId(ttUser.getUserId()); + }).collect(Collectors.toList()); + log.info("更新boxRecord状态为4"); + boxRecordsService.updateBatchById(boxRecordsList, 1); + } + + @Override + public Map> tenTopFight(Integer maxType, String model, Integer minPrice, Integer maxPrice, String keyword) { + if (maxType == null || maxType < 0 || maxType > 3) { + return Collections.emptyMap(); + } + // keyword 仅支持对战房间号(fightId),精确匹配 + Integer fightId = null; + if (StrUtil.isNotEmpty(keyword)) { + try { + fightId = Integer.valueOf(keyword.trim()); + } catch (NumberFormatException e) { + // keyword 非纯数字,直接返回空结果 + return new HashMap<>(); + } + } + long beginTime = System.currentTimeMillis(); + // 创建日期格式化对象,指定日期格式 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + // 获取Calendar实例 + Calendar calendar = Calendar.getInstance(); + // 设置时间为今天凌晨 + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + // 获取今天凌晨时间 + Date todayMidnight = calendar.getTime(); + String formattedTodayMidnight = formatter.format(todayMidnight); + // 获取明天凌晨时间 + calendar.add(Calendar.DAY_OF_MONTH, 1); + Date tomorrowMidnight = calendar.getTime(); + String formattedTomorrowMidnight = formatter.format(tomorrowMidnight); + // 获取昨天凌晨时间 + calendar.add(Calendar.DAY_OF_MONTH, -2); // 从今天凌晨减去2天 + + List todayFightResult = new ArrayList<>(); + + // 超神模式(maxType=0)不传日期,查全历史 + String startDate = (maxType == 0) ? null : formattedTodayMidnight; + String endDate = (maxType == 0) ? null : formattedTomorrowMidnight; + List todayTenTopFight = apiFightMapper.tenTopFight(startDate, endDate, maxType, model, minPrice, maxPrice, fightId); + log.info("today time is {}", System.currentTimeMillis() - beginTime); + beginTime = System.currentTimeMillis(); + if (CollectionUtils.isNotEmpty(todayTenTopFight)) { + // 收集所有 fightId,批量查询替代 N+1 循环 + List fightIds = todayTenTopFight.stream() + .filter(ten -> ten != null && ten.getId() != null) + .map(ApiFightTenTopVo::getId) + .collect(Collectors.toList()); + // 构建 fightId -> totalOrnamentsPrice 映射(用 SQL 计算值覆盖) + Map priceMap = todayTenTopFight.stream() + .filter(ten -> ten != null && ten.getId() != null) + .collect(Collectors.toMap(ApiFightTenTopVo::getId, + ApiFightTenTopVo::getTotalOrnamentsPrice, (a, b) -> a)); + List batchResults = ttFightResultService.getFightResultBatch(fightIds); + for (FightResultVO vo : batchResults) { + if (vo != null && vo.getFight() != null) { + vo.setTotalOrnamentsPrice(priceMap.getOrDefault(vo.getFight().getId(), vo.getTotalOrnamentsPrice())); + todayFightResult.add(vo); + } + } + } + log.info("today for time is {}", System.currentTimeMillis() - beginTime); + + // 填充房主昵称 + List fightList = todayFightResult.stream() + .map(FightResultVO::getFight) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + enrichOwnerNickNames(fightList); + + Map> map = new HashMap<>(); + map.put("todayFightResult", todayFightResult); + return map; + } + + private void enrichBoxPrices(List fights) { + List boxIds = new ArrayList<>(); + for (TtFight fight : fights) { + if (fight == null || fight.getBoxData() == null || fight.getBoxData().isEmpty()) continue; + for (String boxId : fight.getBoxData().keySet()) { + Object value = fight.getBoxData().get(boxId); + Integer id = extractBoxId(value); + if (id != null) boxIds.add(id); + } + } + if (boxIds.isEmpty()) return; + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxIds); + List boxes = boxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + + for (TtFight fight : fights) { + if (fight == null || fight.getBoxData() == null) continue; + for (String boxId : fight.getBoxData().keySet()) { + Object value = fight.getBoxData().get(boxId); + FightBoxVO vo = resolveFightBoxVO(value); + if (vo != null) { + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), BigDecimal.ZERO)); + fight.getBoxData().put(boxId, vo); + } + } + } + } + + private Map buildRoomUpdateData(List fights) { + enrichBoxPrices(fights); + enrichOwnerNickNames(fights); + enrichSeatVipLevels(fights); + Map data = new HashMap<>(); + data.put("rows", fights); + return data; + } + + @Override + public void enrichOwnerNickNames(List fights) { + if (fights == null || fights.isEmpty()) return; + List ownerIds = fights.stream() + .map(TtFight::getUserId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (ownerIds.isEmpty()) return; + + List users = ttUserMapper.selectBatchIds(ownerIds); + Map nickMap = users.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(TtUser::getUserId, + u -> u.getNickName() != null ? u.getNickName() : "", (a, b) -> a)); + for (TtFight fight : fights) { + if (fight.getUserId() != null) { + // 用户存在时用实时昵称覆盖;用户被物理删除时保留入库的 ownerNickName + String liveNick = nickMap.get(fight.getUserId()); + if (liveNick != null) { + fight.setOwnerNickName(liveNick); + } + } + } + } + + private void enrichSeatVipLevels(List fights) { + if (fights == null || fights.isEmpty()) return; + List allPlayerIds = fights.stream() + .filter(f -> f.getSeats() != null) + .flatMap(f -> f.getSeats().stream()) + .map(FightSeat::getPlayerId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (allPlayerIds.isEmpty()) return; + + List users = new LambdaQueryChainWrapper<>(ttUserMapper) + .in(TtUser::getUserId, allPlayerIds) + .select(TtUser::getUserId, TtUser::getVipLevel) + .list(); + Map vipMap = users.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtUser::getUserId, u -> u.getVipLevel() != null && u.getVipLevel() > 0 ? u.getVipLevel() - 1 : 0, (a, b) -> a)); + for (TtFight fight : fights) { + if (fight.getSeats() == null) continue; + for (FightSeat seat : fight.getSeats()) { + if (seat.getPlayerId() != null) { + seat.setVipLevel(vipMap.getOrDefault(seat.getPlayerId(), 0)); + } + } + } + } + + private void enrichOwnerNickNamesForVO(List list) { + if (list == null || list.isEmpty()) return; + List ownerIds = list.stream() + .map(ApiFightListDataVO::getUserId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (ownerIds.isEmpty()) return; + + List users = ttUserMapper.selectBatchIds(ownerIds); + Map nickMap = users.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(TtUser::getUserId, + u -> u.getNickName() != null ? u.getNickName() : "", (a, b) -> a)); + for (ApiFightListDataVO vo : list) { + if (vo.getUserId() != null) { + // 用户存在时用实时昵称覆盖;用户被物理删除时保留入库的 ownerNickName + String liveNick = nickMap.get(vo.getUserId()); + if (liveNick != null) { + vo.setOwnerNickName(liveNick); + } + } + } + } + + private Integer extractBoxId(Object value) { + if (value == null) return null; + if (value instanceof FightBoxVO) return ((FightBoxVO) value).getBoxId(); + if (value instanceof LinkedHashMap) { + Object idField = ((LinkedHashMap) value).get("boxId"); + return parseIntSafely(idField); + } + return null; + } + + private FightBoxVO resolveFightBoxVO(Object value) { + if (value == null) return null; + if (value instanceof FightBoxVO) return (FightBoxVO) value; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + return new FightBoxVO( + map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + } + return null; + } + + private ArrayList enrichFightBoxVOList(TtFight fight) { + ArrayList list = new ArrayList<>(); + if (fight == null || fight.getBoxData() == null || fight.getBoxData().isEmpty()) return list; + + Map boxData = fight.getBoxData(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxData.keySet()); + List boxes = boxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + Map boxNameMap = boxes.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, b -> b.getBoxName() != null ? b.getBoxName() : "")); + + for (Map.Entry entry : boxData.entrySet()) { + Integer bid = extractBoxId(entry.getValue()); + if (bid == null) continue; + List ornaments = boxOrnamentsMapper.selectTtBoxOrnamentsList(bid); + FightBoxVO vo = resolveFightBoxVO(entry.getValue()); + if (vo != null) { + vo.setOrnaments(ornaments); + vo.setPrice(boxMap.getOrDefault(bid, BigDecimal.ZERO)); + vo.setBoxName(boxNameMap.getOrDefault(bid, "")); + list.add(vo); + } + } + return list; + } + + /** + * 快速加入对战房间 - 在本阵营内根据用户金额找到小于等于用户金额且有空位的最高价房间(不落座) + *

+ * 规则说明: + * 1. 只查询等待中房间(status = '0')且 model 与请求大厅阵营一致(非酋大厅不匹配欧皇房间) + * 2. 找到所有房间价格 <= 用户余额的房间 + * 3. 在这些房间中选择价格最高的一个返回 + *

+ * 示例:用户有 10 金币,大厅有 1 元/2 元/9 元三个房间 → 选择 9 元的房间 + * + * @param userId 用户 ID + * @param model 房间模式(大厅阵营):0-欧皇 1-非酋 + * @return 房间 id(Long),本阵营无可加入房间则 data 为 null + */ + @Override + public R quickJoinFightRoom(Integer userId, String model) { + // 0. 阵营必传:缺失时无法保证不跨阵营匹配,直接返回 null + if (StringUtils.isBlank(model)) { + log.warn("用户 {} 快速加入对战未传 model,拒绝匹配", userId); + return R.ok(null, "暂无可用房间"); + } + + // 1. 获取用户信息及账户金额 + TtUser user = userService.getById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + + BigDecimal userAmount = user.getAccountAmount(); + if (userAmount == null || userAmount.compareTo(BigDecimal.ZERO) < 0) { + return R.fail("用户余额异常"); + } + + log.info("用户 {} 请求快速加入对战,阵营:{},当前余额:{}", userId, model, userAmount); + + // 2. 只查询等待中且同阵营的对战房间(status = 0) + List fightList = new LambdaQueryChainWrapper<>(fightMapper) + .eq(TtFight::getStatus, 0) // 0=准备/等待中 + .eq(TtFight::getModel, model) // 只在当前大厅阵营内匹配 + .orderByDesc(TtFight::getCreateTime) + .list(); + + if (CollectionUtils.isEmpty(fightList)) { + return R.ok(null, "暂无可用房间"); + } + + // 3. 批量查询所有涉及的箱子信息(避免 N+1 查询) + Set allBoxIds = fightList.stream() + .filter(fight -> fight.getBoxData() != null) + .flatMap(fight -> fight.getBoxData().keySet().stream()) + .map(this::parseIntSafely) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map boxPriceMap = allBoxIds.isEmpty() + ? Collections.emptyMap() + : boxMapper.selectBatchIds(allBoxIds).stream() + .filter(box -> box != null && box.getBoxId() != null) + .collect(Collectors.toMap(TtBox::getBoxId, + box -> box.getPrice() != null ? box.getPrice() : BigDecimal.ZERO, + (a, b) -> a)); + + // 4. 过滤出有空位且房间总价 <= 用户余额的房间,取价格最高的一个 + TtFight targetFight = null; + BigDecimal targetPrice = null; + for (TtFight fight : fightList) { + if (!isFightAvailable(fight)) { + continue; + } + BigDecimal price = calculateTotalRoomPrice(fight, boxPriceMap); + if (price.compareTo(userAmount) > 0) { + continue; // 超出用户余额 + } + if (targetPrice == null || price.compareTo(targetPrice) > 0) { + targetFight = fight; + targetPrice = price; + } + } + + if (targetFight == null) { + return R.ok(null, "没有适合您余额的对战房间"); + } + + log.info("用户 {} 快速加入成功,阵营:{},推荐房间 id: {}, 房间价格:{}, 用户余额:{}", + userId, model, targetFight.getId(), targetPrice, userAmount); + + return R.ok(targetFight.getId().longValue(), "快速加入成功"); + } + + /** + * 安全解析整数,非数字格式返回 null 并记录告警 + */ + private Integer parseIntSafely(Object value) { + if (value == null) { + return null; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + String text = value.toString().trim(); + if (StrUtil.isBlank(text)) { + return null; + } + try { + return Integer.valueOf(text); + } catch (NumberFormatException e) { + log.warn("无法解析为整数的值:{}", text); + return null; + } + } + + /** + * 检查房间是否至少有一个空座位(seat.status == 0) + *

+ * seats 经 JacksonTypeHandler 反序列化后可能为 Map,此处兼容两种类型 + */ + private boolean isFightAvailable(TtFight fight) { + if (fight == null || fight.getSeats() == null || fight.getSeats().isEmpty()) { + return false; + } + + for (Object seat : fight.getSeats()) { + Integer status = extractSeatStatus(seat); + if (status != null && status == 0) { + return true; + } + } + return false; + } + + /** + * 提取座位状态(0空位 1入座 2就绪) + */ + private Integer extractSeatStatus(Object seat) { + if (seat == null) { + return null; + } + if (seat instanceof FightSeat) { + return ((FightSeat) seat).getStatus(); + } + if (seat instanceof Map) { + return parseIntSafely(((Map) seat).get("status")); + } + return null; + } + + /** + * 计算房间所有箱子的总价 + * + * @param fight 对战房间 + * @param boxPriceMap 箱子 ID 到价格的映射 + * @return 房间总价 + */ + private BigDecimal calculateTotalRoomPrice(TtFight fight, Map boxPriceMap) { + if (fight == null || fight.getBoxData() == null || fight.getBoxData().isEmpty()) { + return BigDecimal.ZERO; + } + + BigDecimal total = BigDecimal.ZERO; + for (Object boxVO : fight.getBoxData().values()) { + Integer boxId = extractBoxId(boxVO); + if (boxId == null) { + continue; + } + total = total.add(boxPriceMap.getOrDefault(boxId, BigDecimal.ZERO)); + } + return total; + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiFightUserServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiFightUserServiceImpl.java new file mode 100644 index 0000000..3d3c595 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiFightUserServiceImpl.java @@ -0,0 +1,11 @@ +package com.ruoyi.playingmethod.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.mapper.TtFightUserMapper; +import com.ruoyi.domain.other.TtFightUser; +import com.ruoyi.playingmethod.service.ApiFightUserService; +import org.springframework.stereotype.Service; + +@Service +public class ApiFightUserServiceImpl extends ServiceImpl implements ApiFightUserService { +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiLuckyUpgradeServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiLuckyUpgradeServiceImpl.java new file mode 100644 index 0000000..68e4ba8 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiLuckyUpgradeServiceImpl.java @@ -0,0 +1,825 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; +import com.ruoyi.admin.mapper.TtUpgradeFailOrnamentsMapper; +import com.ruoyi.admin.mapper.TtUpgradeOrnamentsMapper; +import com.ruoyi.admin.mapper.TtUpgradeRecordMapper; +import com.ruoyi.admin.model.UpdateUserAccountBo; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtUpgradeOrnamentsService; +import com.ruoyi.admin.service.TtUpgradeRecordService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.domain.common.constant.LockKey.UpgradeLock; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.ApiLuckyUpgradeBody; +import com.ruoyi.domain.other.TtUpgradeOrnaments; +import com.ruoyi.domain.other.TtUpgradeRecord; +import com.ruoyi.domain.other.UpgradeBodyA; +import com.ruoyi.domain.vo.ApiLuckyOrnamentsDataVO; +import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO; +import com.ruoyi.playingmethod.mapper.ApiLuckyUpgradeMapper; +import com.ruoyi.playingmethod.mapper.ApiTtUserBlendErcashMapper; +import com.ruoyi.playingmethod.service.ApiLuckyUpgradeService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.message.service.MessageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class ApiLuckyUpgradeServiceImpl implements ApiLuckyUpgradeService { + + @Value("${mkcsgo.upgrade.defaultRequired}") + private Integer defaultRequired; + + @Value("${mkcsgo.upgrade.anchorDefaultRequired}") + private Integer anchorDefaultRequired; + + @Autowired + private Executor customThreadPoolExecutor; + + @Autowired + private ApiTtUserBlendErcashMapper apiTtUserBlendErcashMapper; + + private final TtUserService userService; + private final ISysConfigService configService; + private final ApiLuckyUpgradeMapper apiLuckyUpgradeMapper; + private final TtUpgradeOrnamentsMapper upgradeOrnamentsMapper; + private final TtUpgradeFailOrnamentsMapper upgradeFailOrnamentsMapper; + private final TtUpgradeRecordMapper upgradeRecordMapper; + private final TtOrnamentMapper ornamentsMapper; + private final TtBoxRecordsMapper boxRecordsMapper; + private final TtOrnamentsLevelMapper ornamentsLevelMapper; + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private RedisLock redisLock; + + @Autowired + private RedisCache redisCache; + + public ApiLuckyUpgradeServiceImpl(TtUserService userService, + ISysConfigService configService, + ApiLuckyUpgradeMapper apiLuckyUpgradeMapper, + TtUpgradeOrnamentsMapper upgradeOrnamentsMapper, + TtUpgradeFailOrnamentsMapper upgradeFailOrnamentsMapper, + TtUpgradeRecordMapper upgradeRecordMapper, + TtOrnamentMapper ornamentsMapper, + TtBoxRecordsMapper boxRecordsMapper, + TtOrnamentsLevelMapper ornamentsLevelMapper) { + this.userService = userService; + this.configService = configService; + this.apiLuckyUpgradeMapper = apiLuckyUpgradeMapper; + this.upgradeOrnamentsMapper = upgradeOrnamentsMapper; + this.upgradeFailOrnamentsMapper = upgradeFailOrnamentsMapper; + this.upgradeRecordMapper = upgradeRecordMapper; + this.ornamentsMapper = ornamentsMapper; + this.boxRecordsMapper = boxRecordsMapper; + this.ornamentsLevelMapper = ornamentsLevelMapper; + } + + @Autowired + private TtUpgradeOrnamentsService ttUpgradeOrnamentsService; + + @Autowired + private TtUpgradeOrnamentsMapper ttUpgradeOrnamentsMapper; + + @Autowired + private TtBoxRecordsService ttBoxRecordsService; + + @Autowired + private TtUpgradeRecordService ttUpgradeRecordService; + + @Autowired + private MessageService messageService; + + @Override + public List getOrnamentsList(ApiLuckyUpgradeBody apiLuckyUpgradeBody) { + // type 对应 tt_ornament.type 数字字典码(1匕首 2手枪 3步枪 4微型冲锋枪 5重型武器 6手套 7印花 8其它)。 + // 在这里只把空字符串归一为 null,避免 SQL 里 'type = '' 造成隐式转换全表扫描;传具体值时要透传到 mapper 。 + if (apiLuckyUpgradeBody.getType() != null && apiLuckyUpgradeBody.getType().trim().isEmpty()) { + apiLuckyUpgradeBody.setType(null); + } + return apiLuckyUpgradeMapper.getOrnamentsList(apiLuckyUpgradeBody); + } + + public R upgradeCheck(TtUser player, BigDecimal price) { + + if (player == null) { + return R.fail("用户不存在"); + } + + // 白号允许游玩:playing() 走纯概率分支,不写共享幸运值/本轮进度, + // 与真实玩家(01/02)完全隔离,不会影响真实池出货节奏。 + if (!isBlankAccount(player) + && !UserType.ANCHOR.getCode().equals(player.getUserType()) + && !UserType.COMMON_USER.getCode().equals(player.getUserType())) { + return R.fail("非法的用户类型"); + } + + // 封禁 / 删除用户不允许参与游戏(status='1' 封禁,del_flag!='0' 已删除) + if ("1".equals(player.getStatus())) { + return R.fail("账号已被封禁,无法参与游戏"); + } + if (!"0".equals(player.getDelFlag())) { + return R.fail("账号状态异常,无法参与游戏"); + } + + // 充值记录校验:未充过值的账号不允许参与幸运升级,防止黑产账户刷单。先于余额校验,保证新用户返回 703。 + // 归档兼容:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL, + // 避免历史充值流水被归档后误判为“未充值”。 + BigDecimal cacheRecharge = player.getTotalRechargeAmount(); + boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0; + if (!recharged) { + BigDecimal sumRecharge = apiTtUserBlendErcashMapper.sumAmountByUserIdsAndSource( + Collections.singletonList(player.getUserId()), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0; + } + if (!recharged) { + return R.fail(703, "玩家未充值不能游玩任何游戏"); + } + + // 余额校验:与本次升级所需金额比较,不足则返回 702 + if (player.getAccountAmount() == null || player.getAccountAmount().compareTo(price) < 0) { + return R.fail(702, "账户余额不足"); + } + + return R.ok(); + } + + /** + * 白号判定:全局口径 is_blank_account=1,兼容历史数据只打了 user_type='06' 标签的行。 + */ + private boolean isBlankAccount(TtUser user) { + return Integer.valueOf(1).equals(user.getIsBlankAccount()) + || UserType.BLANK_ACCOUNT.getCode().equals(user.getUserType()); + } + + /** + * 奖池类型:普通(02)/主播(01)/白号(06) 各一套进度与幸运值,互不污染。 + * 白号优先按 is_blank_account 判定,兼容历史数据只改了标记没改 user_type 的行。 + */ + private String poolType(TtUser user) { + return isBlankAccount(user) ? UserType.BLANK_ACCOUNT.getCode() : user.getUserType(); + } + + /** + * 玩家使用的幸运区间:主播=anchor_luck_section,白号=blank_luck_section(未配置则回退普通),其余=普通 luck_section。 + */ + private String resolveLuckSection(TtUpgradeOrnaments ornament, TtUser user) { + if (isBlankAccount(user)) { + return StringUtils.isNotEmpty(ornament.getBlankLuckSection()) + ? ornament.getBlankLuckSection() + : ornament.getLuckSection(); + } + return UserType.ANCHOR.getCode().equals(user.getUserType()) + ? ornament.getAnchorLuckSection() + : ornament.getLuckSection(); + } + + /** + * 根据目标进度计算本轮目标金额:price * targetProgress / 100,保留两位小数。 + */ + private BigDecimal calcTargetAmount(BigDecimal ornamentPrice, Integer targetProgress) { + if (ornamentPrice == null || targetProgress == null || targetProgress <= 0) { + return null; + } + return ornamentPrice + .multiply(BigDecimal.valueOf(targetProgress)) + .divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP); + } + + /** + * 根据升级饰品价格与前端传入的概率计算本次消费金额。 + * 公式:price = ornamentPrice * probability / 100,保留两位小数。 + * 价格必须大于 0,以防止 100% 成功率、价格为 0 的异常场景。 + */ + private BigDecimal calcUpgradePrice(TtUpgradeOrnaments upgradeOrnament, Integer probability) { + BigDecimal ornamentPrice = upgradeOrnament.getOrnamentPrice(); + if (ornamentPrice == null || ornamentPrice.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return ornamentPrice + .multiply(BigDecimal.valueOf(probability)) + .divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP); + } + + /** + * 单次升级概率封顶值:超过此值一律按此值计算。 + * 设计意图:避免高概率(如 90% / 100%)一拉就出货,保障多升级几次的体验。 + * 同时扣款按封顶后概率计算,对用户可能更友好。 + */ + private static final int PROBABILITY_CAP = 75; + + @Override + @Transactional + public R upgrade(TtUser ttUser, UpgradeBodyA upgradeParam) { + + // 双口径概率设计: + // 1)前端传入的原始概率(rawProbability,如 90)用于“价格计算 + 扣款 + 数据库落库记录”, + // 保证账单与历史记录与用户看到的一致。 + // 2)后端抑制后的有效概率(effectiveProbability,超过 PROBABILITY_CAP 一律按 75)仅用于中奖判定, + // 避免高概率(如 90%)一拉就出货。 + Integer rawProbability = upgradeParam.getProbability(); + if (rawProbability == null) { + return R.fail("概率不能为空。"); + } + int effectiveProbability = Math.min(rawProbability, PROBABILITY_CAP); + + TtUpgradeOrnaments upgradeOrnament = new LambdaQueryChainWrapper<>(upgradeOrnamentsMapper) + .eq(TtUpgradeOrnaments::getId, upgradeParam.getUpgradeId()) + .eq(TtUpgradeOrnaments::getStatus, "0") + .one(); + if (ObjectUtil.isNull(upgradeOrnament)) { + return R.fail("该饰品未在幸运升级开放。"); + } + + // 价格口径与前端展示对齐:以 tt_ornament.use_price(实时价)为准。 + // tt_upgrade_ornaments.ornament_price 是“批量填货”当时的快照, + // 三方价格任务刷新 tt_ornament.use_price 后并不会同步该快照, + // 若仍按快照计算会导致前端显示金额(实时价×概率)与记录里 amount_consumed 不一致。 + TtOrnament currentOrnament = new LambdaQueryChainWrapper<>(ornamentsMapper) + .eq(TtOrnament::getId, upgradeOrnament.getOrnamentsId()) + .one(); + if (ObjectUtil.isNotNull(currentOrnament) && ObjectUtil.isNotEmpty(currentOrnament.getUsePrice()) + && currentOrnament.getUsePrice().compareTo(BigDecimal.ZERO) > 0) { + upgradeOrnament.setOrnamentPrice(currentOrnament.getUsePrice()); + } + + // 后端根据升级饰品价格和原始概率计算消费金额,不再用前端传入的 price, + // 以防止前端传入 0 价格返回 100% 成功率这种问题。 + // 注意:价格使用 rawProbability(如 90),而非 effectiveProbability(封顶 75)。 + BigDecimal price = calcUpgradePrice(upgradeOrnament, rawProbability); + if (price.compareTo(BigDecimal.ZERO) <= 0) { + return R.fail("该饰品价格异常,无法参与幸运升级。"); + } + + // 真实计算金额:用于后台进度统计,超过 75% 按 75% 计算 + BigDecimal effectiveAmount = calcTargetAmount(upgradeOrnament.getOrnamentPrice(), effectiveProbability); + + // 检查 + R check = upgradeCheck(ttUser, price); + if (!check.getCode().equals(200)) return check; + + // lock_key:锁粒度 = 饰品位 + 奖池类型(普通/主播/白号各自串行,互不阻塞) + Integer upgradeOrnamentId = upgradeParam.getUpgradeId(); + String poolType = poolType(ttUser); + String upgradeLock = UpgradeLock.UPGRADE_LOCK.getLock() + upgradeOrnamentId + ":" + poolType; + + // 2 尝试获取锁 + Boolean lock = false; + for (int t = 0; t < 2; t++) { + + lock = redisLock.tryLock(upgradeLock, 2L, 7L, TimeUnit.SECONDS); + + if (!lock) { + + } else { + break; + } + } + + if (!lock) return R.fail("系统繁忙,请稍后重试。"); + + // 3 扣款 + R> mapR = upgradeAccounting(price, ttUser); + if (!mapR.getCode().equals(200)) { + redisLock.unlock(upgradeLock); + return mapR; + } + + // 4 计算游戏(中奖判定用封顶后的 effectiveProbability) + UpgradePlayResult playResult; + try { + + // 升级核心 + playResult = playing(ttUser, upgradeOrnament, upgradeParam, effectiveProbability); + + } finally { + // 锁释放必须发生在 finally 中,避免异常捕获后 unlock 本身抛异常导致锁泄漏 + try { + redisLock.unlock(upgradeLock); + } catch (Exception e) { + log.error("释放幸运升级锁失败。userId={}, upgradeId={}", + ttUser.getUserId(), upgradeParam.getUpgradeId(), e); + } + } + + boolean isVictory = playResult.isWin(); + + // 5 同步保存本次结果。 + // 快照本轮目标金额与已投金额(用于升级记录历史展示,需在累加/重置前读取) + // 本轮目标金额使用实际随机出来的目标进度(fullV),而非幸运区间固定最大值 + boolean anchorPool = UserType.ANCHOR.getCode().equals(poolType); + boolean blankPool = UserType.BLANK_ACCOUNT.getCode().equals(poolType); + + BigDecimal snapshotRequired = calcTargetAmount(upgradeOrnament.getOrnamentPrice(), playResult.getTargetProgress()); + if (snapshotRequired == null) { + snapshotRequired = anchorPool + ? upgradeOrnament.getAnchorAmountRequired() + : upgradeOrnament.getAmountRequired(); + } + // 本轮已投金额快照:白号读自己池的一整套(与普通/主播同口径,只是各记各的列) + BigDecimal snapshotInvested = blankPool + ? upgradeOrnament.getBlankAmountInvested() + : (anchorPool ? upgradeOrnament.getAnchorAmountInvested() : upgradeOrnament.getAmountInvested()); + BigDecimal snapshotEffectiveInvested = blankPool + ? upgradeOrnament.getBlankEffectiveAmountInvested() + : (anchorPool ? upgradeOrnament.getAnchorEffectiveAmountInvested() : upgradeOrnament.getEffectiveAmountInvested()); + + // 幸运值快照(累计原始概率,累加/重置前读取,按奖池类型区分) + Integer snapshotProbabilityInvested = blankPool + ? upgradeOrnament.getBlankProbabilityInvested() + : (anchorPool ? upgradeOrnament.getAnchorProbabilityInvested() : upgradeOrnament.getProbabilityInvested()); + int prevProbabilityInvested = snapshotProbabilityInvested == null ? 0 : snapshotProbabilityInvested; + // 本饰品当前幸运值:出货成功清 0,失败累加前端原始概率(白号在自己的池里同样累计) + int luckyValue = isVictory ? 0 : prevProbabilityInvested + rawProbability; + + // 更新本轮已投入金额(含幸运值) + // 白号维护自己的一整套字段(本轮已投金额 + 幸运值),与普通/主播同样的算法,只是不碰它们的列 + LambdaUpdateWrapper investedUpdate = new LambdaUpdateWrapper<>(); + investedUpdate.eq(TtUpgradeOrnaments::getId, upgradeOrnament.getId()); + if (blankPool) { + if (isVictory) { + // 成功:白号本轮进度清 0 重置,溢出不带入下一轮 + investedUpdate.set(TtUpgradeOrnaments::getBlankAmountInvested, BigDecimal.ZERO); + investedUpdate.set(TtUpgradeOrnaments::getBlankEffectiveAmountInvested, BigDecimal.ZERO); + investedUpdate.set(TtUpgradeOrnaments::getBlankProbabilityInvested, 0); + } else { + // 失败:实际扣款累计 + price;真实计算口径累计 + effectiveAmount;幸运值累加原始概率 + investedUpdate.setSql("blank_amount_invested = blank_amount_invested + " + price); + investedUpdate.setSql("blank_effective_amount_invested = blank_effective_amount_invested + " + effectiveAmount); + investedUpdate.setSql("blank_probability_invested = COALESCE(blank_probability_invested, 0) + " + rawProbability); + } + } else if (anchorPool) { + if (isVictory) { + // 成功:本轮进度清 0 重置,溢出不带入下一轮 + investedUpdate.set(TtUpgradeOrnaments::getAnchorAmountInvested, BigDecimal.ZERO); + investedUpdate.set(TtUpgradeOrnaments::getAnchorEffectiveAmountInvested, BigDecimal.ZERO); + // 幸运值(累计原始概率)同步清 0 + investedUpdate.set(TtUpgradeOrnaments::getAnchorProbabilityInvested, 0); + } else { + // 失败:实际扣款累计 + price;真实计算口径累计 + effectiveAmount + investedUpdate.setSql("anchor_amount_invested = anchor_amount_invested + " + price); + investedUpdate.setSql("anchor_effective_amount_invested = anchor_effective_amount_invested + " + effectiveAmount); + // 幸运值累加前端原始概率(如 90) + investedUpdate.setSql("anchor_probability_invested = COALESCE(anchor_probability_invested, 0) + " + rawProbability); + } + } else { + if (isVictory) { + // 成功:本轮进度清 0 重置,溢出不带入下一轮 + investedUpdate.set(TtUpgradeOrnaments::getAmountInvested, BigDecimal.ZERO); + investedUpdate.set(TtUpgradeOrnaments::getEffectiveAmountInvested, BigDecimal.ZERO); + // 幸运值(累计原始概率)同步清 0 + investedUpdate.set(TtUpgradeOrnaments::getProbabilityInvested, 0); + } else { + // 失败:实际扣款累计 + price;真实计算口径累计 + effectiveAmount + investedUpdate.setSql("amount_invested = amount_invested + " + price); + investedUpdate.setSql("effective_amount_invested = effective_amount_invested + " + effectiveAmount); + // 幸运值累加前端原始概率(如 90) + investedUpdate.setSql("probability_invested = COALESCE(probability_invested, 0) + " + rawProbability); + } + } + upgradeOrnamentsMapper.update(null, investedUpdate); + TtUpgradeRecord upgradeRecord; // 升级记录 + List boxRecords; // 获得物品记录 + List gainList; // 与 boxRecords 顺序一一对应,落库后会回填 id 用于分解接口 + if (isVictory) { + + Long ornamentId = upgradeOrnament.getOrnamentsId(); + + // 胜 + List prizeList = ornamentsMapper.simpleOrnamentInfo(Arrays.asList(ornamentId)); + SimpleOrnamentVO ornament = prizeList.get(0); + + // 奖励集合 并统计奖励总价值 + // List prizeList = Arrays.asList(ornament); + BigDecimal prizeTotal = BigDecimal.ZERO; + for (SimpleOrnamentVO vo : prizeList) { + // 兜底:价格为空时跳过累加,避免 NPE(use_price 未回填) + if (vo.getOrnamentPrice() != null) { + prizeTotal = prizeTotal.add(vo.getOrnamentPrice()); + } + } + gainList = prizeList; + + // 构造升级记录(gainOrnamentList 在 boxRecords 落库拿到 id 后再填充) + upgradeRecord = TtUpgradeRecord.builder() + .userId(ttUser.getUserId()) + .userType(ttUser.getUserType()) + .nickName(ObjectUtil.isNotEmpty(ttUser.getNickName()) ? ttUser.getNickName() : ttUser.getUserName()) + .userAvatar(ttUser.getAvatar()) + .isVictory(isVictory) + + .amountConsumed(price) + .probability(upgradeParam.getProbability()) + .luckyValue(luckyValue) + + .amountRequired(snapshotRequired) + .amountInvested(snapshotInvested) + .effectiveAmountInvested(snapshotEffectiveInvested) + .effectiveAmount(effectiveAmount) + + .targetUpgradeId(upgradeOrnament.getId()) + .targetOrnamentId(ornamentId) + .targetOrnamentPrice(ornament.getOrnamentPrice()) + + .gainOrnamentsPrice(prizeTotal) + + .openTime(new Date()) + .build(); + + // 构造获得物品记录 + boxRecords = Arrays.asList(TtBoxRecords.builder() + .source(TtboxRecordSource.UPGRADE.getCode()) + .ornamentId(ornamentId) + .ornamentName(ornament.getOrnamentName()) + .userId(ttUser.getUserId()) + .holderUserId(ttUser.getUserId()) + .imageUrl(ornament.getOrnamentImgUrl()) + .ornamentsPrice(ornament.getOrnamentPrice()) + .ornamentsLevelId(ornament.getOrnamentLevelId()) + .ornamentLevelImg(ornament.getOrnamentLevelImg()) + .status(TtboxRecordStatus.IN_PACKSACK_ON.getCode()) + .marketHashName(ornament.getOrnamentHashName()) + .createTime(new Date()) + .updateTime(new Date()) + .build()); + + } else { + + // 负 + // 奖励集合 并统计奖励总价值 + List failPrize = upgradeFailOrnamentsMapper.ornamentInfoByUpgradeId(upgradeOrnament.getId()); + + BigDecimal prizeTotal = BigDecimal.ZERO; + if (!failPrize.isEmpty()) { + for (SimpleOrnamentVO item : failPrize) { + // 兜底:安慰奖价格/数量为空时按 0 计,避免 NPE(ornament_price 未配置或 use_price 未回填) + BigDecimal itemPrice = item.getOrnamentPrice() == null ? BigDecimal.ZERO : item.getOrnamentPrice(); + int itemNumber = item.getOrnamentNumber() == null ? 0 : item.getOrnamentNumber(); + prizeTotal = prizeTotal.add(itemPrice.multiply(BigDecimal.valueOf(itemNumber))); + } + } + gainList = failPrize; + + // 构造升级记录(gainOrnamentList 在 boxRecords 落库拿到 id 后再填充) + upgradeRecord = TtUpgradeRecord.builder() + .userId(ttUser.getUserId()) + .userType(ttUser.getUserType()) + .nickName(ObjectUtil.isNotEmpty(ttUser.getNickName()) ? ttUser.getNickName() : ttUser.getUserName()) + .userAvatar(ttUser.getAvatar()) + + .amountConsumed(price) + .probability(upgradeParam.getProbability()) + .luckyValue(luckyValue) + + .amountRequired(snapshotRequired) + .amountInvested(snapshotInvested) + .effectiveAmountInvested(snapshotEffectiveInvested) + .effectiveAmount(effectiveAmount) + + .isVictory(isVictory) + + .targetUpgradeId(upgradeOrnament.getId()) + .targetOrnamentId(upgradeOrnament.getOrnamentsId()) + .targetOrnamentPrice(upgradeOrnament.getOrnamentPrice()) + + .gainOrnamentsPrice(prizeTotal) + + .openTime(new Date()) + .build(); + + // 构造获得物品记录 + boxRecords = failPrize.stream().map(item -> { + return TtBoxRecords.builder() + .source(TtboxRecordSource.UPGRADE.getCode()) + .ornamentId(item.getOrnamentId()) + .ornamentName(item.getOrnamentName()) + .userId(ttUser.getUserId()) + .holderUserId(ttUser.getUserId()) + .imageUrl(item.getOrnamentImgUrl()) + .ornamentsPrice(item.getOrnamentPrice()) + .ornamentsLevelId(item.getOrnamentLevelId()) + .ornamentLevelImg(item.getOrnamentLevelImg()) + .status(TtboxRecordStatus.IN_PACKSACK_ON.getCode()) + .marketHashName(item.getOrnamentHashName()) + .createTime(new Date()) + .updateTime(new Date()) + .build(); + }).collect(Collectors.toList()); + + } + + // 游戏记录 + // 1) 先落库 boxRecords 拿到 id(tt_box_records.id,即背包记录ID), + // 然后回填到 gainOrnamentList 中的每个饰品,供前端在《幸运升级》中 + // 调用分解接口 /api/userPackSack/decompose(入参 packSackIds)使用。 + ttBoxRecordsService.saveBatch(boxRecords); + + if (gainList != null && !gainList.isEmpty()) { + int size = Math.min(gainList.size(), boxRecords.size()); + for (int i = 0; i < size; i++) { + gainList.get(i).setId(boxRecords.get(i).getId()); + } + upgradeRecord.setGainOrnamentList(JSONUtil.toJsonStr(gainList)); + } else { + upgradeRecord.setGainOrnamentList("[]"); + } + + ttUpgradeRecordService.save(upgradeRecord); + + // 发送邮件通知(取第一件获得的饰品) + if (!boxRecords.isEmpty()) { + TtBoxRecords record = boxRecords.get(0); + String priceStr = record.getOrnamentsPrice() == null ? "0" + : record.getOrnamentsPrice().stripTrailingZeros().toPlainString(); + String content = isVictory + ? "恭喜您在进阶风暴中成功升级,获得【" + record.getOrnamentName() + "】(价值 ¥" + priceStr + "),已放入背包。" + : "您本次进阶风暴升级未成功,获得【" + record.getOrnamentName() + "】(价值 ¥" + priceStr + "),已放入背包。"; + messageService.sendNotice( + Long.valueOf(ttUser.getUserId()), + MessageCategoryEnum.GAME_LUCKY_UPGRADE.getTitle(), + content, + MessageCategoryEnum.GAME_LUCKY_UPGRADE.getCode()); + } + + // 回填本饰品当前幸运值(瞬态字段,不入库),返回给前端 cocos 展示 + upgradeRecord.setLuckyValue(luckyValue); + + return R.ok(upgradeRecord); + + } + + public UpgradePlayResult playing(TtUser ttUser, TtUpgradeOrnaments upgradeOrnament, UpgradeBodyA upgradeParam, int effectiveProbability) { + + Integer upgradeOrnamentId = upgradeOrnament.getId(); + // 奖池类型:白号(06)走自己的进度池与幸运值,Redis key 天然按类型隔离,白号之间共享 06 池 + String poolType = poolType(ttUser); + + String rangeFixedKey = RedisConstants.UPGRADE_RANGE_FIXED + upgradeOrnamentId + ":" + poolType; + String rangeFloatKey = RedisConstants.UPGRADE_RANGE + upgradeOrnamentId + ":" + poolType; + + // 读redis的固定概率 [x,y] x进度,y空间大小(缓存缺失或非法时返回 null) + Integer[] rangeFixed = parseRange(redisCache.getCacheObject(rangeFixedKey)); + // 读redis的饰品概率区间 [begin,end] + Integer[] rangeFloat = parseRange(redisCache.getCacheObject(rangeFloatKey)); + + if (isValidRange(rangeFixed)) { + // 抽奖 + return doUpgrade( + ttUser, + upgradeOrnament, + rangeFixed, + upgradeParam, + rangeFixedKey, + rangeFloatKey, + rangeFloat, + effectiveProbability); + } + + // 没有,使用redis的概率区间新建, + if (isValidRange(rangeFloat)) { + + // 使用饰品概率区间创建新的固定概率(进度从 0 开始) + Integer[] newRangeFixed = createRangeFixed(rangeFloat, 0); + + // 抽奖 + return doUpgrade( + ttUser, + upgradeOrnament, + newRangeFixed, + upgradeParam, + rangeFixedKey, + rangeFloatKey, + rangeFloat, + effectiveProbability); + } + + // 也没有概率区间,用mysql的数据(按奖池类型取:主播/白号/普通)创建固定概率,抽奖 + String luckSection = resolveLuckSection(upgradeOrnament, ttUser); + if (StringUtils.isNotEmpty(luckSection)) { + rangeFloat = JSONUtil.toList(luckSection, Integer.class).toArray(new Integer[2]); + } + + rangeFixed = createRangeFixed(rangeFloat, 0); + + // 缓存固定概率 + // redisCache.setCacheList(rangeFixedKey, Arrays.asList(rangeFixed)); + // redisCache.setCacheObject(rangeFixedKey, Arrays.asList(rangeFixed)); + // 缓存概率区间 + redisCache.setCacheObject(rangeFloatKey, rangeFloat, 1, TimeUnit.DAYS); + + // 抽奖 + return doUpgrade( + ttUser, + upgradeOrnament, + rangeFixed, + upgradeParam, + rangeFixedKey, + rangeFloatKey, + rangeFloat, + effectiveProbability); + + } + + /** + * @param rangeFixed 本次的固定概率 + * @param param http请求参数 + * @param rangeFixedKey redis固定概率key + * @param rangeFloat 升级饰品的概率区间 + * @param effectiveProbability 封顶后的有效概率(仅用于中奖判定) + * @return + */ + public UpgradePlayResult doUpgrade(TtUser ttUser, + TtUpgradeOrnaments upgradeOrnament, + Integer[] rangeFixed, + UpgradeBodyA param, + String rangeFixedKey, + String rangeFloatKey, + Integer[] rangeFloat, + int effectiveProbability) { + + // 计算进度 + Integer currentV = rangeFixed[0]; + Integer fullV = rangeFixed[1]; + + // 中奖判定使用封顶后的 effectiveProbability(如 75),而非前端原始传入的 param.getProbability()(如 90) + int sub = fullV - (effectiveProbability + currentV); + + if (sub > 0) { + // 失败,记录进度 + int i = effectiveProbability + currentV; + rangeFixed[0] = i; + // redisCache.setCacheList(rangeFixedKey, Arrays.asList(rangeFixed)); + redisCache.setCacheObject(rangeFixedKey, rangeFixed, 1, TimeUnit.DAYS); + return new UpgradePlayResult(false, fullV); + } + + // 成功:先保存本轮目标进度(创建新的固定概率前) + Integer currentRoundTarget = fullV; + + // 如果没有概率区间,新建 + if (isValidRange(rangeFloat)) { + + // 按奖池类型重新读一遍幸运区间(主播/白号/普通),后台改配置后本轮中奖即生效 + String luckSection = resolveLuckSection(upgradeOrnament, ttUser); + if (StringUtils.isNotEmpty(luckSection)) { + rangeFloat = JSONUtil.toList(luckSection, Integer.class).toArray(new Integer[2]); + } + + // 缓存概率区间 + redisCache.setCacheObject(rangeFloatKey, rangeFloat, 1, TimeUnit.DAYS); + + // rangeFixed = createRangeFixed(rangeFloat,0); + + } + + // 成功,随机创建新的固定概率(进度清 0 重置,溢出不带入下一轮) + Integer[] newRangeFixed = createRangeFixed(rangeFloat, 0); + + // 保存新的固定概率 + // redisCache.setCacheList(rangeFixedKey, Arrays.asList(newRangeFixed)); + redisCache.setCacheObject(rangeFixedKey, newRangeFixed, 1, TimeUnit.DAYS); + return new UpgradePlayResult(true, currentRoundTarget); + + } + + + public Integer[] createRangeFixed(Integer[] rangeFloat, Integer initVal) { + + // 防护:区间为空或元素缺失时使用默认区间,避免 NPE + if (rangeFloat == null || rangeFloat.length < 2 || rangeFloat[0] == null || rangeFloat[1] == null) { + log.warn("幸运区间为空或非法,使用默认区间 [100, 160]"); + rangeFloat = new Integer[]{100, 160}; + } + + // 防护:如果区间配置异常(min >= max),强制修正 + if (rangeFloat[0] >= rangeFloat[1]) { + log.warn("幸运区间配置异常: [{}, {}],强制交换", rangeFloat[0], rangeFloat[1]); + int temp = rangeFloat[0]; + rangeFloat[0] = rangeFloat[1]; + rangeFloat[1] = temp; + // 如果交换后仍然相等,设置默认值 + if (rangeFloat[0] >= rangeFloat[1]) { + rangeFloat[0] = 0; + rangeFloat[1] = 160; + } + } + + // 创建固定概率:进度初值 initVal,空间大小在 [min, max) 内随机 + Integer[] newRangeFixed = new Integer[2]; + newRangeFixed[0] = initVal; + newRangeFixed[1] = ThreadLocalRandom.current().nextInt(rangeFloat[1] - rangeFloat[0]) + rangeFloat[0]; + + return newRangeFixed; + } + + /** + * 升级游戏结果包装:携带胜负与本轮实际目标进度(fullV),用于计算本轮目标金额快照。 + */ + private static class UpgradePlayResult { + private final boolean win; + private final Integer targetProgress; + + UpgradePlayResult(boolean win, Integer targetProgress) { + this.win = win; + this.targetProgress = targetProgress; + } + + boolean isWin() { + return win; + } + + Integer getTargetProgress() { + return targetProgress; + } + } + + /** + * 安全解析 Redis 中缓存的概率区间对象为 Integer[2]。 + * 缓存不存在、解析失败或元素缺失时返回 null,避免 [null, null] 被误判为有效区间。 + */ + private Integer[] parseRange(Object cacheObj) { + if (cacheObj == null) { + return null; + } + try { + List list = JSONUtil.toList(JSONUtil.parseArray(cacheObj), Integer.class); + if (list.size() < 2 || list.get(0) == null || list.get(1) == null) { + return null; + } + return new Integer[]{list.get(0), list.get(1)}; + } catch (Exception e) { + log.warn("解析幸运升级概率区间失败: {}", cacheObj, e); + return null; + } + } + + /** + * 判断概率区间是否有效:非空且两个元素都不为 null。 + */ + private boolean isValidRange(Integer[] range) { + return range != null && range.length >= 2 && range[0] != null && range[1] != null; + } + + // 扣款 + public R> upgradeAccounting(BigDecimal consumption, TtUser player) { + R updateResult = userService.updateUserAccount(player.getUserId(), consumption.negate(), + TtAccountRecordSource.GOLD_DEVICE); + if (R.isError(updateResult)) { + return R.fail(updateResult.getMsg()); + } + + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate.eq(TtUser::getUserId, player.getUserId()); + + Map map = new HashMap<>(); + map.put("Amount", updateResult.getData().getAccountAmount()); + map.put("Credits", updateResult.getData().getAccountCredits()); + map.put("total", consumption); + + return R.ok(map); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiRedPackServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiRedPackServiceImpl.java new file mode 100644 index 0000000..8783b4a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiRedPackServiceImpl.java @@ -0,0 +1,288 @@ +package com.ruoyi.playingmethod.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.ruoyi.admin.mapper.TtRedPacketMapper; +import com.ruoyi.admin.mapper.TtRedPacketRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtRedPackService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRedPack; +import com.ruoyi.domain.other.TtRedPacket; +import com.ruoyi.domain.other.TtRedPacketRecord; +import com.ruoyi.domain.param.RedPackRecordParam; +import com.ruoyi.playingmethod.service.ApiRedPackService; +import com.ruoyi.thirdparty.wechat.domain.PtLoginUser; +import com.ruoyi.thirdparty.wechat.entity.TtCoinRechargeParam; +import com.ruoyi.thirdparty.wechat.entity.TtCoinRechargeRecord; +import com.ruoyi.thirdparty.wechat.service.ITtCoinRechargeRecordService; +import com.ruoyi.thirdparty.wechat.service.ITtCoinRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class ApiRedPackServiceImpl implements ApiRedPackService { + + @Autowired + private TtRedPackService ttRedPackService; + + @Autowired + private ITtCoinRechargeRecordService coinRechargeRecordService; + + @Autowired + private ITtCoinRecordService recordService; + + @Autowired + private TtUserService ttUserService; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private TtRedPacketRecordMapper ttRedPacketRecordMapper; + + @Autowired + private TtRedPacketMapper ttRedPacketMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private RedisLock redisLock; + + @Override + public List getRedPackList(PtLoginUser user, Integer type) { + TtRedPack redPackParam = new TtRedPack(); + // redPackParam.setStatus(1L); + // redPackParam.setPackType(type + ""); + return ttRedPackService.selectTtRedPackList(redPackParam); + } + + @Override + @Transactional + public AjaxResult getRedPack(Long userId, Long redPackId) { + TtUser user = ttUserService.getById(userId); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + TtRedPack redPack = ttRedPackService.selectTtRedPackById(redPackId); + TtCoinRechargeParam rechargeParam = new TtCoinRechargeParam(); + rechargeParam.setUid(user.getUserId().longValue()); + rechargeParam.setBeginTime(format.format(redPack.getBeginTime())); + rechargeParam.setEndTime(format.format(redPack.getEndTime())); + rechargeParam.setCoin(redPack.getRechargeLimit()); + + // 查询用户在指定时间内的大于最小充值金额的充值记录 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TtUserBlendErcash::getUserId, rechargeParam.getUid()) + .eq(TtUserBlendErcash::getType, 1) + .eq(TtUserBlendErcash::getSource, 0) + .gt(TtUserBlendErcash::getTotal, rechargeParam.getCoin()) + .gt(TtUserBlendErcash::getCreateTime, rechargeParam.getBeginTime()) + .lt(TtUserBlendErcash::getCreateTime, rechargeParam.getEndTime()); + List rechargeRecords = ttUserBlendErcashMapper.selectList(queryWrapper); + + // 根据红包ID和用户ID查询红包记录,检查用户是否已经领取过某个特定的红包 + LambdaQueryWrapper ttRedPacketRecordWrapper = new LambdaQueryWrapper<>(); + ttRedPacketRecordWrapper + .eq(TtRedPacketRecord::getRedPacketId, redPack.getId()) + .eq(TtRedPacketRecord::getUserId, user.getUserId()); + List redPackRecords = ttRedPacketRecordMapper.selectList(ttRedPacketRecordWrapper); + if (redPackRecords.size() >= rechargeRecords.size()) { + return AjaxResult.error("可领取次数不足"); + } + // TODO 减红包剩余数 + String lockName = "RED_PACK_LOCK:" + redPackId; + boolean lock = false; + try { + // 尝试获取锁,最多尝试10次,每次等待3秒,锁定时间7秒 + for (int i = 0; i < 10; i++) { + lock = redisLock.tryLock(lockName, 3, 7, TimeUnit.SECONDS); + if (lock) { + break; + } + } + + if (!lock) { + return AjaxResult.error("服务器繁忙,稍后重试。"); + } + + // 执行业务逻辑 + TtRedPack ttRedPack = ttRedPackService.selectTtRedPackById(redPackId); + if (ttRedPack.getValidCount() <= 0) { + return AjaxResult.error("红包已被领完"); + } else { + ttRedPack.setValidCount(ttRedPack.getValidCount() - 1); + ttRedPackService.updateTtRedPack(ttRedPack); + } + } finally { + if (lock) { + redisLock.unlock(lockName); + } + } + + TtRedPacketRecord ttRedPackRecord = new TtRedPacketRecord(); + + BigDecimal randomValue = RandomUtils.getRandomaBigdecimal(redPack.getValueMin(), redPack.getValueMax()); + + ttRedPackRecord.setRedPacketId(redPackId.intValue()); + ttRedPackRecord.setUserId(user.getUserId()); + ttRedPackRecord.setReceiveAmount(randomValue); + ttRedPackRecord.setReceiveTime(new Date()); + + ttRedPacketRecordMapper.insert(ttRedPackRecord); + + // 加钱 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_credits = account_credits + " + randomValue); + userService.update(userUpdate); + + // 综合消费日志 + TtUser ttUser = userService.getById(user.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + .credits(randomValue.compareTo(BigDecimal.ZERO) > 0 ? randomValue : null) + .finalCredits(randomValue.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountCredits().add(randomValue) : null) + .total(randomValue.add(randomValue)) // 收支合计 + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.CHARGE_RED_PACKET.getCode()) + .remark(TtAccountRecordSource.CHARGE_RED_PACKET.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + ttUserBlendErcashMapper.insert(blendErcash); + + return AjaxResult.success(ttRedPackRecord); + } + + @Override + @Transactional + public AjaxResult getRedPackByKey(LoginUser user, String redPackKey) { + + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + // 根据KEY查询红包 + LambdaUpdateWrapper selectRedPackByKeyWrapper = new LambdaUpdateWrapper<>(); + selectRedPackByKeyWrapper + .eq(TtRedPacket::getPassword, redPackKey); + TtRedPacket redPack = ttRedPacketMapper.selectOne(selectRedPackByKeyWrapper); + if (redPack == null){ + return AjaxResult.error("未找到红包,请检查口令是否正确"); + } + // 指定领取人的红包(亏损返利CDK)只走 /api/bonus/receiveRedPacket 领取。 + // 这个旧入口(ApiRedPackContoller 已 @Deprecated)不校验收款人、个数、状态、有效期, + // 放行等于绕过收款人绑定,直接拒绝。 + if (redPack.getReceiverUserId() != null && redPack.getReceiverUserId() > 0) { + return AjaxResult.error("请前往「CDK兑换口令页」领取"); + } + TtCoinRechargeParam rechargeParam = new TtCoinRechargeParam(); + rechargeParam.setUid(user.getUserId()); + rechargeParam.setBeginTime(format.format(new Date()) + " 00:00:00"); + rechargeParam.setEndTime(format.format(new Date()) + " 23:59:59"); + + + LambdaQueryWrapper ttRedPacketRecordWrapper = new LambdaQueryWrapper<>(); + ttRedPacketRecordWrapper + .eq(TtRedPacketRecord::getRedPacketId, redPack.getId()) + .eq(TtRedPacketRecord::getUserId, user.getUserId().intValue()); + List redPackRecords = ttRedPacketRecordMapper.selectList(ttRedPacketRecordWrapper); + if (redPackRecords.size() > 0){ + return AjaxResult.error("您已经领取过。"); + } + + TtRedPacketRecord ttRedPackRecord = new TtRedPacketRecord(); + + // 此处解析最小值和最大值[1, 5] + String amount = redPack.getAmount(); + // 去除方括号和空格,得到 "1,5" + String cleanString = amount.replaceAll("\\[|\\]|\\s", ""); + // 分割字符串,得到字符串数组 ["1", "5"] + String[] parts = cleanString.split(","); + // 转换为 BigDecimal + BigDecimal minValue = new BigDecimal(parts[0]); + BigDecimal maxValue = new BigDecimal(parts[1]); + BigDecimal randomValue = RandomUtils.getRandomaBigdecimal(minValue, maxValue); + + ttRedPackRecord.setRedPacketId(redPack.getId()); + ttRedPackRecord.setUserId(user.getUserId().intValue()); + ttRedPackRecord.setReceivePassword(redPackKey); + ttRedPackRecord.setReceiveAmount(randomValue); + ttRedPackRecord.setReceiveTime(new Date()); + + ttRedPacketRecordMapper.insert(ttRedPackRecord); + + // 加钱 + userService.updateOnlyUserAccount(user.getUserId().intValue(), randomValue, TtAccountRecordSource.RECEIVE_RED_PACKET); + + return AjaxResult.success(ttRedPackRecord); + } + + @Override + public List getActivityDataHis(PtLoginUser user) { + RedPackRecordParam redPackRecordParam = new RedPackRecordParam(); + redPackRecordParam.setuId(user.getUserId().intValue()); + LambdaQueryWrapper ttRedPacketRecordWrapper = new LambdaQueryWrapper<>(); + ttRedPacketRecordWrapper + .eq(TtRedPacketRecord::getUserId, user.getUserId().intValue()); + List redPackRecords = ttRedPacketRecordMapper.selectList(ttRedPacketRecordWrapper); + return redPackRecords; + } + + @Override + public AjaxResult getRedPackDataDetail(PtLoginUser user, Long recordId) { + + TtRedPacketRecord ttRedPackRecord = ttRedPacketRecordMapper.selectById(recordId); + if (ttRedPackRecord.getReceivePassword() != null) { + TtRedPacket redPackKey = ttRedPacketMapper.selectById(ttRedPackRecord.getRedPacketId()); + // TODO 缺少属性 + // ttRedPackRecord.setRedPackKey(redPackKey); + } else { + TtRedPack redPack = ttRedPackService.selectTtRedPackById(ttRedPackRecord.getRedPacketId().longValue()); + // TODO 缺少属性 + // ttRedPackRecord.setRedPack(redPack); + } + + return AjaxResult.success(ttRedPackRecord); + } + + /** + * 解析红包最小值和最大值 + */ + public static List parseBigDecimalList(String input) { + // 去掉中括号和空格 + String cleanInput = input.replace("[", "").replace("]", "").replaceAll("\\s+", ""); + + // 按逗号分割成字符串数组 + String[] parts = cleanInput.split(","); + + // 转换为 BigDecimal 对象列表 + List bigDecimals = Arrays.stream(parts) + .map(BigDecimal::new) // 使用 BigDecimal 的构造方法转换为 BigDecimal 对象 + .collect(Collectors.toList()); + + return bigDecimals; + } + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiReplacementRecordServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiReplacementRecordServiceImpl.java new file mode 100644 index 0000000..bfbd848 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiReplacementRecordServiceImpl.java @@ -0,0 +1,198 @@ +package com.ruoyi.playingmethod.service.impl; + +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.dto.packSack.PackSackCondition; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtReplacementRecord; +import com.ruoyi.domain.vo.UserPackSackDataVO; +import com.ruoyi.playingmethod.mapper.ApiReplacementRecordMapper; +import com.ruoyi.playingmethod.service.IApiReplacementRecordService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.wechat.domain.PtLoginUser; +import com.ruoyi.user.mapper.ApiUserPackSackMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.*; + +/** + * 业务端汰换逻辑实现 + * + * @author junhai + */ +@Service +public class ApiReplacementRecordServiceImpl implements IApiReplacementRecordService { + + @Autowired + private ApiReplacementRecordMapper apiReplacementRecordMapper; + + @Autowired + private TtBoxRecordsService ttBoxRecordsService; + + @Autowired + private TtBoxRecordsMapper ttBoxRecordsMapper; + + @Autowired + private ISysConfigService sysConfigService; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + // 概率配置数组 + private static final double[][] percent = { + {0.5, 0.6}, + {0.6, 0.7}, + {0.7, 0.8}, + {0.8, 0.9}, + {0.9, 1}, + {1, 2} + }; + + + @Override + @Transactional + public AjaxResult synthesizeItems(LoginUser user, List itemIds) { + int size = 0; + int size1 = 0; + if (itemIds != null) { + size = itemIds.size(); + } + + int allCount = size + size1; + if (allCount < 3 || allCount > 9) { + return AjaxResult.error("汰换数量在3~9个"); + } + + BigDecimal totalPrice = new BigDecimal(0); + + //查询饰品列表 + // 1.先查询宝箱记录里面的饰品 + if(!itemIds.isEmpty()){ + // 从我的背包中查询饰品总价值 + List boxs = apiReplacementRecordMapper + .selectUserPackSack(user.getUserId().intValue(), itemIds); + for (int i = 0; i < boxs.size(); i++) { + BigDecimal bigDecimal = boxs.get(i).getOrnamentsPrice(); + totalPrice = totalPrice.add(bigDecimal); + } + } + //累加装备总价值 + String status = "8"; + //更新记录的状态 updateStatusByIds + if(!itemIds.isEmpty()){ + ttBoxRecordsMapper.updateStatusByIds(itemIds, status); + } + + //获取最小汰换金额 + BigDecimal minPrice = new BigDecimal(sysConfigService.selectConfigByKey("minExchangeAmount")); + // 汰换饰品合计总价不得低于 + if (totalPrice.compareTo(minPrice) < 0) { + return AjaxResult.error("汰换饰品合计总价不得低于" + minPrice); + } + + //获取饰品的id + Long id = getOId(totalPrice); + //获取饰品详情 + TtOrnament ttOrnament = ttOrnamentMapper.selectOrnamentById(id); + //进行汰换的日志记录 + TtReplacementRecord replacementRecord = new TtReplacementRecord(); + replacementRecord.setUid(user.getUserId()); + replacementRecord.setUname(user.getUserData().getNickName()); + replacementRecord.setAwardOid(ttOrnament.getId()); + replacementRecord.setAwardOname(ttOrnament.getName()); + replacementRecord.setAwardOimg(ttOrnament.getImageUrl()); + replacementRecord.setAwardOprice(ttOrnament.getUsePrice()); + replacementRecord.setTime(DateUtils.getNowDate()); + replacementRecord.setCreateTime(DateUtils.getNowDate()); + apiReplacementRecordMapper.insertTtReplacementRecord(replacementRecord); + //将汰换的奖品加入背包 + TtBoxRecords ttBoxRecords = new TtBoxRecords(); + ttBoxRecords.setSource(TtboxRecordSource.REPLACEMENT.getCode()); + ttBoxRecords.setUserId(user.getUserId().intValue()); + ttBoxRecords.setHolderUserId(user.getUserId().intValue()); + ttBoxRecords.setBoxName("汰换奖品"); + ttBoxRecords.setOrnamentName(ttOrnament.getName()); + ttBoxRecords.setImageUrl(ttOrnament.getImageUrl()); + ttBoxRecords.setOrnamentId(ttOrnament.getId()); + ttBoxRecords.setOrnamentsPrice(ttOrnament.getUsePrice()); + ttBoxRecords.setStatus(0); + ttBoxRecords.setCreateTime(DateUtils.getNowDate()); + ttBoxRecordsService.save(ttBoxRecords); + + return AjaxResult.success(replacementRecord); + } + + + /** + * 随机产生饰品id + * + * @param bean + * @return + */ + public Long getOId(BigDecimal bean) { + List ornament = new ArrayList<>(); + initArray(bean, ornament); + Random random = new Random(); + int randomIndex = random.nextInt(ornament.size()); + return ornament.get(randomIndex).getId(); + } + + /** + * 初始化 饰品列表 + * + * @param bean + * @param ornaments + */ + private void initArray(BigDecimal bean, List ornaments) { + for (double[] range : percent) { + BigDecimal start = bean.multiply(BigDecimal.valueOf(range[0])); + BigDecimal end = bean.multiply(BigDecimal.valueOf(range[1])); + + Map map = new HashMap<>(); + map.put("start", start); + map.put("end", end); + + int configQuantity = 1; + + if (configQuantity > 0) { + List eligibleOrnaments = apiReplacementRecordMapper.findByPriceRange(map); + shuffleList(eligibleOrnaments); + int quantityToAdd = Math.min(configQuantity, eligibleOrnaments.size()); + + for (int i = 0; i < quantityToAdd; i++) { + ornaments.add(eligibleOrnaments.get(i)); + } + } + } + } + + /** + * 采用了 Fisher-Yates 随机洗牌算法, + * + * @param list + */ + private void shuffleList(List list) { + Random random = new Random(); + int n = list.size(); + for (int i = n - 1; i > 0; i--) { + int j = random.nextInt(i + 1); + TtOrnament temp = list.get(i); + list.set(i, list.get(j)); + list.set(j, temp); + } + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiRollServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiRollServiceImpl.java new file mode 100644 index 0000000..f2cb544 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiRollServiceImpl.java @@ -0,0 +1,879 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtOrnamentsLevelService; +import com.ruoyi.admin.service.TtRollUserService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.common.constant.roll.RollGetPrizeWay; +import com.ruoyi.domain.common.constant.roll.RollStatus; +import com.ruoyi.domain.common.constant.roll.RollType; +import com.ruoyi.domain.dto.roll.GetRollOpenPrizeParam; +import com.ruoyi.domain.dto.roll.GetRollPlayersParam; +import com.ruoyi.domain.dto.roll.GetRollPrizePool; +import com.ruoyi.domain.entity.*; +import com.ruoyi.domain.entity.roll.*; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.*; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsVO; +import com.ruoyi.domain.vo.roll.RollUserPrizeVO; +import com.ruoyi.domain.vo.roll.RollUserVO; +import com.ruoyi.domain.vo.roll.SimpleRollOrnamentVO; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.playingmethod.contract.api.request.GetRollListParamRequest; +import com.ruoyi.playingmethod.controller.ApiRollController.JoinRollParam; +import com.ruoyi.playingmethod.mapper.ApiRollMapper; +import com.ruoyi.playingmethod.service.ApiRollService; +import com.ruoyi.domain.vo.RollDetailsDataVO; +import com.ruoyi.domain.vo.RollListDataVO; +import com.ruoyi.message.service.MessageService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON; + +@Service +@Slf4j +public class ApiRollServiceImpl extends ServiceImpl implements ApiRollService { + + private final ApiRollMapper apiRollMapper; + private final TtUserMapper userMapper; + private final TtOrnamentMapper ornamentsMapper; + private final TtBoxRecordsService boxRecordsService; + private final TtRollMapper rollMapper; + private final TtRollUserService rollUserService; + private final TtRollJackpotOrnamentsMapper rollJackpotOrnamentsMapper; + private final TtRechargeRecordMapper rechargeRecordMapper; + private final RedisLock redisLock; + + public ApiRollServiceImpl(ApiRollMapper apiRollMapper, + TtUserMapper userMapper, + TtOrnamentMapper ornamentsMapper, + TtBoxRecordsService boxRecordsService, + TtRollMapper rollMapper, + TtRollUserService rollUserService, + TtRollJackpotOrnamentsMapper rollJackpotOrnamentsMapper, + TtRechargeRecordMapper rechargeRecordMapper, + RedisLock redisLock) { + this.apiRollMapper = apiRollMapper; + this.userMapper = userMapper; + this.ornamentsMapper = ornamentsMapper; + this.boxRecordsService = boxRecordsService; + this.rollMapper = rollMapper; + this.rollUserService = rollUserService; + this.rollJackpotOrnamentsMapper = rollJackpotOrnamentsMapper; + this.rechargeRecordMapper = rechargeRecordMapper; + this.redisLock = redisLock; + } + + @Autowired + private TtRollUserMapper ttRollUserMapper; + + @Autowired + private MessageService messageService; + + @Autowired + private TtRollUserPrizeMapper ttRollUserPrizeMapper; + + @Autowired + private TtRollJackpotMapper ttRollJackpotMapper; + + @Autowired + private TtRollJackpotOrnamentsMapper ttRollJackpotOrnamentsMapper; + + @Autowired + private TtRollMapper ttRollMapper; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private TtBoxRecordsService ttBoxRecordsService; + + @Autowired + private TtOrnamentsLevelMapper ttOrnamentsLevelMapper; + + @Autowired + private TtOrnamentsLevelService ttOrnamentsLevelService; + + @Autowired + private TtBoxRecordsMapper ttBoxRecordsMapper; + + @Autowired + private TtUserService ttUserService; + + public R joinRollCheck(TtRoll ttRoll, TtRollUser rollUser, TtUser player, JoinRollParam param) { + + if (StringUtils.isNull(ttRoll)) return R.fail("当前Roll房已结束"); + if (StringUtils.isNotNull(rollUser)) return R.fail("您当前已在该房间内,请勿重复加入房间!"); + + String password = ttRoll.getRollPassword(); + if (StringUtils.isNotEmpty(password)) { + if (StringUtils.isEmpty(param.getRollPassword())) return R.fail("请输入密码!"); + if (!password.equals(param.getRollPassword())) return R.fail("密码错误!"); + } + + // 检查加入条件:充值下限 + BigDecimal minRecharge = ttRoll.getMinRecharge(); + if (minRecharge.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal rechargeTotalAmount = getRechargeTotalAmount(player.getUserId(), ttRoll.getRechargeStartTime()); + if (minRecharge.compareTo(rechargeTotalAmount) > 0) return R.fail("未满足该房间的充值条件!"); + } + + // 检查加入条件:VIP等级 + Integer vipLevel = ttRoll.getVipLevel(); + if (vipLevel != null && vipLevel > 0) { + Integer userVipLevel = player.getVipLevel(); + if (userVipLevel == null || userVipLevel < vipLevel) { + return R.fail("未满足该房间的VIP等级条件!需要VIP" + vipLevel + "以上。"); + } + } + + return R.ok(); + } + + @Override + public R joinRoll(JoinRollParam param, TtUser player) { + + // roll房 + TtRoll ttRoll = new LambdaQueryChainWrapper<>(rollMapper) + .eq(TtRoll::getId, param.getRollId()) + .eq(TtRoll::getRollStatus, "0") + .gt(TtRoll::getEndTime, DateUtils.getNowDate()) + .one(); + // roll房成员 + TtRollUser rollUser = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper()) + .eq(TtRollUser::getUserId, player.getUserId()) + .eq(TtRollUser::getRollId, param.getRollId()) + .one(); + + // 检查 + R check = joinRollCheck(ttRoll, rollUser, player, param); + if (!check.getCode().equals(200)) return check; + + // 如果是主播房 + if (ttRoll.getRollType().equals(RollType.ANCHOR.getCode())) { + TtUser anchor = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, ttRoll.getUserId()) + .eq(TtUser::getUserType, UserType.ANCHOR.getCode()) + .eq(TtUser::getDelFlag, "0") + .one(); + // 检查是否粉丝 + Integer parentId = player.getParentId(); + if (StringUtils.isNull(parentId) || !anchor.getUserId().equals(parentId)) { + return R.fail("您不是该主播的粉丝,请先加入主播粉丝团才可加入房间!"); + } + } + + // 加入房间 + Boolean lock = false; + for (int i = 0; i < 2; i++) { + + lock = redisLock.tryLock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId(), 2L, 7L, TimeUnit.SECONDS); + + if (!lock) { + try { + TimeUnit.SECONDS.sleep(1L); + } catch (Exception e) { + log.warn("try lock sleep warn"); + } finally { + continue; + } + } else { + break; + } + + } + + if (lock) { + try { + + List rollUserList = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper()) + .eq(TtRollUser::getRollId, param.getRollId()) + .list(); + + if (ttRoll.getPeopleNum() <= rollUserList.size()) { + return R.fail("roll房人满。"); + } + + // 锁内二次校验:防止并发重复加入 + boolean alreadyJoined = rollUserList.stream() + .anyMatch(ru -> ru.getUserId().equals(player.getUserId())); + if (alreadyJoined) { + return R.fail("您当前已在该房间内,请勿重复加入房间!"); + } + + TtRollUser ttRollUser = TtRollUser.builder() + .rollId(param.getRollId()) + .userId(player.getUserId()) + .userName(player.getUserName()) + .nickName(player.getNickName()) + .avatar(player.getAvatar()) + .joinTime(DateUtils.getNowDate()) + .build(); + + rollUserService.getBaseMapper().insert(ttRollUser); + + return R.ok(); + + } catch (Exception e) { + e.printStackTrace(); + return R.fail("加入失败,请稍后重试。"); + } finally { + redisLock.unlock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId()); + } + } + + redisLock.unlock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId()); + return R.fail("服务器繁忙。"); + + } + + // roll房开奖 + @Override + @Transactional(rollbackFor = Exception.class) + public R endROLL(Integer rollId) { + + log.info("server roll房开奖 rollId={}", rollId); + + // 1 原子性抢占:将状态从0改为1,数据库行锁防止并发重复开奖 + boolean updated = new LambdaUpdateChainWrapper<>(rollMapper) + .eq(TtRoll::getId, rollId) + .eq(TtRoll::getRollStatus, "0") + .eq(TtRoll::getDelFlag, "0") + .set(TtRoll::getRollStatus, RollStatus.END.getCode()) + .set(TtRoll::getUpdateTime, new Date()) + .update(); + if (!updated) { + log.info("roll房{}已开奖或不存在,跳过", rollId); + return R.ok("已开奖,拒绝重复开奖"); + } + + TtRoll ttRoll = rollMapper.selectById(rollId); + + List list = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper()) + .eq(TtRollUser::getRollId, rollId) + .list(); + if (ObjectUtil.isEmpty(list) || list.isEmpty()) { + ttRoll.setRollStatus(RollStatus.END.getCode()); + rollMapper.updateById(ttRoll); + // 无人参与时,主播房退还全部奖池价值的金币和对应积分 + if (RollType.ANCHOR.getCode().equals(ttRoll.getRollType()) + && ttRoll.getUserId() != null && ttRoll.getJackpotId() != null) { + TtRollJackpot jackpot = ttRollJackpotMapper.selectById(ttRoll.getJackpotId()); + if (jackpot != null && jackpot.getTotalPrice() != null + && jackpot.getTotalPrice().compareTo(BigDecimal.ZERO) > 0) { + BigDecimal refundAmount = jackpot.getTotalPrice(); + // 全额退回金币 + R refund = ttUserService.updateOnlyUserAccount( + ttRoll.getUserId(), refundAmount, TtAccountRecordSource.ROLL_UNISSUED_REFUND); + if (R.isError(refund)) { + log.error("Roll房无人参与退款失败 rollId={}, userId={}, amount={}, msg={}", + rollId, ttRoll.getUserId(), refundAmount, refund.getMsg()); + } else { + // 全额退回积分(创建时实际扣除额) + BigDecimal refundCredits = ttRoll.getDeductCredits() != null + ? ttRoll.getDeductCredits() : BigDecimal.ZERO; + if (refundCredits.compareTo(BigDecimal.ZERO) > 0) { + ttUserService.updateUserCredits( + ttRoll.getUserId(), refundCredits, TtAccountRecordSource.ROLL_UNISSUED_REFUND); + } + String amountStr = refundAmount.stripTrailingZeros().toPlainString(); + String creditsStr = refundCredits.stripTrailingZeros().toPlainString(); + String content = "您创建的【" + ttRoll.getRollName() + + "】无人参与,已退回 " + amountStr + " 金币、" + + creditsStr + " 积分至您的账户。"; + messageService.sendNotice( + Long.valueOf(ttRoll.getUserId()), "Roll房", content, resolveRollCategory(ttRoll)); + log.info("Roll房无人参与退款成功 rollId={}, userId={}, amount={}, credits={}", + rollId, ttRoll.getUserId(), refundAmount, refundCredits); + } + } + } + return R.ok("roll房无人参与。"); + } + + // 2 分配系统指定,并返回剩余奖品 + List rollUsers = new LambdaQueryChainWrapper<>(ttRollUserMapper) + .eq(TtRollUser::getRollId, ttRoll.getId()) + .eq(TtRollUser::getGetPrizeWay, RollGetPrizeWay.SYS.getCode()) + .list(); + + log.info("所有参与人员" + rollUsers); + + Map surplusPrize = rollSurplusOrnaments(rollUsers, ttRoll); + + // 3 过滤出未分配奖品的用户 + LambdaQueryWrapper rollUserQuery = new LambdaQueryWrapper<>(); + rollUserQuery + .eq(TtRollUser::getRollId, rollId) + .eq(TtRollUser::getStatus, "0"); + if (!rollUsers.isEmpty()) { + List rollUserIds = rollUsers.stream() + .map(TtRollUser::getId) + .collect(Collectors.toList()); + rollUserQuery.notIn(TtRollUser::getId, rollUserIds); + } + List rollUserList = rollUserService.list(rollUserQuery); + + // 4 打乱用户顺序 + Collections.shuffle(rollUserList); + log.info("过滤系统指定以后" + rollUserList); + + // 5 分配奖品、 + List res = new ArrayList<>(); + + // 预加载所有等级信息,避免循环内逐条查询 + Set levelIds = surplusPrize.values().stream() + .map(TtRollJackpotOrnaments::getOrnamentLevelId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map levelMap = new HashMap<>(); + if (!levelIds.isEmpty()) { + List levels = ttOrnamentsLevelService.listByIds(levelIds); + for (TtOrnamentsLevel lv : levels) { + levelMap.put(lv.getId(), lv); + } + } + + // 一人拿一件 - 创建副本避免并发修改原始对象 + boolean iGetIt = false; + for (TtRollUser rollUser : rollUserList) { + + // if (iGetIt) continue; + + for (Map.Entry entry : surplusPrize.entrySet()) { + + Integer numb = entry.getValue().getOrnamentsNum(); + if (numb <= 0) continue; + + // 等级信息(从预加载 Map 获取) + TtOrnamentsLevel level = levelMap.get(entry.getValue().getOrnamentLevelId()); + + TtBoxRecords boxRecords = TtBoxRecords.builder() + .rollId(rollId) + .createTime(new Timestamp(System.currentTimeMillis())) + .holderUserId(rollUser.getUserId()) + .ornamentId(entry.getKey()) + .ornamentsPrice(entry.getValue().getPrice()) + .imageUrl(entry.getValue().getImgUrl()) + .ornamentName(entry.getValue().getOrnamentName()) + .ornamentLevelImg(level != null ? level.getLevelImg() : null) + .source(TtboxRecordSource.ROLL.getCode()) + .userId(rollUser.getUserId()) + .updateTime(new Timestamp(System.currentTimeMillis())) + .status(IN_PACKSACK_ON.getCode()) + .build(); + res.add(boxRecords); + + log.info("{}获得 {}", rollUser.getNickName(), entry.getValue().getOrnamentName()); + + // 扣减数量 - 使用独立变量避免修改共享对象 + int updatedNum = numb - 1; + if (updatedNum <= 0) { + surplusPrize.remove(entry.getKey()); + } else { + TtRollJackpotOrnaments updatedEntry = new TtRollJackpotOrnaments(); + updatedEntry.setOrnamentLevelId(entry.getValue().getOrnamentLevelId()); + updatedEntry.setPrice(entry.getValue().getPrice()); + updatedEntry.setImgUrl(entry.getValue().getImgUrl()); + updatedEntry.setOrnamentName(entry.getValue().getOrnamentName()); + updatedEntry.setOrnamentsId(entry.getKey()); + updatedEntry.setOrnamentsNum(updatedNum); + updatedEntry.setJackpotId(entry.getValue().getJackpotId()); + surplusPrize.put(entry.getKey(), updatedEntry); + } + + break; + + } + + } + + log.info("保存开奖结果:" + res); + + boxRecordsService.saveBatch(res); + + // ===== 新增:主播房开奖后,未发放饰品价值退回创建者(主播),并发送退款通知 ===== + refundUnissuedOrnaments(ttRoll, surplusPrize); + // ===== 新增退款结束 ===== + + // ===== 新增:Roll房开奖通知(过滤机器人用户 userType=03,覆盖中奖与未中奖用户)===== + // 事务化后,开奖通知需在事务提交后再异步发送,否则通知线程读取不到本事务尚未提交的中奖记录 + final TtRoll noticeRoll = ttRoll; + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + AsyncManager.me().run(() -> sendRollResultNotice(noticeRoll)); + } + }); + } else { + AsyncManager.me().run(() -> sendRollResultNotice(noticeRoll)); + } + // ===== 新增roll房开奖通知结束 ===== + + // 6 状态已在方法开头原子更新,无需重复更新 + log.info("roll房开奖成功 rollId={} rollName={}", ttRoll.getId(), ttRoll.getRollName()); + + return R.ok(); + + } + + /** + * 主播房开奖后,将未发放的饰品价值退回给创建者(主播),并发送退款通知。 + * 官方房(rollType=0)创建时不扣费,不退款。 + * + * @param ttRoll Roll房 + * @param surplusPrize 开奖分配后剩余的奖池饰品,value.ornamentsNum 为剩余未发放数量 + */ + private void refundUnissuedOrnaments(TtRoll ttRoll, Map surplusPrize) { + try { + // 仅主播房退款,官方房不扣费也不退款 + if (ttRoll == null || !RollType.ANCHOR.getCode().equals(ttRoll.getRollType()) || ttRoll.getUserId() == null) { + return; + } + if (ObjectUtil.isEmpty(surplusPrize)) { + return; + } + + // 汇总未发放饰品的数量与价值 + int unissuedCount = 0; + BigDecimal refundAmount = BigDecimal.ZERO; + for (TtRollJackpotOrnaments ornament : surplusPrize.values()) { + Integer num = ornament.getOrnamentsNum(); + if (num == null || num <= 0) { + continue; + } + BigDecimal price = ornament.getPrice() == null ? BigDecimal.ZERO : ornament.getPrice(); + unissuedCount += num; + refundAmount = refundAmount.add(price.multiply(BigDecimal.valueOf(num))); + } + + // 全部发放完毕,无需退款 + if (refundAmount.compareTo(BigDecimal.ZERO) <= 0) { + return; + } + + // 退款分母:奖池总价值(对应创建时扣减的金币额) + BigDecimal jackpotTotal = BigDecimal.ZERO; + if (ttRoll.getJackpotId() != null) { + TtRollJackpot jackpot = ttRollJackpotMapper.selectById(ttRoll.getJackpotId()); + if (jackpot != null && jackpot.getTotalPrice() != null) { + jackpotTotal = jackpot.getTotalPrice(); + } + } + + // 积分按未发放价值占奖池总价值的比例,退还创建时实际扣除的积分(避免积分虚增) + BigDecimal deductCredits = ttRoll.getDeductCredits() != null ? ttRoll.getDeductCredits() : BigDecimal.ZERO; + BigDecimal refundCredits = BigDecimal.ZERO; + if (deductCredits.compareTo(BigDecimal.ZERO) > 0 && jackpotTotal.compareTo(BigDecimal.ZERO) > 0) { + refundCredits = deductCredits.multiply(refundAmount) + .divide(jackpotTotal, 2, java.math.RoundingMode.HALF_UP); + } + + // 退回主播金币(未发放价值,不动积分),来源标记为 Roll房未发放退回主播 + R refund = ttUserService.updateOnlyUserAccount(ttRoll.getUserId(), refundAmount, TtAccountRecordSource.ROLL_UNISSUED_REFUND); + if (R.isError(refund)) { + log.error("Roll房未发放饰品退款失败 rollId={}, userId={}, amount={}, msg={}", + ttRoll.getId(), ttRoll.getUserId(), refundAmount, refund.getMsg()); + return; + } + // 按比例退回积分 + if (refundCredits.compareTo(BigDecimal.ZERO) > 0) { + ttUserService.updateUserCredits(ttRoll.getUserId(), refundCredits, TtAccountRecordSource.ROLL_UNISSUED_REFUND); + } + + // 发送退款通知给创建者(主播) + String amountStr = refundAmount.stripTrailingZeros().toPlainString(); + String creditsStr = refundCredits.stripTrailingZeros().toPlainString(); + String content = "您创建的【" + ttRoll.getRollName() + "】已开奖,本次有 " + unissuedCount + + " 件饰品未发放,已退回价值 " + amountStr + " 金币、" + creditsStr + " 积分至您的账户。"; + messageService.sendNotice(Long.valueOf(ttRoll.getUserId()), "Roll房", content, resolveRollCategory(ttRoll)); + + log.info("Roll房未发放饰品退款成功 rollId={}, userId={}, count={}, amount={}", + ttRoll.getId(), ttRoll.getUserId(), unissuedCount, refundAmount); + } catch (Exception e) { + log.error("Roll房未发放饰品退款异常 rollId={}, error={}", + ttRoll == null ? null : ttRoll.getId(), e.getMessage(), e); + } + } + + //{ornamentId:TtRollJackpotOrnaments} TtRollJackpotOrnaments中有数量 + public Map rollSurplusOrnaments(List rollUsers, TtRoll ttRoll) { + + // 奖池所有物品 + List jackpotOrnaments = new LambdaQueryChainWrapper<>(rollJackpotOrnamentsMapper) + .eq(TtRollJackpotOrnaments::getJackpotId, ttRoll.getJackpotId()) + .list(); + + HashMap AllPrizePool = new HashMap<>(); + for (TtRollJackpotOrnaments i : jackpotOrnaments) { + AllPrizePool.put(i.getOrnamentsId(), i); + } + + // 没有系统指定,直接返回奖池所有物品 + if (rollUsers.isEmpty()) return AllPrizePool; + + List rollUserIds = rollUsers.stream() + .map(TtRollUser::getId) + .collect(Collectors.toList()); + // 系统指定信息 + List RollUserPrizeVOs = ttRollUserPrizeMapper.byRollUserIds(rollUserIds); + // 没有系统指定,直接返回奖池所有物品 + if (RollUserPrizeVOs.isEmpty()) return AllPrizePool; + + // 分配系统指定 + List res = new ArrayList<>(); + for (RollUserPrizeVO userPrize : RollUserPrizeVOs) { + + // 分配奖品 + for (int i = 0; i < userPrize.getNumber(); i++) { + + TtBoxRecords boxRecords = TtBoxRecords.builder() + .rollId(ttRoll.getId()) + .createTime(new Timestamp(System.currentTimeMillis())) + .holderUserId(userPrize.getUserId()) + .ornamentsPrice(userPrize.getPrice()) + .imageUrl(userPrize.getImgUrl()) + .ornamentId(userPrize.getOrnamentId()) + .ornamentName(userPrize.getOrnamentName()) + .source(TtboxRecordSource.ROLL.getCode()) + .userId(userPrize.getUserId()) + .updateTime(new Timestamp(System.currentTimeMillis())) + .status(IN_PACKSACK_ON.getCode()) + .build(); + res.add(boxRecords); + + } + + // 在所有奖品中减去当前用户获得的奖品 + for (Long ornamentId : AllPrizePool.keySet()) { + if (ornamentId.equals(userPrize.getOrnamentId())) { + // 减去 + TtRollJackpotOrnaments ornaments = AllPrizePool.get(ornamentId); + int count = ornaments.getOrnamentsNum() - userPrize.getNumber(); + ornaments.setOrnamentsNum(Math.max(count, 0)); + AllPrizePool.put(ornamentId, ornaments); + break; + } + } + + } + + // 批量保存已分配的奖品 + log.info("保存系统指定奖品:{}", res.size()); + boxRecordsService.saveBatch(res); + + return AllPrizePool; + } + + @Override + public List getRollList(GetRollListParamRequest param) { + + List rollList = apiRollMapper.getRollList(param); + + if (CollectionUtils.isEmpty(rollList)) { + return new ArrayList<>(); + } + // 对数据进行处理 + rollList.forEach(item -> { + // VIP等级门槛直接返回数字,空值默认0 + if (item.getVipLevel() == null) { + item.setVipLevel(0); + } + + // 补充奖池信息 + TtRoll roll = this.getById(item.getId()); + List prizeList = ttRollJackpotOrnamentsMapper.rollShow(roll.getJackpotId()); + // 直接设置对象列表,便于前端直接使用,无需额外 JSON.parse + item.setJackpotOrnamentsDataList(prizeList); + Integer ornamentsNumberOfRoll = null; + if (CollectionUtils.isNotEmpty(prizeList)) { + ornamentsNumberOfRoll = ttRollJackpotOrnamentsMapper.ornamentsNumberOfRoll(prizeList.get(0).getJackpotId()); + } + // 空奖池或统计为空时默认0,避免前端 ornamentsNum.toString() 空指针崩溃 + item.setOrnamentsNum(ornamentsNumberOfRoll == null ? 0 : ornamentsNumberOfRoll); + + if (!StringUtils.isBlank(item.getRollPassword())) { + item.setHasPW(true); + item.setRollPassword(null); + } else { + item.setHasPW(false); + } + }); + + return rollList; + } + + @Override + public R getRollDetails(Integer rollId, Long userId) { + + TtRoll ttRoll = getById(rollId); + if (ObjectUtil.isEmpty(ttRoll)) return R.fail("不存在的roll房。"); + + RollDetailsDataVO data = apiRollMapper.getRollDetails(rollId); + data.setHasPW(!StringUtils.isBlank(ttRoll.getRollPassword())); + + // 判断当前登录用户是否已参与 + boolean joined = false; + if (userId != null) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(TtRollUser::getRollId, rollId); + wrapper.eq(TtRollUser::getUserId, userId.intValue()); + joined = ttRollUserMapper.selectCount(wrapper) > 0; + } + data.setJoined(joined); + + // 奖池 + List prizeList = ttRollJackpotOrnamentsMapper.rollShow(ttRoll.getJackpotId()); + data.setJackpotOrnamentsDataList(prizeList); + + // 获奖名单 + // List boxRecords = ttBoxRecordsMapper.rollOpenPrize(rollId); + // data.setOpenPrizeList(boxRecords); + return R.ok(data); + } + + @Override + public R> getRollPlayers(GetRollPlayersParam param) { + + param.setLimit((param.getPage() - 1) * param.getSize()); + + List collect = ttRollUserMapper.pageByRollId(param); + + return R.ok(collect); + } + + @Override + public R> getRollPrizePool(GetRollPrizePool param) { + + param.setLimit((param.getPage() - 1) * param.getSize()); + + TtRoll one = new LambdaQueryChainWrapper<>(ttRollMapper) + .eq(TtRoll::getId, param.getRollId()) + .eq(TtRoll::getDelFlag, 0) + .one(); + if (ObjectUtil.isNull(one)) return R.fail("不存在的roll房。"); + + param.setJackpotId(one.getJackpotId()); + List list = ttRollJackpotOrnamentsMapper.listByRollId(param); + + return R.ok(list); + } + + @Override + public R> getRollOpenPrize(GetRollOpenPrizeParam param) { + + param.setLimit((param.getPage() - 1) * param.getSize()); + + // 获奖名单 + List boxRecords = ttBoxRecordsMapper.rollOpenPrize(param); + return R.ok(boxRecords); + } + + private TtBoxRecords addBoxRecordsData(Integer rollId, Long ornamentsId, Integer userId, TtRollJackpotOrnaments rollJackpotOrnaments) { + TtOrnament ttOrnament = new LambdaQueryChainWrapper<>(ornamentsMapper).eq(TtOrnament::getId, ornamentsId).one(); + TtBoxRecords boxRecords = TtBoxRecords.builder().build(); + boxRecords.setUserId(userId); + boxRecords.setOrnamentId(ornamentsId); + boxRecords.setOrnamentsPrice(ttOrnament.getUsePrice()); + boxRecords.setOrnamentsLevelId(rollJackpotOrnaments.getOrnamentLevelId()); + boxRecords.setStatus(IN_PACKSACK_ON.getCode()); + boxRecords.setCreateTime(DateUtils.getNowDate()); + boxRecords.setSource(TtboxRecordSource.ROLL.getCode()); + boxRecords.setRollId(rollId); + boxRecords.setHolderUserId(userId); + return boxRecords; + } + + private TtRollJackpotOrnaments getRollJackpotOrnaments(Long ornamentsId, Map map, Integer jackpotId) { + TtRollJackpotOrnaments ttRollJackpotOrnaments = map.get(ornamentsId); + if (ttRollJackpotOrnaments == null) { + ttRollJackpotOrnaments = new LambdaQueryChainWrapper<>(rollJackpotOrnamentsMapper).eq(TtRollJackpotOrnaments::getJackpotId, jackpotId) + .eq(TtRollJackpotOrnaments::getOrnamentsId, ornamentsId).one(); + map.put(ornamentsId, ttRollJackpotOrnaments); + } + return ttRollJackpotOrnaments; + } + + private List getSurplusOrnamentsList(Integer rollId, Integer jackpotId) { + + // roll房奖池 + List ornamentsList = new LambdaQueryChainWrapper<>(rollJackpotOrnamentsMapper) + .eq(TtRollJackpotOrnaments::getJackpotId, jackpotId) + .list(); + + Map data = new HashMap<>(); + for (TtRollJackpotOrnaments ornaments : ornamentsList) { + data.put(ornaments.getOrnamentsId(), ornaments.getOrnamentsNum()); + } + + // 打乱奖池 + List surplusOrnamentsList = RandomUtils.toList(data); + + // 系统指定的获奖者 + List allocatedRollUserList = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper()) + .eq(TtRollUser::getRollId, rollId) + .eq(TtRollUser::getStatus, "2") + .list(); + List allocated = allocatedRollUserList.stream().map(TtRollUser::getOrnamentsId).collect(Collectors.toList()); + + for (Long i : allocated) { + Iterator iterator = surplusOrnamentsList.iterator(); + while (iterator.hasNext()) { + Long next = iterator.next(); + if (Objects.equals(next, i)) { + iterator.remove(); + break; + } + } + } + return surplusOrnamentsList; + } + + private BigDecimal getRechargeTotalAmount(Integer userId, Date rechargeStartTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtRechargeRecord::getUserId, userId) + .eq(TtRechargeRecord::getStatus, "0") + .eq(TtRechargeRecord::getDelFlag, "0") + .and(w -> w.isNull(TtRechargeRecord::getIsBlankCard).or().ne(TtRechargeRecord::getIsBlankCard, 1)); + if (StringUtils.isNotNull(rechargeStartTime)) wrapper.gt(TtRechargeRecord::getCreateTime, rechargeStartTime); + return rechargeRecordMapper.selectList(wrapper).stream() + .map(e -> e.getAmountActuallyPaid() == null ? BigDecimal.ZERO : e.getAmountActuallyPaid()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + /** + * Roll房开奖结果通知(过滤机器人用户) + * 中奖用户:您参与的【官方Roll房/主播Roll房】已开奖,获得“龙狙(价值 ¥12,000)”,已放入背包。 + * 未中奖用户:您参与的【官方Roll房/主播Roll房】已开奖,本次未中奖,感谢您的参与。 + */ + private void sendRollResultNotice(TtRoll ttRoll) { + try { + Integer rollId = ttRoll.getId(); + + // 1. 查询该 roll 房所有真实参与用户(过滤机器人) + List rollUserList = new LambdaQueryChainWrapper<>(ttRollUserMapper) + .eq(TtRollUser::getRollId, rollId) + .list(); + if (ObjectUtil.isEmpty(rollUserList)) { + log.warn("Roll 房无参与用户,跳过的开奖通知 rollId={}", rollId); + return; + } + List allParticipantIds = rollUserList.stream() + .map(TtRollUser::getUserId) + .distinct() + .collect(Collectors.toList()); + List realUsers = new LambdaQueryChainWrapper<>(userMapper) + .in(TtUser::getUserId, allParticipantIds) + .ne(TtUser::getUserType, UserType.ROBOT_USER.getCode()) + .list(); + if (ObjectUtil.isEmpty(realUsers)) { + return; + } + + // 2. 汇总全部中奖记录(包含系统指定 + 随机分配,从数据库查询保证完整) + List allWinRecords = new LambdaQueryChainWrapper<>(ttBoxRecordsMapper) + .eq(TtBoxRecords::getRollId, rollId) + .eq(TtBoxRecords::getSource, TtboxRecordSource.ROLL.getCode()) + .list(); + fillMissingOrnamentNames(allWinRecords); + Map> userItemsMap = allWinRecords.stream() + .filter(r -> r.getHolderUserId() != null) + .collect(Collectors.groupingBy(TtBoxRecords::getHolderUserId)); + + // 3. 给每个真实用户发送通知 + String rollTypeName = RollType.OFFICIAL.getCode().equals(ttRoll.getRollType()) ? "官方Roll房" : "主播Roll房"; + String category = resolveRollCategory(ttRoll); + for (TtUser user : realUsers) { + List items = userItemsMap.get(user.getUserId()); + String contentStr; + if (items == null || items.isEmpty()) { + contentStr = "您参与的【" + rollTypeName + "】已开奖,本次未中奖,感谢您的参与。"; + } else { + // 一个账号理论上只获得一件饰品,取第一件 + TtBoxRecords item = items.get(0); + String priceStr = item.getOrnamentsPrice() == null ? "0" + : item.getOrnamentsPrice().stripTrailingZeros().toPlainString(); + contentStr = "您参与的【" + rollTypeName + "】已开奖,获得“" + + item.getOrnamentName() + "(价值 ¥" + priceStr + ")”,已放入背包。"; + } + messageService.sendNotice(Long.valueOf(user.getUserId()), "Roll房", contentStr, category); + } + } catch (Exception e) { + log.error("Roll 房开奖通知推送失败,rollId={}, error={}", + ttRoll == null ? null : ttRoll.getId(), e.getMessage(), e); + } + } + + /** + * 根据 Roll房类型解析消息分类 + */ + private String resolveRollCategory(TtRoll roll) { + if (roll == null) { + return MessageCategoryEnum.SYS_ROLL_STREAMER.getCode(); + } + return RollType.OFFICIAL.getCode().equals(roll.getRollType()) + ? MessageCategoryEnum.SYS_ROLL_OFFICIAL.getCode() + : MessageCategoryEnum.SYS_ROLL_STREAMER.getCode(); + } + + /** + * 防御性补充中奖记录中的空饰品名称 + */ + private void fillMissingOrnamentNames(List records) { + if (ObjectUtil.isEmpty(records)) { + return; + } + Set missingIds = records.stream() + .filter(r -> StringUtils.isEmpty(r.getOrnamentName()) && r.getOrnamentId() != null) + .map(TtBoxRecords::getOrnamentId) + .collect(Collectors.toSet()); + if (missingIds.isEmpty()) { + return; + } + List ornaments = new LambdaQueryChainWrapper<>(ornamentsMapper) + .in(TtOrnament::getId, missingIds) + .list(); + Map nameMap = new HashMap<>(); + for (TtOrnament ornament : ornaments) { + String name = StringUtils.isNotEmpty(ornament.getShortName()) ? ornament.getShortName() : ornament.getName(); + nameMap.put(ornament.getId(), StringUtils.isNotEmpty(name) ? name : "未知饰品"); + } + for (TtBoxRecords record : records) { + if (StringUtils.isEmpty(record.getOrnamentName()) && record.getOrnamentId() != null) { + record.setOrnamentName(nameMap.getOrDefault(record.getOrnamentId(), "未知饰品")); + } + } + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiUpgradeRecordServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiUpgradeRecordServiceImpl.java new file mode 100644 index 0000000..874b807 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiUpgradeRecordServiceImpl.java @@ -0,0 +1,171 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.upgrade.MyUpgradeRecordRequest; +import com.ruoyi.domain.dto.upgrade.UpgradeCondition; +import com.ruoyi.domain.dto.upgrade.UpgradeRecordPageRequest; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.*; +import com.ruoyi.domain.vo.ApiLuckyUpgradeRecentPullVO; +import com.ruoyi.domain.vo.ApiUpgradeRecordItemVO; +import com.ruoyi.playingmethod.mapper.ApiUpgradeRecordMapper; + +import com.ruoyi.playingmethod.service.ApiUpgradeRecordService; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + + +@Service +@Slf4j +public class ApiUpgradeRecordServiceImpl extends ServiceImpl implements ApiUpgradeRecordService { + + + @Autowired + TtUserService userService; + + @Override + public R historyDetail(UpgradeCondition param) { + + Page pageInfo = new Page<>(param.getPage(), param.getSize()); + pageInfo.setOptimizeCountSql(false); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + wrapper.eq(ObjectUtil.isNotNull(param.getUpgradeRecordId()),TtUpgradeRecord::getId,param.getUpgradeRecordId()); + + wrapper.eq(ObjectUtil.isNotNull(param.getOrnamentId()),TtUpgradeRecord::getTargetOrnamentId,param.getOrnamentId()); + + wrapper.eq(ObjectUtil.isNotNull(param.getUserType()),TtUpgradeRecord::getUserType,param.getUserType()); + + wrapper.eq(ObjectUtil.isNotNull(param.getUserId()),TtUpgradeRecord::getUserId,param.getUserId()); + wrapper.orderByDesc(TtUpgradeRecord::getOpenTime); + + pageInfo = this.page(pageInfo, wrapper); + + if (pageInfo.getRecords().size() == 0){ + return R.ok(pageInfo); + } + + Set userIds = pageInfo.getRecords().stream().map(TtUpgradeRecord::getUserId).collect(Collectors.toSet()); + + List list = userService.list(Wrappers.lambdaQuery(TtUser.class) + .in(TtUser::getUserId, userIds)); + Map avatarMap = new HashMap<>(); + list.forEach(u -> avatarMap.put(u.getUserId(), u.getAvatar())); + pageInfo.getRecords().forEach(r->{ + if (r.getUserId() !=null){ + r.setUserAvatar(avatarMap.get(r.getUserId())); + } + // gainOrnamentList 使用了 @JsonRawValue,null/空串会导致输出非法 JSON,此处兑底为 "[]" + if (r.getGainOrnamentList() == null || r.getGainOrnamentList().trim().isEmpty()) { + r.setGainOrnamentList("[]"); + } + }); + + + return R.ok(pageInfo); + + } + + @Override + public R tenTopDetail(Boolean isVictory) { + Page pageInfo = new Page<>(1, 10); + pageInfo.setOptimizeCountSql(false); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(isVictory != null, TtUpgradeRecord::getIsVictory, isVictory); + // 「十佳掉落」语义:按实际获得饰品总价值倒序取 Top 10, + // 使用 gain_ornaments_price(实际掉落价值)而非 target_ornament_price(目标饰品价值,升级失败也会计入) + wrapper.orderByDesc(TtUpgradeRecord::getGainOrnamentsPrice); + pageInfo = this.page(pageInfo, wrapper); + if (pageInfo.getRecords().size() == 0){ + return R.ok(pageInfo); + } + Set userIds = pageInfo.getRecords().stream().map(TtUpgradeRecord::getUserId).collect(Collectors.toSet()); + List list = userService.list(Wrappers.lambdaQuery(TtUser.class) + .in(TtUser::getUserId, userIds)); + Map avatarMap = new HashMap<>(); + list.forEach(u -> avatarMap.put(u.getUserId(), u.getAvatar())); + pageInfo.getRecords().forEach(r->{ + if (r.getUserId() !=null){ + r.setUserAvatar(avatarMap.get(r.getUserId())); + } + }); + return R.ok(pageInfo); + } + + @Override + public R myHistoryRecord(Integer userId, MyUpgradeRecordRequest request) { + if (userId == null) { + return R.fail(401, "登录过期,请重新登录。"); + } + + Integer pageNum = request.getPage() == null ? 1 : request.getPage(); + Integer pageSize = request.getSize() == null ? 10 : request.getSize(); + + Page pageInfo = new Page<>(pageNum, pageSize); + // 单表单条件查询,交由MyBatis-Plus自动优化 COUNT(*) 、去除 ORDER BY + pageInfo.setOptimizeCountSql(true); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + // 必选:仅查当前登录用户的记录(依赖复合索引 (user_id, open_time)) + // 不区分升级成功/失败,全部记录返回 + wrapper.eq(TtUpgradeRecord::getUserId, userId); + wrapper.orderByDesc(TtUpgradeRecord::getOpenTime); + + pageInfo = this.page(pageInfo, wrapper); + + if (pageInfo.getRecords().isEmpty()) { + return R.ok(pageInfo); + } + + // 同一用户记录,避免 N+1:仅查询一次用户信息,复用到所有记录上 + TtUser user = userService.getById(userId); + String avatar = user == null ? null : user.getAvatar(); + String nickName = user == null ? null : (user.getNickName() != null ? user.getNickName() : user.getUserName()); + pageInfo.getRecords().forEach(r -> { + r.setUserAvatar(avatar); + if (r.getNickName() == null) { + r.setNickName(nickName); + } + }); + + return R.ok(pageInfo); + } + + @Override + public List getRecentPulls(Integer upgradeId) { + return baseMapper.getRecentPulls(upgradeId); + } + + @Override + public R recordPage(Integer userId, UpgradeRecordPageRequest req) { + Integer pageNum = req.getPage() == null ? 1 : req.getPage(); + Integer pageSize = req.getSize() == null ? 10 : req.getSize(); + + Page page = new Page<>(pageNum, pageSize); + // 存在无过滤的 LEFT JOIN,交由 MyBatis-Plus 优化 COUNT(*)(去除 JOIN 与 ORDER BY) + page.setOptimizeCountSql(true); + + // itemScope=1(当前饰品) 才按 upgradeId 过滤;否则查全部饰品 + Long upgradeId = Integer.valueOf(1).equals(req.getItemScope()) ? req.getUpgradeId() : null; + + baseMapper.selectRecordPage(page, userId, upgradeId); + return R.ok(page); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiUserBlendErcashServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiUserBlendErcashServiceImpl.java new file mode 100644 index 0000000..9f3bbb0 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiUserBlendErcashServiceImpl.java @@ -0,0 +1,18 @@ +package com.ruoyi.playingmethod.service.impl; + + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.playingmethod.mapper.ApiTtUserBlendErcashMapper; + +import com.ruoyi.playingmethod.service.ApiUserBlendErcashService; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; + + +@Service +@Slf4j +public class ApiUserBlendErcashServiceImpl extends ServiceImpl implements ApiUserBlendErcashService { +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiWelfareMonthlyRechargesServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiWelfareMonthlyRechargesServiceImpl.java new file mode 100644 index 0000000..1b67820 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiWelfareMonthlyRechargesServiceImpl.java @@ -0,0 +1,147 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.ruoyi.admin.mapper.TtWelfareMonthlyRechargesMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtOrnamentsA; +import com.ruoyi.domain.other.TtWelfareMonthlyRecharges; +import com.ruoyi.domain.vo.OpenBoxVO; +import com.ruoyi.playingmethod.mapper.ApiBindBoxMapper; +import com.ruoyi.playingmethod.mapper.ApiWelfareMonthlyRechargesMapper; +import com.ruoyi.playingmethod.model.ApiWelfare; +import com.ruoyi.playingmethod.model.ApiWelfareRecord; +import com.ruoyi.playingmethod.model.vo.ApiWelfareMonthlyRechargesVO; +import com.ruoyi.playingmethod.service.ApiWelfareMonthlyRechargesService; +import com.ruoyi.playingmethod.utils.customException.OrnamentNullException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +@Service +public class ApiWelfareMonthlyRechargesServiceImpl implements ApiWelfareMonthlyRechargesService { + + @Autowired + private ApiWelfareMonthlyRechargesMapper apiWelfareMonthlyRechargesMapper; + + @Autowired + private TtUserService ttUserService; + + @Autowired + private TtBoxService ttBoxService; + + @Autowired + private LotteryMachine lotteryMachine; + + @Autowired + private ApiBindBoxMapper bindBoxMapper; + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private TtWelfareMonthlyRechargesMapper ttWelfareMonthlyRechargesMapper; + + @Override + public List getWelfareList(Long userId) { + List apiWelfareMonthlyRechargesVOS = + apiWelfareMonthlyRechargesMapper.getWelfareList(userId); + for (ApiWelfareMonthlyRechargesVO apiWelfareMonthlyRechargesVO : apiWelfareMonthlyRechargesVOS) { + // 检查领取条件 + // BigDecimal rechargeRecordsOfMonth = apiWelfareMonthlyRechargesMapper.selectRechargeRecordsOfMonth(userId); + // if (rechargeRecordsOfMonth.compareTo(apiWelfareMonthlyRechargesVO.getAmount()) >= 0) { + // apiWelfareMonthlyRechargesVO.setEligible("1"); + // } else { + // apiWelfareMonthlyRechargesVO.setEligible("0"); + // } + if (checkEligible(apiWelfareMonthlyRechargesVO.getWelfareId(), userId)) + apiWelfareMonthlyRechargesVO.setEligible("1"); + else + apiWelfareMonthlyRechargesVO.setEligible("0"); + // 检查是否领取 + boolean isClaimed = apiWelfareMonthlyRechargesMapper.checkClaimed( + apiWelfareMonthlyRechargesVO.getWelfareId(), + userId, + "2"); + apiWelfareMonthlyRechargesVO.setClaimStatus(isClaimed ? "1" : "0"); + } + return apiWelfareMonthlyRechargesVOS; + } + + @Override + public OpenBoxVO claimWelfare(Integer welfareId, Long userId) { + // 1.获取福利对应的宝箱信息 + Integer boxId = apiWelfareMonthlyRechargesMapper.getBoxIdByWelfareId(welfareId); + TtUser ttUser = ttUserService.getById(userId); + TtBox ttBox = ttBoxService.getById(boxId); + // 2.抽奖(福利开箱跳过临时爆率) + String ornamentId = lotteryMachine.singleLottery(ttUser, ttBox, true); + if (Objects.isNull(ornamentId)) { + throw new OrnamentNullException("福利开箱未抽中"); + } + // 3.获取宝箱详细信息 + TtOrnamentsA ornamentsData = bindBoxMapper.ornamentsInfo(boxId, ornamentId); + if (Objects.isNull(ornamentsData)) { + throw new OrnamentNullException("福利开箱未抽中"); + } + // 4.构建并存储开箱记录 + TtBoxRecords ttBoxRecords = new TtBoxRecords(); + ttBoxRecords.setUserId(ttUser.getUserId()); + ttBoxRecords.setBoxId(ttBox.getBoxId()); + ttBoxRecords.setBoxName(ttBox.getBoxName()); + ttBoxRecords.setBoxPrice(ttBox.getPrice()); + ttBoxRecords.setOrnamentId(Long.valueOf(ornamentId)); + ttBoxRecords.setOrnamentName(ornamentsData.getName()); + ttBoxRecords.setImageUrl(ornamentsData.getImageUrl()); + ttBoxRecords.setOrnamentsPrice(ornamentsData.getUsePrice()); + ttBoxRecords.setOrnamentsLevelId(ornamentsData.getOrnamentsLevelId()); + ttBoxRecords.setOrnamentLevelImg(ornamentsData.getLevelImg()); + ttBoxRecords.setHolderUserId(ttUser.getUserId()); + ttBoxRecords.setSource(TtboxRecordSource.BLIND_BOX.getCode()); + ttBoxRecords.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); + ttBoxRecords.setCreateTime(new Timestamp(System.currentTimeMillis())); + boxRecordsService.save(ttBoxRecords); + // 5.存储领取记录 + ApiWelfareRecord apiWelfareRecord = new ApiWelfareRecord(); + apiWelfareRecord.setWelfareId(welfareId); + apiWelfareRecord.setUserId(userId); + apiWelfareRecord.setType("2"); + apiWelfareRecord.setCreateTime(new Date()); + apiWelfareMonthlyRechargesMapper.saveClaimWelfareRecord(apiWelfareRecord); + // 6.返回前端数据 + OpenBoxVO openBoxVO = new OpenBoxVO(); + BeanUtil.copyProperties(ttBoxRecords, openBoxVO); + openBoxVO.setUsePrice(openBoxVO.getOrnamentsPrice()); + openBoxVO.setLevelImg(ttBoxRecords.getOrnamentLevelImg()); + return openBoxVO; + } + + @Override + public boolean checkEligible(Integer welfareId, Long userId) { + BigDecimal rechargeRecordsOfMonth = apiWelfareMonthlyRechargesMapper.selectRechargeRecordsOfMonth(userId); + if (Objects.isNull(rechargeRecordsOfMonth)) + return false; + TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges = ttWelfareMonthlyRechargesMapper.selectTtWelfareMonthlyRechargesById(welfareId); + TtUser ttUser = ttUserService.getById(userId); + boolean isRechargeAmountSufficient = rechargeRecordsOfMonth.compareTo(ttWelfareMonthlyRecharges.getAmount()) >= 0; + boolean isVipLevelSufficient = ttUser.getVipLevel() >= ttWelfareMonthlyRecharges.getVipLevel(); + return isRechargeAmountSufficient && isVipLevelSufficient; + } + + @Override + public boolean checkClaimed(Integer welfareId, Long userId) { + return apiWelfareMonthlyRechargesMapper.checkClaimed(welfareId, userId, "2"); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiWelfareServiceImpl.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiWelfareServiceImpl.java new file mode 100644 index 0000000..f24856a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/service/impl/ApiWelfareServiceImpl.java @@ -0,0 +1,354 @@ +package com.ruoyi.playingmethod.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.ruoyi.admin.mapper.TtWelfareMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.util.core.fight.LotteryMachine; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtWelfareBoxInventory; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.other.TtOrnamentsA; +import com.ruoyi.domain.other.TtWelfare; +import com.ruoyi.domain.vo.OpenBoxVO; +import com.ruoyi.playingmethod.mapper.ApiBindBoxMapper; +import com.ruoyi.playingmethod.mapper.ApiWelfareBoxInventoryMapper; +import com.ruoyi.playingmethod.mapper.ApiWelfareMapper; +import com.ruoyi.playingmethod.model.ApiWelfare; +import com.ruoyi.playingmethod.model.ApiWelfareRecord; +import com.ruoyi.playingmethod.service.ApiWelfareService; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.playingmethod.utils.customException.OrnamentNullException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +public class ApiWelfareServiceImpl implements ApiWelfareService { + + @Autowired + private ApiWelfareMapper apiWelfareMapper; + + @Autowired + private TtUserService ttUserService; + + @Autowired + private TtBoxService ttBoxService; + + @Autowired + private LotteryMachine lotteryMachine; + + @Autowired + private ApiBindBoxMapper bindBoxMapper; + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private TtWelfareMapper ttWelfareMapper; + + @Autowired + private ApiWelfareBoxInventoryMapper welfareBoxInventoryMapper; + + @Autowired + private RedisLock redisLock; + + @Override + public List getWelfareList(Long userId) { + List welfareList = apiWelfareMapper.getWelfareList(userId); + for (ApiWelfare apiWelfare : welfareList) { + if ("1".equals(apiWelfare.getType())) { + boolean isEligible = checkVipUpgradeEligibility(userId, apiWelfare.getVipLevel()); + apiWelfare.setEligible(isEligible ? "1" : "0"); + boolean isClaimed = apiWelfareMapper.checkClaimed(apiWelfare.getWelfareId(), userId, "1"); + apiWelfare.setClaimStatus(isClaimed ? "1" : "0"); + } + } + return welfareList; + } + + @Transactional + @Override + public OpenBoxVO claimWelfare(Integer welfareId, Long userId) { + String lockKey = "user:welfare:claim:lock:" + userId + ":" + welfareId; + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + throw new ServiceException("领取进行中,请勿重复操作"); + } + try { + // 1.获取福利对应的宝箱信息 + Integer boxId = apiWelfareMapper.getBoxIdByWelfareId(welfareId); + TtUser ttUser = ttUserService.getById(userId); + TtBox ttBox = ttBoxService.getById(boxId); + // 2.抽奖(福利开箱跳过临时爆率) + String ornamentId = lotteryMachine.singleLottery(ttUser, ttBox, true); + if (Objects.isNull(ornamentId)) { + throw new OrnamentNullException("福利开箱未抽中"); + } + // 3.获取宝箱详细信息 + TtOrnamentsA ornamentsData = bindBoxMapper.ornamentsInfo(boxId, ornamentId); + if (Objects.isNull(ornamentsData)) { + throw new OrnamentNullException("福利开箱未抽中"); + } + // 4.构建并存储开箱记录 + TtBoxRecords ttBoxRecords = new TtBoxRecords(); + ttBoxRecords.setUserId(ttUser.getUserId()); + ttBoxRecords.setBoxId(ttBox.getBoxId()); + ttBoxRecords.setBoxName(ttBox.getBoxName()); + ttBoxRecords.setBoxPrice(ttBox.getPrice()); + ttBoxRecords.setOrnamentId(Long.valueOf(ornamentId)); + ttBoxRecords.setOrnamentName(ornamentsData.getName()); + ttBoxRecords.setImageUrl(ornamentsData.getImageUrl()); + ttBoxRecords.setOrnamentsPrice(ornamentsData.getUsePrice()); + ttBoxRecords.setOrnamentsLevelId(ornamentsData.getOrnamentsLevelId()); + ttBoxRecords.setOrnamentLevelImg(ornamentsData.getLevelImg()); + ttBoxRecords.setHolderUserId(ttUser.getUserId()); + ttBoxRecords.setSource(TtboxRecordSource.WELFARE_BOX.getCode()); + ttBoxRecords.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); + ttBoxRecords.setCreateTime(new Timestamp(System.currentTimeMillis())); + boxRecordsService.save(ttBoxRecords); + // 5.存储领取记录 + ApiWelfareRecord apiWelfareRecord = new ApiWelfareRecord(); + apiWelfareRecord.setWelfareId(welfareId); + apiWelfareRecord.setUserId(userId); + apiWelfareRecord.setType("1"); + apiWelfareRecord.setCreateTime(new Date()); + apiWelfareMapper.saveClaimWelfareRecord(apiWelfareRecord); + // 6.返回前端数据 + OpenBoxVO openBoxVO = new OpenBoxVO(); + BeanUtil.copyProperties(ttBoxRecords, openBoxVO); + openBoxVO.setUsePrice(openBoxVO.getOrnamentsPrice()); + openBoxVO.setLevelImg(ttBoxRecords.getOrnamentLevelImg()); + return openBoxVO; + } finally { + redisLock.unlock(lockKey); + } + } + + @Override + public boolean checkNotEligible(Integer welfareId, Long userId) { + TtWelfare ttWelfare = ttWelfareMapper.selectTtWelfareByWelfareId(welfareId); + if ("1".equals(ttWelfare.getType())) { + return !checkVipUpgradeEligibility(userId, ttWelfare.getVipLevel()); + } + return false; + } + + @Override + public boolean checkClaimed(Integer welfareId, Long userId) { + return apiWelfareMapper.checkClaimed(welfareId, userId, "1"); + } + + // ==================== 福利箱子每日领取 + 开箱 ==================== + + @Transactional + @Override + public TtWelfareBoxInventory claimDailyBox(Long userId) { + String lockKey = "user:welfare:dailybox:lock:" + userId; + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + throw new ServiceException("领取进行中,请勿重复操作"); + } + try { + // 1. 检查今日是否已领取 + if (welfareBoxInventoryMapper.checkTodayClaimed(userId)) { + throw new RuntimeException("今日已领取,请明天再来"); + } + // 2. 获取用户VIP等级 + TtUser ttUser = ttUserService.getById(userId); + Integer vipLevel = ttUser.getVipLevel(); + // 3. 查找对应VIP等级的福利配置 + TtWelfare welfare = ttWelfareMapper.selectTtWelfareByVipLevel(vipLevel); + if (welfare == null) { + throw new RuntimeException("当前VIP等级暂无可领取的福利箱子"); + } + // 4. 写入库存 + TtWelfareBoxInventory inventory = TtWelfareBoxInventory.builder() + .userId(userId) + .welfareId(welfare.getWelfareId()) + .boxId(welfare.getBoxId()) + .status(0) + .claimDate(new Date()) + .createTime(new Date()) + .updateTime(new Date()) + .build(); + welfareBoxInventoryMapper.insert(inventory); + // 5. 存储领取记录 + ApiWelfareRecord apiWelfareRecord = new ApiWelfareRecord(); + apiWelfareRecord.setWelfareId(welfare.getWelfareId()); + apiWelfareRecord.setUserId(userId); + apiWelfareRecord.setType("3"); + apiWelfareRecord.setCreateTime(new Date()); + apiWelfareMapper.saveClaimWelfareRecord(apiWelfareRecord); + return inventory; + } finally { + redisLock.unlock(lockKey); + } + } + + @Override + public List getMyWelfareBoxes(Long userId) { + return welfareBoxInventoryMapper.getUnopenedBoxes(userId); + } + + @Transactional + @Override + public int[] batchAutoIssueDailyBoxes(List users) { + if (users == null || users.isEmpty()) { + return new int[]{0, 0}; + } + + // 1. 一次查出今日已领取的用户ID集合 + Set claimedUserIds = new HashSet<>(welfareBoxInventoryMapper.selectTodayClaimedUserIds()); + + // 2. 一次查出所有福利配置,按VIP等级建立索引 + List allWelfares = ttWelfareMapper.selectTtWelfareList(new TtWelfare()); + Map vipToWelfare = allWelfares.stream() + .collect(Collectors.toMap(TtWelfare::getVipLevel, w -> w, (a, b) -> a)); + + // 3. 在内存中组装批量数据 + Date now = new Date(); + List inventoryList = new ArrayList<>(); + List recordList = new ArrayList<>(); + int skipCount = 0; + + for (TtUser user : users) { + Long userId = user.getUserId().longValue(); + // 跳过已领取 + if (claimedUserIds.contains(userId)) { + skipCount++; + continue; + } + // 跳过无对应福利配置的VIP等级 + TtWelfare welfare = vipToWelfare.get(user.getVipLevel()); + if (welfare == null) { + skipCount++; + continue; + } + inventoryList.add(TtWelfareBoxInventory.builder() + .userId(userId) + .welfareId(welfare.getWelfareId()) + .boxId(welfare.getBoxId()) + .status(0) + .claimDate(now) + .createTime(now) + .updateTime(now) + .build()); + ApiWelfareRecord record = new ApiWelfareRecord(); + record.setWelfareId(welfare.getWelfareId()); + record.setUserId(userId); + record.setType("3"); + record.setCreateTime(now); + recordList.add(record); + } + + // 4. 批量写入(无数据则直接返回) + if (!inventoryList.isEmpty()) { + welfareBoxInventoryMapper.batchInsertInventory(inventoryList); + apiWelfareMapper.batchSaveClaimRecord(recordList); + } + + return new int[]{inventoryList.size(), skipCount}; + } + + @Transactional + @Override + public List openWelfareBoxes(List boxInventoryIds, Long userId) { + String lockKey = "user:welfare:openboxes:lock:" + userId; + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + throw new ServiceException("开箱进行中,请勿重复操作"); + } + try { + // 0. 批次大小限制 + if (boxInventoryIds.size() > 5) { + throw new ServiceException("单次最多开启5个福利箱子"); + } + + // 1. 校验箱子归属和状态 + List boxes = welfareBoxInventoryMapper.getUnopenedBoxes(userId); + Set validIds = boxes.stream().map(TtWelfareBoxInventory::getId).collect(Collectors.toSet()); + for (Long id : boxInventoryIds) { + if (!validIds.contains(id)) { + throw new RuntimeException("包含无效或已开启的箱子"); + } + } + + // 2. 获取用户信息 + TtUser ttUser = ttUserService.getById(userId); + List results = new ArrayList<>(); + + // 3. 逐个开箱 + for (Long inventoryId : boxInventoryIds) { + TtWelfareBoxInventory inv = boxes.stream() + .filter(b -> b.getId().equals(inventoryId)).findFirst().orElse(null); + if (inv == null) continue; + + TtBox ttBox = ttBoxService.getById(inv.getBoxId()); + if (ttBox == null) continue; + + // 抽奖(福利箱子跳过临时爆率) + String ornamentId = lotteryMachine.singleLottery(ttUser, ttBox, true); + if (Objects.isNull(ornamentId)) continue; + + TtOrnamentsA ornamentsData = bindBoxMapper.ornamentsInfo(ttBox.getBoxId(), ornamentId); + if (Objects.isNull(ornamentsData)) continue; + + // 构建开箱记录,默认放入库存 + TtBoxRecords record = new TtBoxRecords(); + record.setUserId(ttUser.getUserId()); + record.setBoxId(ttBox.getBoxId()); + record.setBoxName(ttBox.getBoxName()); + record.setBoxPrice(ttBox.getPrice()); + record.setOrnamentId(Long.valueOf(ornamentId)); + record.setOrnamentName(ornamentsData.getName()); + record.setImageUrl(ornamentsData.getImageUrl()); + record.setOrnamentsPrice(ornamentsData.getUsePrice()); + record.setOrnamentsLevelId(ornamentsData.getOrnamentsLevelId()); + record.setOrnamentLevelImg(ornamentsData.getLevelImg()); + record.setHolderUserId(ttUser.getUserId()); + record.setSource(TtboxRecordSource.WELFARE_BOX.getCode()); + record.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); + record.setCreateTime(new Timestamp(System.currentTimeMillis())); + boxRecordsService.save(record); + + // 构建返回VO + OpenBoxVO vo = new OpenBoxVO(); + BeanUtil.copyProperties(record, vo); + vo.setOrnamenName(ornamentsData.getName()); + vo.setShortName(ornamentsData.getShortName()); + vo.setExteriorName(ornamentsData.getExteriorName()); + vo.setUsePrice(ornamentsData.getUsePrice()); + vo.setLevelImg(ornamentsData.getLevelImg()); + results.add(vo); + } + + // 4. 更新福利箱子库存状态为已开启 + welfareBoxInventoryMapper.batchUpdateStatus(boxInventoryIds, 1); + + return results; + } finally { + redisLock.unlock(lockKey); + } + } + + /** + * 检查是否具备VIP等级福利领取条件 + */ + private boolean checkVipUpgradeEligibility(Long userId, Integer vipLevel) { + TtUser ttUser = ttUserService.getById(userId); + return ttUser.getVipLevel() >= vipLevel; + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/utils/Constant/OPPS.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/utils/Constant/OPPS.java new file mode 100644 index 0000000..9f907e6 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/utils/Constant/OPPS.java @@ -0,0 +1,10 @@ +package com.ruoyi.playingmethod.utils.Constant; + +public enum OPPS { + // 展示爆率 + OPPS, + // 真实爆率 + REAL_OPPS, + // 主播爆率 + ANCHOR_ODDS; +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/utils/Constant/RedisLockKey.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/utils/Constant/RedisLockKey.java new file mode 100644 index 0000000..af9fc5b --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/utils/Constant/RedisLockKey.java @@ -0,0 +1,4 @@ +package com.ruoyi.playingmethod.utils.Constant; + +public enum RedisLockKey { +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/utils/customException/OrnamentNullException.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/utils/customException/OrnamentNullException.java new file mode 100644 index 0000000..9a073c1 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/utils/customException/OrnamentNullException.java @@ -0,0 +1,7 @@ +package com.ruoyi.playingmethod.utils.customException; + +public class OrnamentNullException extends RuntimeException { + public OrnamentNullException(String msg){ + super(msg); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/config/WebSocketConfig.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000..87d6d42 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/config/WebSocketConfig.java @@ -0,0 +1,15 @@ +package com.ruoyi.playingmethod.websocket.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration +public class WebSocketConfig { + @Bean + @ConditionalOnMissingBean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/constant/CMsgKey.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/constant/CMsgKey.java new file mode 100644 index 0000000..7ffffdd --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/constant/CMsgKey.java @@ -0,0 +1,19 @@ +package com.ruoyi.playingmethod.websocket.constant; + +// 客户端发送消息key +public enum CMsgKey { + + FIGHT_END, + + SUBSCRIBE_HALL, + UNSUBSCRIBE_HALL, + SUBSCRIBE_FIGHT, + UNSUBSCRIBE_FIGHT, + + // 订阅盲盒房间 + SUBSCRIBE_BINDBOX, + + // 取消订阅盲盒房间 + UNSUBSCRIBE_BINDBOX; + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/constant/SMsgKey.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/constant/SMsgKey.java new file mode 100644 index 0000000..85777a9 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/constant/SMsgKey.java @@ -0,0 +1,33 @@ +package com.ruoyi.playingmethod.websocket.constant; + +// 服务端发送消息key +public enum SMsgKey { + + // 所有对战房间列表(初始全量推送) + ALL_FIGHT_ROOM, + + // 对战房间增量更新 + FIGHT_ROOM_UPDATE, + + // 对战结果 + FIGHT_RESULT, + + // 房间信息 + FIGHT_ROOM_INFO, + + // 盲盒房间连接第一次数据 + Blind_Box_Init_Data, + + // 盲盒房间实时数据 + Blind_Box_Current_Data, + + // 对战结果数据 + FIGHT_RESULT_DATA, + + // 对战状态同步(替代 FIGHT_RESULT / FIGHT_ROOM_INFO) + FIGHT_STATE, + + // 服务端通知客户端频道关闭 + CHANNEL_CLOSED; + +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/handler/BindBoxWsHandler.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/handler/BindBoxWsHandler.java new file mode 100644 index 0000000..f053f6a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/handler/BindBoxWsHandler.java @@ -0,0 +1,105 @@ +package com.ruoyi.playingmethod.websocket.handler; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtBoxService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.common.constant.TtboxRecordSource; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.common.constant.sys.UserStatus; +import com.ruoyi.domain.dto.boxRecords.queryCondition; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO; +import com.ruoyi.framework.websocket.connector.MessageDispatcher; +import com.ruoyi.framework.websocket.connector.PushService; +import com.ruoyi.framework.websocket.connector.SubscriptionManager; +import com.ruoyi.framework.websocket.connector.WsResult; +import com.ruoyi.playingmethod.service.ApiBoxRecordsService; +import com.ruoyi.playingmethod.websocket.constant.CMsgKey; +import com.ruoyi.playingmethod.websocket.constant.SMsgKey; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class BindBoxWsHandler { + + @Autowired + private MessageDispatcher dispatcher; + @Autowired + private SubscriptionManager subManager; + @Autowired + private PushService pushService; + @Autowired + private ApiBoxRecordsService apiBoxRecordsService; + @Autowired + private TtBoxService boxService; + @Autowired + private TtUserService userService; + @Autowired + private TtUserMapper userMapper; + + @PostConstruct + public void init() { + dispatcher.register(CMsgKey.SUBSCRIBE_BINDBOX.name(), this::onSubscribeBindbox); + dispatcher.register(CMsgKey.UNSUBSCRIBE_BINDBOX.name(), this::onUnsubscribeBindbox); + } + + private void onSubscribeBindbox(Long userId, String key, Object data) { + Long boxId = ((Number) ((Map) data).get("boxId")).longValue(); + String channel = SubscriptionManager.bindboxChannel(boxId); + + subManager.subscribe(userId, channel); + + TtUser player = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, userId.intValue()) + .eq(TtUser::getStatus, UserStatus.NORMAL.getCode()) + .eq(TtUser::getDelFlag, 0) + .one(); + + if (player == null) { + pushService.sendToUser(userId, WsResult.fail(SMsgKey.Blind_Box_Init_Data, null, "用户不存在")); + return; + } + + TtBox box = boxService.getById(boxId.intValue()); + + if (box == null) { + pushService.sendToUser(userId, WsResult.fail(SMsgKey.Blind_Box_Init_Data, null, "宝箱不存在")); + return; + } + + List sources = Arrays.asList(TtboxRecordSource.BLIND_BOX.getCode()); + List status = Arrays.asList( + TtboxRecordStatus.IN_PACKSACK_ON.getCode(), + TtboxRecordStatus.DELIVERY_YET.getCode(), + TtboxRecordStatus.APPLY_DELIVERY.getCode(), + TtboxRecordStatus.RESOLVE.getCode()); + queryCondition param = queryCondition.builder() + .boxId(boxId.intValue()) + .source(sources) + .status(status) + .orderByFie(0) + .page(1) + .size(10) + .build(); + List records = apiBoxRecordsService.byCondition(param); + + pushService.sendToUser(userId, WsResult.ok(SMsgKey.Blind_Box_Init_Data, records, "盲盒初始数据")); + + log.info("用户{}订阅盲盒房间 boxId={}, 推送{}条记录", userId, boxId, records.size()); + } + + private void onUnsubscribeBindbox(Long userId, String key, Object data) { + Long boxId = ((Number) ((Map) data).get("boxId")).longValue(); + subManager.unsubscribe(userId, SubscriptionManager.bindboxChannel(boxId)); + } +} diff --git a/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/handler/FightWsHandler.java b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/handler/FightWsHandler.java new file mode 100644 index 0000000..1df9d1a --- /dev/null +++ b/skins-service/service-playingmethod/src/main/java/com/ruoyi/playingmethod/websocket/handler/FightWsHandler.java @@ -0,0 +1,352 @@ +package com.ruoyi.playingmethod.websocket.handler; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtBoxMapper; +import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper; +import com.ruoyi.admin.mapper.TtFightMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.domain.entity.fight.FightSeat; +import com.ruoyi.domain.entity.fight.TtFight; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtBox; +import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO; +import com.ruoyi.domain.vo.fight.FightBoxVO; +import com.ruoyi.domain.vo.fight.FightBrief; +import com.ruoyi.domain.vo.fight.FightState; +import com.ruoyi.framework.websocket.connector.MessageDispatcher; +import com.ruoyi.framework.websocket.connector.PushService; +import com.ruoyi.framework.websocket.connector.SubscriptionManager; +import com.ruoyi.framework.websocket.connector.WsResult; +import com.ruoyi.playingmethod.service.ApiFightService; +import com.ruoyi.playingmethod.service.FightStateManager; +import com.ruoyi.playingmethod.websocket.constant.CMsgKey; +import com.ruoyi.playingmethod.websocket.constant.SMsgKey; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class FightWsHandler { + + @Autowired + private MessageDispatcher dispatcher; + @Autowired + private SubscriptionManager subManager; + @Autowired + private PushService pushService; + @Autowired + private ApiFightService fightService; + @Autowired + private TtBoxMapper ttBoxMapper; + @Autowired + private TtBoxOrnamentsMapper boxOrnamentsMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private Executor customThreadPoolExecutor; + + @Autowired + private FightStateManager fightStateManager; + + @Autowired + private TtFightMapper fightMapper; + + @Value("${mkcsgo.fight.roundTime:6000}") + private Integer fightRoundTime; + + @Value("${mkcsgo.fight.startingDuration:3000}") + private Integer startingDuration; + + @Value("${mkcsgo.fight.revealDuration:4000}") + private Integer revealDuration; + + @PostConstruct + public void init() { + dispatcher.register(CMsgKey.SUBSCRIBE_HALL.name(), this::onSubscribeHall); + dispatcher.register(CMsgKey.UNSUBSCRIBE_HALL.name(), this::onUnsubscribeHall); + dispatcher.register(CMsgKey.SUBSCRIBE_FIGHT.name(), this::onSubscribeFight); + dispatcher.register(CMsgKey.UNSUBSCRIBE_FIGHT.name(), this::onUnsubscribeFight); + } + + private void onSubscribeHall(Long userId, String key, Object data) { + subManager.subscribe(userId, SubscriptionManager.hallChannel()); + + LambdaQueryWrapper fightQuery = new LambdaQueryWrapper<>(); + fightQuery.in(TtFight::getStatus, 0, 1) + .orderByDesc(TtFight::getCreateTime) + .last("LIMIT 200"); + List fightList = fightService.list(fightQuery); + + enrichFightBoxPrices(fightList); + enrichOwnerNickNames(fightList); + enrichSeatVipLevels(fightList); + + int total = fightList.size(); + log.info("用户{}订阅大厅,推送{}个房间", userId, total); + + CompletableFuture.runAsync(() -> { + int batchSize = 10; + if (total == 0) { + Map map = new HashMap<>(); + map.put("rows", new ArrayList<>()); + map.put("total", 0); + pushService.sendToUser(userId, WsResult.ok(SMsgKey.ALL_FIGHT_ROOM, map, "所有对战房间")); + return; + } + for (int i = 0; i < total; i += batchSize) { + List batch = fightList.subList(i, Math.min(i + batchSize, total)); + Map map = new HashMap<>(); + map.put("rows", batch); + if (i == 0) { + map.put("total", total); + pushService.sendToUser(userId, WsResult.ok(SMsgKey.ALL_FIGHT_ROOM, map, "所有对战房间")); + } else { + pushService.sendToUser(userId, WsResult.ok(SMsgKey.FIGHT_ROOM_UPDATE, map, "对战房间最新信息")); + } + if (i + batchSize < total) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + break; + } + } + } + }, customThreadPoolExecutor); + } + + private void onUnsubscribeHall(Long userId, String key, Object data) { + subManager.unsubscribe(userId, SubscriptionManager.hallChannel()); + } + + private void onSubscribeFight(Long userId, String key, Object data) { + Long fightId = ((Number) ((Map) data).get("fightId")).longValue(); + String channel = SubscriptionManager.fightChannel(fightId); + + subManager.subscribe(userId, channel); + + LambdaQueryWrapper fightQuery = new LambdaQueryWrapper<>(); + fightQuery.eq(TtFight::getId, fightId); + TtFight fight = fightService.getOne(fightQuery); + + if (fight == null) { + pushService.sendToUser(userId, WsResult.fail(SMsgKey.FIGHT_STATE, null, "不存在的对局")); + return; + } + + ArrayList fightBoxVOList = enrichFightBoxOrnaments(fight); + + FightState redisState = fightStateManager.getStateSnapshot(fight.getId()); + + // 僵尸对战检测:status=1 但 Redis 状态已丢失(服务重启/异常导致推进线程消失) + if (redisState == null && fight.getStatus() != null && fight.getStatus() == 1 && fight.getBeginTime() != null) { + long elapsedMs = System.currentTimeMillis() - fight.getBeginTime().getTime(); + long expectedDurationMs = (long) (fight.getRoundNumber() != null ? fight.getRoundNumber() : 0) * fightRoundTime + + startingDuration + revealDuration + 15000L; + if (elapsedMs > expectedDurationMs) { + log.warn("检测到僵尸对战 fightId={}, beginTime={}, 自动强制结束", fight.getId(), fight.getBeginTime()); + new LambdaUpdateChainWrapper<>(fightMapper) + .eq(TtFight::getId, fight.getId()) + .eq(TtFight::getStatus, 1) + .set(TtFight::getStatus, 3) + .set(TtFight::getEndTime, new Timestamp(System.currentTimeMillis())) + .set(TtFight::getRemark, "超时强制更新结束状态") + .update(); + pushService.sendToUser(userId, WsResult.fail(SMsgKey.FIGHT_STATE, null, "该对战已超时结束")); + return; + } + } + + if (redisState == null || FightStateManager.PHASE_WAITING.equals(redisState.getPhase())) { + FightState staticState = FightState.builder() + .phase(FightStateManager.PHASE_WAITING) + .fight(FightBrief.from(fight)) + .fightBoxVOList(fightBoxVOList) + .totalRounds(fight.getRoundNumber()) + .build(); + pushService.sendToUser(userId, WsResult.ok(SMsgKey.FIGHT_STATE, staticState, "房间静态数据")); + return; + } + + FightState.FightStateBuilder syncBuilder = FightState.builder() + .sync(true) + .fight(FightBrief.from(fight)) + .fightBoxVOList(fightBoxVOList) + .totalRounds(fight.getRoundNumber()) + .revealedRounds(redisState.getRevealedRounds() != null + ? redisState.getRevealedRounds() : Collections.emptyList()); + + String phase = redisState.getPhase(); + if (FightStateManager.PHASE_STARTING.equals(phase)) { + syncBuilder.phase(FightStateManager.PHASE_STARTING); + } else if (FightStateManager.PHASE_ROUND_OPENING.equals(phase)) { + syncBuilder.phase(FightStateManager.PHASE_ROUND_OPENING) + .currentRound(redisState.getCurrentRound()); + } else { + // REVEAL 和 SETTLED 统一返回 REVEAL phase,让观察者看完动画流程 + syncBuilder.phase(FightStateManager.PHASE_REVEAL) + .winnerIds(redisState.getWinnerIds()) + .settlement(redisState.getSettlement()); + } + + pushService.sendToUser(userId, WsResult.ok(SMsgKey.FIGHT_STATE, syncBuilder.build(), "状态同步")); + } + + private void onUnsubscribeFight(Long userId, String key, Object data) { + Long fightId = ((Number) ((Map) data).get("fightId")).longValue(); + subManager.unsubscribe(userId, SubscriptionManager.fightChannel(fightId)); + } + + private void enrichFightBoxPrices(List fightList) { + List boxIds = new ArrayList<>(); + for (TtFight fight : fightList) { + if (fight == null || fight.getBoxData() == null || fight.getBoxData().isEmpty()) continue; + for (Map.Entry entry : fight.getBoxData().entrySet()) { + Integer boxId = extractBoxId(entry.getValue()); + if (boxId != null) boxIds.add(boxId); + } + } + if (boxIds.isEmpty()) return; + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, boxIds); + List boxes = ttBoxMapper.selectList(queryWrapper); + Map boxMap = boxes.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + + for (TtFight fight : fightList) { + if (fight == null || fight.getBoxData() == null) continue; + for (Map.Entry entry : fight.getBoxData().entrySet()) { + FightBoxVO vo = resolveFightBoxVO(entry.getValue()); + if (vo != null) { + vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), BigDecimal.ZERO)); + } + } + } + } + + private ArrayList enrichFightBoxOrnaments(TtFight fight) { + ArrayList list = new ArrayList<>(); + if (fight.getBoxData() == null) return list; + + Map boxData = fight.getBoxData(); + List bIds = new ArrayList<>(); + for (FightBoxVO raw : boxData.values()) { + Integer bid = extractBoxId(raw); + if (bid != null) bIds.add(bid); + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(TtBox::getBoxId, bIds); + List boxes = ttBoxMapper.selectList(queryWrapper); + Map boxPriceMap = boxes.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice)); + Map boxNameMap = boxes.stream().filter(Objects::nonNull) + .collect(Collectors.toMap(TtBox::getBoxId, b -> b.getBoxName() != null ? b.getBoxName() : "")); + + for (Map.Entry entry : boxData.entrySet()) { + Integer bid = extractBoxId(entry.getValue()); + if (bid == null) continue; + List ornaments = boxOrnamentsMapper.selectTtBoxOrnamentsList(bid); + FightBoxVO vo = resolveFightBoxVO(entry.getValue()); + if (vo != null) { + vo.setOrnaments(ornaments); + vo.setPrice(boxPriceMap.getOrDefault(bid, BigDecimal.ZERO)); + vo.setBoxName(boxNameMap.getOrDefault(bid, "")); + list.add(vo); + } + } + return list; + } + + private void enrichOwnerNickNames(List fightList) { + if (fightList == null || fightList.isEmpty()) return; + List ownerIds = fightList.stream() + .map(TtFight::getUserId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (ownerIds.isEmpty()) return; + + List users = ttUserMapper.selectBatchIds(ownerIds); + Map nickMap = users.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(TtUser::getUserId, + u -> u.getNickName() != null ? u.getNickName() : "", (a, b) -> a)); + for (TtFight fight : fightList) { + if (fight.getUserId() != null) { + // 用户存在时用实时昵称覆盖;用户被物理删除时保留入库的 ownerNickName + String liveNick = nickMap.get(fight.getUserId()); + if (liveNick != null) { + fight.setOwnerNickName(liveNick); + } + } + } + } + + private void enrichSeatVipLevels(List fightList) { + if (fightList == null || fightList.isEmpty()) return; + List allPlayerIds = fightList.stream() + .filter(f -> f.getSeats() != null) + .flatMap(f -> f.getSeats().stream()) + .map(FightSeat::getPlayerId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (allPlayerIds.isEmpty()) return; + + List users = new LambdaQueryChainWrapper<>(ttUserMapper) + .in(TtUser::getUserId, allPlayerIds) + .select(TtUser::getUserId, TtUser::getVipLevel) + .list(); + Map vipMap = users.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(TtUser::getUserId, + u -> u.getVipLevel() != null && u.getVipLevel() > 0 ? u.getVipLevel() - 1 : 0, (a, b) -> a)); + for (TtFight fight : fightList) { + if (fight.getSeats() == null) continue; + for (FightSeat seat : fight.getSeats()) { + if (seat.getPlayerId() != null) { + seat.setVipLevel(vipMap.getOrDefault(seat.getPlayerId(), 0)); + } + } + } + } + + private Integer extractBoxId(Object value) { + if (value == null) return null; + if (value instanceof FightBoxVO) return ((FightBoxVO) value).getBoxId(); + if (value instanceof LinkedHashMap) { + Object idField = ((LinkedHashMap) value).get("boxId"); + return idField != null ? Integer.valueOf(idField.toString()) : null; + } + return null; + } + + private FightBoxVO resolveFightBoxVO(Object value) { + if (value == null) return null; + if (value instanceof FightBoxVO) return (FightBoxVO) value; + if (value instanceof LinkedHashMap) { + Map map = (LinkedHashMap) value; + return new FightBoxVO( + map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null, + map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null, + map.get("boxImg01") != null ? map.get("boxImg01").toString() : null, + map.get("boxImg02") != null ? map.get("boxImg02").toString() : null); + } + return null; + } +} diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiAttendanceRecordMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiAttendanceRecordMapper.xml new file mode 100644 index 0000000..22c4db9 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiAttendanceRecordMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + insert into tt_attendance_record + + user_id, + coin, + create_time, + update_time, + + + #{userId}, + #{coin}, + #{createTime}, + #{updateTime}, + + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiBattleRoyaleMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiBattleRoyaleMapper.xml new file mode 100644 index 0000000..4889567 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiBattleRoyaleMapper.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiBindBoxMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiBindBoxMapper.xml new file mode 100644 index 0000000..962c3a2 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiBindBoxMapper.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiCompoundMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiCompoundMapper.xml new file mode 100644 index 0000000..434edae --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiCompoundMapper.xml @@ -0,0 +1,41 @@ + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiFightMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiFightMapper.xml new file mode 100644 index 0000000..c75b81d --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiFightMapper.xml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiLuckyUpgradeMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiLuckyUpgradeMapper.xml new file mode 100644 index 0000000..e8fc36d --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiLuckyUpgradeMapper.xml @@ -0,0 +1,59 @@ + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiReplacementRecordMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiReplacementRecordMapper.xml new file mode 100644 index 0000000..bbb90a4 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiReplacementRecordMapper.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, uid, uname, oids, award_oid, award_oname, award_oprice,award_oimg, time, create_time, update_time + from tt_replacement_record + + + + insert into tt_replacement_record + + uid, + uname, + oids, + award_oid, + award_oname, + award_oprice, + award_oimg, + time, + create_time, + update_time, + + + #{uid}, + #{uname}, + #{oids}, + #{awardOid}, + #{awardOname}, + #{awardOprice}, + #{awardOimg}, + #{time}, + #{createTime}, + #{updateTime}, + + + + + select + id, + `name`, + use_price, + image_url, + market_hash_name, + item_id, + price, + quantity, + short_name, + `type`, + type_name, + quality, + quality_name, + quality_color, + rarity, + rarity_name, + rarity_color, + exterior, + exterior_name, + create_time, + update_time, + remark, + is_putaway, + is_proprietary_property, + zbt_id, + yyyouping_id, + type_hash_name, + exterior_hash_name, + quality_hash_name, + rarity_hash_name + from tt_ornament + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiRobotFightGroupMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiRobotFightGroupMapper.xml new file mode 100644 index 0000000..e87e876 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiRobotFightGroupMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiRollMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiRollMapper.xml new file mode 100644 index 0000000..fe113a7 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiRollMapper.xml @@ -0,0 +1,120 @@ + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiUpgradeRecordMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiUpgradeRecordMapper.xml new file mode 100644 index 0000000..94d28a0 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiUpgradeRecordMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiWelfareBoxInventoryMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiWelfareBoxInventoryMapper.xml new file mode 100644 index 0000000..3a84609 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiWelfareBoxInventoryMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + INSERT INTO tt_welfare_box_inventory (user_id, welfare_id, box_id, status, claim_date, create_time, update_time) + VALUES + + (#{item.userId}, #{item.welfareId}, #{item.boxId}, #{item.status}, + #{item.claimDate}, #{item.createTime}, #{item.updateTime}) + + + + + + + UPDATE tt_welfare_box_inventory + SET status = #{status}, update_time = NOW() + WHERE id IN + + #{id} + + + + diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiWelfareMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiWelfareMapper.xml new file mode 100644 index 0000000..3caa8f0 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiWelfareMapper.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO tt_welfare_record(welfare_id, user_id, type, create_time) + VALUES (#{welfareId}, #{userId}, #{type}, #{createTime}) + + + + INSERT INTO tt_welfare_record (welfare_id, user_id, type, create_time) + VALUES + + (#{item.welfareId}, #{item.userId}, #{item.type}, #{item.createTime}) + + + \ No newline at end of file diff --git a/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiWelfareMonthlyRechargesMapper.xml b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiWelfareMonthlyRechargesMapper.xml new file mode 100644 index 0000000..ed98ee6 --- /dev/null +++ b/skins-service/service-playingmethod/src/main/resources/com/ruoyi/playingmethod/mapper/ApiWelfareMonthlyRechargesMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO tt_welfare_record(welfare_id, user_id, type, create_time) + VALUES (#{welfareId}, #{userId}, #{type}, #{createTime}) + + \ No newline at end of file diff --git a/skins-service/service-task/pom.xml b/skins-service/service-task/pom.xml new file mode 100644 index 0000000..60a8d3e --- /dev/null +++ b/skins-service/service-task/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + com.ruoyi + skins-service + 4.8.2 + + + service-task + + + + + + + + + + + com.ruoyi + ruoyi-common + + + + com.ruoyi + service-admin + 4.8.2 + + + + com.ruoyi + message-service + 4.8.2 + + + + + \ No newline at end of file diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/MQReceiver/DirectReceiver.java b/skins-service/service-task/src/main/java/com/ruoyi/task/MQReceiver/DirectReceiver.java new file mode 100644 index 0000000..81c1ac5 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/MQReceiver/DirectReceiver.java @@ -0,0 +1,29 @@ +package com.ruoyi.task.MQReceiver; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rabbitmq.client.Channel; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.task.DTO.pWelfareMQData; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.math.BigDecimal; + +import static com.ruoyi.task.config.DirectExchangeConfig.*; + + +// 不用 +@Slf4j +// @Component +public class DirectReceiver { + +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/config/DirectExchangeConfig.java b/skins-service/service-task/src/main/java/com/ruoyi/task/config/DirectExchangeConfig.java new file mode 100644 index 0000000..2f096e0 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/config/DirectExchangeConfig.java @@ -0,0 +1,48 @@ +package com.ruoyi.task.config; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; + + +@Configurable +public class DirectExchangeConfig { + + @Autowired + private AmqpAdmin amqpAdmin; + public static final String PROMOTION_WELFARE_QUEUE = "pWelfareQueue"; + public static final String PROMOTION_WELFARE_EXCHANGE = "pWelfareExchange"; + public static final String PROMOTION_WELFARE_KEY1 = "pwKey1"; + + // public static void main(String[] args) { + // System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); + // } + + + // @Bean + // public Queue directQueue() { + // return new Queue(DIRECT_QUEUE, true); + // } + // + // @Bean + // public DirectExchange directExchange() { + // return new DirectExchange(DIRECT_EXCHANGE, true, false); + // } + // + // @Bean + // public Binding bindingDirectExchange(Queue directQueue, DirectExchange directExchange) { + // return BindingBuilder.bind(directQueue).to(directExchange).with(DIRECT_ROUTING_KEY); + // } + + // @Bean + // public Queue directQueue2() { + // return new Queue(DIRECT_QUEUE2, true); + // } + + // @Bean + // public Binding bindingDirectExchange2(Queue directQueue2, DirectExchange directExchange) { + // return BindingBuilder.bind(directQueue2).to(directExchange).with(DIRECT_ROUTING_KEY); + // } + + +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/contract/request/GetAwardRequest.java b/skins-service/service-task/src/main/java/com/ruoyi/task/contract/request/GetAwardRequest.java new file mode 100644 index 0000000..1b2629e --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/contract/request/GetAwardRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.task.contract.request; + +import lombok.Data; + +@Data +public class GetAwardRequest { + private Integer taskDoingid; +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/contract/request/TaskTestRequest.java b/skins-service/service-task/src/main/java/com/ruoyi/task/contract/request/TaskTestRequest.java new file mode 100644 index 0000000..b5e8d9b --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/contract/request/TaskTestRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.task.contract.request; + +import lombok.Data; + +@Data +public class TaskTestRequest { + private String kind; +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/controller/MQController.java b/skins-service/service-task/src/main/java/com/ruoyi/task/controller/MQController.java new file mode 100644 index 0000000..0b77006 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/controller/MQController.java @@ -0,0 +1,38 @@ +package com.ruoyi.task.controller; + +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.task.DTO.pWelfareMQData; +import com.ruoyi.domain.task.VO.TtTaskDoingVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.List; + +import static com.ruoyi.domain.task.constant.mq.MQMoudle.PROMOTION_WELFARE_EXCHANGE; +import static com.ruoyi.domain.task.constant.mq.MQMoudle.PROMOTION_WELFARE_KEY1; + +@Slf4j +@RestController +@RequestMapping("/api/mq") +public class MQController { + + @Autowired + private RabbitTemplate rabbitTemplate; + + @GetMapping("/t1") + @Anonymous + public AjaxResult t1() { + + log.debug("mq测试"); + return AjaxResult.success("mq测试"); + + } + +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/controller/TtTaskController.java b/skins-service/service-task/src/main/java/com/ruoyi/task/controller/TtTaskController.java new file mode 100644 index 0000000..c4d74f9 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/controller/TtTaskController.java @@ -0,0 +1,73 @@ +package com.ruoyi.task.controller; + +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.task.VO.TtTaskDoingVO; +import com.ruoyi.task.contract.request.GetAwardRequest; +import com.ruoyi.task.contract.request.TaskTestRequest; +import com.ruoyi.task.scheduled.TaskUpdate; +import com.ruoyi.task.service.TtTaskService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@Api(tags = "任务模块") +@RestController +@RequestMapping("/api/ttTask") +public class TtTaskController extends BaseController { + + @Autowired + private TtTaskService ttTaskService; + + @Autowired + private TaskUpdate taskUpdate; + + @ApiOperation("关于我的任务") + @PostMapping("/taskOfme") + @Anonymous + // public List taskOfme(@PathVariable("page") Integer page,@PathVariable("size") Integer size) { + public List taskOfme() { + Long userId = getUserId(); + return ttTaskService.taskOfme(1, 5, Integer.valueOf(String.valueOf(userId))); //暂时不需要分页 + // return ttTaskService.taskOfme(1,5,1); + } + + // 领取奖励 + @PostMapping("/getAward") + @Anonymous + public AjaxResult getAward(@RequestBody GetAwardRequest request) { + Long userId = getUserId(); + return ttTaskService.getAward(Integer.valueOf(String.valueOf(userId)), request.getTaskDoingid()); + // return ttTaskService.getAward(1,1); + } + + // 接首次下载任务 + @PostMapping("/firsDownLoadTask") + @Anonymous + public AjaxResult firsDownLoadTask() { + Long userId = getUserId(); + return ttTaskService.firsDownLoadTask(Integer.valueOf(String.valueOf(userId))); + } + + // 接首次下载任务 + @PostMapping("/test") + @Anonymous + public AjaxResult firsDownLoadTask(@RequestBody TaskTestRequest request) { + String kind = request.getKind(); + + switch (kind) { + // case "rank": + // // 已废弃:综合流水排行榜奖励改由 RechargeRankingRewardTask 统一发放 + // taskUpdate.blendErcashRankPrize(); + case "zhubo": + taskUpdate.pWelfarePrize(); + } + return AjaxResult.success("测试完成"); + } + +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtTaskDoingMapper.java b/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtTaskDoingMapper.java new file mode 100644 index 0000000..d4c6578 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtTaskDoingMapper.java @@ -0,0 +1,13 @@ +package com.ruoyi.task.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.task.TtTask; +import com.ruoyi.domain.task.TtTaskDoing; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface TtTaskDoingMapper extends BaseMapper { + + TtTaskDoing isOwnUser(@Param("userId") Integer userId,@Param("tid") Integer tid); +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtTaskMapper.java b/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtTaskMapper.java new file mode 100644 index 0000000..6115196 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtTaskMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.task.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.task.TtTask; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtTaskMapper extends BaseMapper { + + TtTask byId(Integer id); + + List listByState(@Param("state") Integer state); +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtUserBlendErcashArchiveMapper.java b/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtUserBlendErcashArchiveMapper.java new file mode 100644 index 0000000..67a2c84 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtUserBlendErcashArchiveMapper.java @@ -0,0 +1,28 @@ +package com.ruoyi.task.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtUserBlendErcashArchive; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TtUserBlendErcashArchiveMapper extends BaseMapper { + + /** + * 获取待归档的 id 列表(按 id 排序,每批 batchSize 条) + */ + List selectArchiveIds(@Param("cutoffTime") String cutoffTime, + @Param("batchSize") int batchSize); + + /** + * 将指定 id 列表的记录插入归档表 + */ + int insertFromMain(@Param("ids") List ids); + + /** + * 从主表删除已归档的记录 + */ + int deleteFromMain(@Param("ids") List ids); +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtUserSourceReportTaskMapper.java b/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtUserSourceReportTaskMapper.java new file mode 100644 index 0000000..027376d --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/mapper/TtUserSourceReportTaskMapper.java @@ -0,0 +1,42 @@ +package com.ruoyi.task.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.TtUserSourceReport; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface TtUserSourceReportTaskMapper extends BaseMapper { + + /** + * 增量模式:统计昨日各 user+source 的 delta(仅扫昨日 1 天流水) + */ + List> selectYesterdayDelta( + @Param("yesterdayStart") String yesterdayStart, + @Param("todayStart") String todayStart); + + /** + * 全量模式:统计 report_date 之前所有流水的累计值(仅首次运行时使用) + */ + List> selectFullCumulative( + @Param("todayStart") String todayStart); + + /** + * 批量 UPSERT 报表记录(INSERT ... ON DUPLICATE KEY UPDATE) + */ + int batchUpsert(@Param("list") List list, + @Param("reportDate") String reportDate); + + /** + * 检查指定日期是否已有报表数据(幂等检查) + */ + int countByReportDate(@Param("reportDate") String reportDate); + + /** + * 查询前一天的报表记录(用于增量递推) + */ + List selectByReportDate(@Param("reportDate") String reportDate); +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/FlowArchiveTask.java b/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/FlowArchiveTask.java new file mode 100644 index 0000000..35bc266 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/FlowArchiveTask.java @@ -0,0 +1,101 @@ +package com.ruoyi.task.scheduled; + +import com.ruoyi.system.config.ArchiveDaysConfig; +import com.ruoyi.task.mapper.TtUserBlendErcashArchiveMapper; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * 流水归档定时任务 —— 每天凌晨 5 点执行。 + *

+ * 注册到 RuoYi 系统监控-定时任务: + * - 任务名称:流水数据归档 + * - 任务组名:ARCHIVE + * - 调用目标:flowArchiveTask.archiveOldData + * - cron:0 0 5 * * ? + *

+ * 将 tt_user_blend_ercash 中 7 天前的数据迁移到 tt_user_blend_ercash_archive。 + * 分批处理(每批 1000 条),先插后删,每批独立事务,确保数据安全。 + */ +@Slf4j +@Component("flowArchiveTask") +public class FlowArchiveTask { + + private static final int BATCH_SIZE = 1000; + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Autowired + private TtUserBlendErcashArchiveMapper archiveMapper; + + @Autowired + private PlatformTransactionManager transactionManager; + + /** 每批归档的事务模板:把"插入归档表 + 删除主表"包成单事务,保证原子提交/回滚 */ + private TransactionTemplate transactionTemplate; + + @PostConstruct + private void initTransactionTemplate() { + this.transactionTemplate = new TransactionTemplate(transactionManager); + } + + /** + * 归档 7 天前的流水数据 + */ + public void archiveOldData() { + log.info("FlowArchiveTask start ----------"); + try { + LocalDate cutoff = LocalDate.now().minusDays(ArchiveDaysConfig.getRetainDays()); + String cutoffTime = cutoff.format(DATE_FMT) + " 00:00:00"; + log.info("归档截止时间: {}", cutoffTime); + + int totalArchived = 0; + int batchNum = 0; + + while (true) { + batchNum++; + // Step 1: 获取待归档的 id 列表 + List ids = archiveMapper.selectArchiveIds(cutoffTime, BATCH_SIZE); + if (ids == null || ids.isEmpty()) { + log.info("无更多待归档数据,退出循环"); + break; + } + + // Step 2+3: 插入归档表 + 从主表删除,放入同一事务原子执行。 + // 要么一起提交、要么整批回滚,杜绝"已插未删"造成的两表并存与查询双算; + // 归档表 ON DUPLICATE KEY UPDATE 对重复主键 no-op,残留行不报错并照常从主表删除,实现自愈。 + final List batchIds = ids; + int[] counts = transactionTemplate.execute(status -> { + int ins = archiveMapper.insertFromMain(batchIds); + int del = archiveMapper.deleteFromMain(batchIds); + return new int[]{ins, del}; + }); + int inserted = counts == null ? 0 : counts[0]; + int deleted = counts == null ? 0 : counts[1]; + + totalArchived += deleted; + // 迁移条数以本批 id 数为准更直观;括号内 inserted 为归档表实际新增行数, + // 自愈重复行时因 ON DUPLICATE KEY UPDATE id=id 的 no-op 计为 0,属正常非失败。 + log.info("第 {} 批:迁移 {} 条(归档表新增 {} 条),累计归档 {} 条", + batchNum, batchIds.size(), inserted, totalArchived); + + // 如果本批不足 BATCH_SIZE,说明已处理完 + if (ids.size() < BATCH_SIZE) { + break; + } + } + + log.info("FlowArchiveTask done: totalArchived={} ----------", totalArchived); + + } catch (Exception e) { + log.error("FlowArchiveTask error: {}", e.getMessage(), e); + } + } +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/TaskUpdate.java b/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/TaskUpdate.java new file mode 100644 index 0000000..be267a2 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/TaskUpdate.java @@ -0,0 +1,395 @@ +package com.ruoyi.task.scheduled; + + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.mapper.TtUserAmountRecordsMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserCreditsRecordsMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtRechargeRankingRewardService; +import com.ruoyi.admin.service.TtUserAmountRecordsService; +import com.ruoyi.admin.service.TtUserCreditsRecordsService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.service.TtVipLevelService; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.common.constant.sys.MoneyType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRankingReward; +import com.ruoyi.domain.other.TtVipLevel; +import com.ruoyi.domain.vo.TeamDetailVO; +import com.ruoyi.domain.vo.UserBERankVO; +import com.ruoyi.task.service.TtTaskDoingService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtAccountRecordSource.P_WELFARE; + +@Slf4j +@Configuration // 1.主要用于标记配置类,兼备Component的效果。 +@EnableScheduling // 2.开启定时任务 +public class TaskUpdate { + + @Autowired + private TtUserService userService; + + @Autowired + private TtTaskDoingService ttTaskDoingService; + + @Autowired + private TtUserService ttUserService; + + @Autowired + private TtUserCreditsRecordsService ttUserCreditsRecordsService; + + @Autowired + private TtUserCreditsRecordsMapper ttUserCreditsRecordsMapper; + + @Autowired + private TtUserAmountRecordsService ttUserAmountRecordsService; + + @Autowired + private TtUserAmountRecordsMapper ttUserAmountRecordsMapper; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtPromotionUpdateMapper ttPromotionUpdateMapper; + + @Autowired + private TtVipLevelService vipLevelService; + + @Autowired + private TtRechargeRankingRewardService ttRechargeRankingRewardService; + // 刷新每日任务 + //@Scheduled(cron = "0 0 0 * * ?") + // private void refreshDayTask() { + // + // log.info("刷新{}任务", "每日流水奖励"); + // + // Timestamp now = new Timestamp(System.currentTimeMillis()); + // + // // {每日流水奖励}任务数据重置 + // LambdaUpdateWrapper ttTaskDoingUpdate = new LambdaUpdateWrapper<>(); + // ttTaskDoingUpdate + // .eq(TtTaskDoing::getTaskId, 2) + // .set(TtTaskDoing::getBeginTime, now) + // .set(TtTaskDoing::getCompeteTime, null) + // .set(TtTaskDoing::getCompletionState, TaskCompletionState.DOING.getCode()) + // .set(TtTaskDoing::getProgress, 0); + // ttTaskDoingService.update(ttTaskDoingUpdate); + // + // } + + // 发放综合流水排行榜前十奖励 + // 已废弃:该功能由 RechargeRankingRewardTask.distributeRechargeRankingReward 替代, + // 并通过 ruoyi 定时任务后台统一管理,避免与 source=29 的消费排行榜奖励重复发放。 + // @Scheduled(cron = "2 7 0 * * ?", zone = "Asia/Shanghai") + public void blendErcashRankPrize() { + + log.info("发放综合流水排行榜前十奖励。"); + + // 正式时间区间------------------------------------- + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + Timestamp end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.DAY_OF_MONTH, -1); + Timestamp begin = new Timestamp(c.getTimeInMillis()); + + // 测试时间区间--------------------------------- + // Calendar c = Calendar.getInstance(); + // Timestamp end = new Timestamp(c.getTimeInMillis()); + // c.set(Calendar.HOUR_OF_DAY, 0); + // c.set(Calendar.MINUTE, 0); + // c.set(Calendar.SECOND, 0); + // c.set(Calendar.MILLISECOND, 0); + // c.add(Calendar.MINUTE, -2); + // Timestamp begin = new Timestamp(c.getTimeInMillis()); + //------------------------------------------------- + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String beginT = dateFormat.format(begin); + String endT = dateFormat.format(end); + if (StringUtils.isBlank(beginT) || StringUtils.isBlank(beginT)) { + beginT = null; + endT = null; + } + + var source = TtAccountRecordSource.getGameConsumeCodes(); + + List rank = ttUserBlendErcashMapper.rank( + new ArrayList<>(source), + beginT, + endT, + 0, + 10); + + var rankingRewards = ttRechargeRankingRewardService.selectTtRechargeRankingRewardList(null); + var rankRewardMap = rankingRewards.stream() + .collect(Collectors.toMap(TtRechargeRankingReward::getId, t -> t)); + + for (int i = 1; i <= rank.size(); i++) { + var vo = rank.get(i - 1); + vo.setBeRank(i); + try { + var reward = rankRewardMap.get(vo.getBeRank()); + if (reward == null) { + log.warn("未找到排名奖励配置,排名:{}", vo.getBeRank()); + continue; + } + userService.updateOnlyUserAccount(vo.getUserId(), reward.getReward(), TtAccountRecordSource.RANK_BLEND_ERCASH); + } catch (Exception e) { + log.error("发放综合流水排行榜奖励失败,用户:{},异常:{}", vo.getUserId(), e.getMessage()); + } + } + + log.info("发放综合流水排行榜前十奖励 完成"); + } + + // 发放主播推广福利奖励 + // 已禁用:不再每天凌晨批量发放推广福利(充值返佣已改为实时到账) + // @Scheduled(cron = "2 4 0 * * ?", zone = "Asia/Shanghai") // 每天0点4分2秒执行 + public void pWelfarePrize() { + + Long start = System.currentTimeMillis(); + log.info("发放推广福利奖励。"); + + // 正式时间段 ---------------------------------- + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + Timestamp end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.DAY_OF_MONTH, -1); + Timestamp begin = new Timestamp(c.getTimeInMillis()); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String beginT = dateFormat.format(begin); + String endT = dateFormat.format(end); + + if (StringUtils.isBlank(beginT) || StringUtils.isBlank(endT)) { + log.warn("推广福利统计失败!"); + return; + } + + // 查询今天下级有消费的主播 + List bossIdList = ttUserAmountRecordsMapper.bossByHasConsumeEmployee(beginT, endT); + + // 循环所有boss发放福利 + for (Integer bossId : bossIdList) { + try { + boolean r = pWelfarePrizeToBoss(bossId, beginT, endT); + if (!r) { + log.error("推广福利统计失败,用户【{}】发奖失败!", bossId); + } + } catch (Exception e) { + log.error("推广福利统计失败,用户【{}】发奖异常!{}", bossId, e.getMessage()); + } + } + + Long stop = System.currentTimeMillis(); + log.info("每日推广福利发放完成。用时:{}分钟", (stop - start) / 60000); + + } + + // VIP 流水返利已改为实时发放(见 TtUserServiceImpl.tryVipFlowRebate),本定时任务已禁用 + // @Scheduled(cron = "2 1 0 * * ?", zone = "Asia/Shanghai") // 每天0点1分2秒执行 + private void vipReward() { + Long start = System.currentTimeMillis(); + log.info("发放充值vip流水福利奖励"); + + LocalDate yesterday = LocalDate.now().minusDays(1); + LocalDateTime beginTime = yesterday.atStartOfDay(); + LocalDateTime endTime = yesterday.atTime(23, 59, 59); + + var records = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper).between(TtUserBlendErcash::getCreateTime, beginTime, endTime) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameConsumeCodes()).list(); + + if (CollectionUtils.isEmpty(records)) { + log.info("昨日无充值VIP流水记录,无需发放奖励"); + return; + } + + Map> userRecordsMap = records.stream() + .collect(Collectors.groupingBy(TtUserBlendErcash::getUserId)); + + // 3. 遍历用户进行发放操作 + var ttVipLevelMap = vipLevelService.list().stream().collect(Collectors.toMap(TtVipLevel::getId, t -> t)); + for (Map.Entry> entry : userRecordsMap.entrySet()) { + Integer userId = entry.getKey(); + try { + List userRecords = entry.getValue(); + + // 计算该用户昨日VIP流水总额 + BigDecimal totalAmount = userRecords.stream() + .map(TtUserBlendErcash::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + totalAmount = totalAmount.abs(); + + TtUser ttUser = userService.getById(userId); + + if (ttUser.getVipLevel() > 0) { + TtVipLevel ttVipLevel = ttVipLevelMap.get(ttUser.getVipLevel()); + if (ttVipLevel == null) { + log.warn("用户【{}】VIP等级不存在![{}]", ttUser.getUserId(), ttUser.getVipLevel()); + continue; + } + // 优先使用 flowRebatePercent;若为 null 则回退到 commissions + BigDecimal percent = ttVipLevel.getFlowRebatePercent() != null + ? ttVipLevel.getFlowRebatePercent() + : (ttVipLevel.getCommissions() != null ? ttVipLevel.getCommissions() : BigDecimal.ZERO); + BigDecimal rebate = percent.divide(BigDecimal.valueOf(100), 4, + RoundingMode.HALF_UP) + .multiply(totalAmount).setScale(2, RoundingMode.HALF_UP); + + userService.updateUserAccount(userId, rebate, TtAccountRecordSource.VIP_CONSUME_AWARD); + } + + log.debug("用户ID: {}, 昨日VIP流水总额: {}, 奖励发放处理中...", userId, totalAmount); + } catch (Exception e) { + log.error("发放VIP流水福利失败,用户ID: {},错误: {}", userId, e.getMessage()); + } + } + + + Long stop = System.currentTimeMillis(); + log.info("发放充值vip流水福利奖励完成。用时:{}分钟", (stop - start) / 60000); + } + + // 根据时间区间内下级的消费发放推广福利 + private boolean pWelfarePrizeToBoss(Integer bossId, String beginTimeStr, String endTimeStr) { + var user = userMapper.selectTtUserById(Long.valueOf(bossId)); + if (user == null) { + log.warn("推广福利统计失败,用户【{}】不存在!", bossId); + return false; + } + if (!UserType.ANCHOR.getCode().equals(user.getUserType())) { + log.warn("推广福利统计失败,用户【{}】不是主播!", bossId); + return false; + } + + // 解析时间 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date beginTime = null; + try { + beginTime = dateFormat.parse(beginTimeStr); + } catch (ParseException e) { + log.warn("日期解析异常,【{}】发奖失败。", bossId); + return false; + } + + // 查询所有下级id + List allEmployeesId = userMapper.allEmployeesByParents(Arrays.asList(bossId)); + + // 查询下级的最近一个绑定时间 + List mit = ttPromotionUpdateMapper.latelyUpdate(allEmployeesId); + if (mit.size() < allEmployeesId.size()) log.warn("下级已绑定上级,但未写入更新日志,请及时检查!!!"); + // 如果最近绑定时间大于本次查询的起始时间,以最近绑定时间为准 + List empIds1 = new ArrayList<>(); + List empIds2 = new ArrayList<>(); + for (TeamDetailVO item : mit) { + Timestamp latelyTime = item.getBeginTime(); + if (latelyTime.compareTo(new Timestamp(beginTime.getTime())) < 0) { + empIds1.add(item.getEmployeeId()); + } else { + empIds2.add(item); + } + } + + // 昨日没有换过绑定的玩家数据 + List personBEList1 = new ArrayList<>(); + if (!empIds1.isEmpty()) { + personBEList1 = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper) + .in(TtUserBlendErcash::getUserId, empIds1) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getWelfarePrizeCodes()) + .between(TtUserBlendErcash::getCreateTime, beginTimeStr, endTimeStr) + .list(); + } + + // 当日有 更换绑定的下级消费统计 + List personBEList2 = new ArrayList<>(); + if (!empIds2.isEmpty()) { + for (TeamDetailVO vo : empIds2) { + String beginT = dateFormat.format(vo.getBeginTime()); + var l = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper) + .eq(TtUserBlendErcash::getUserId, vo.getEmployeeId()) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getWelfarePrizeCodes()) + .between(TtUserBlendErcash::getCreateTime, beginT, endTimeStr) + .list(); + personBEList2.addAll(l); + } + } + + personBEList1.addAll(personBEList2); + + var totalRecharge = personBEList1.stream().filter(v -> { + return Objects.equals(v.getSource(), TtAccountRecordSource.RECHARGE.getCode()); + }).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add); + + var allConsumeList = personBEList1.stream().filter(v -> { + return Objects.equals(v.getType(), TtAccountRecordType.OUTPUT.getCode()); + }).toList(); + + var totalNormalGameConsume = allConsumeList.stream().filter(v -> { + return TtAccountRecordSource.isNormalGameConsume(v.getSource()); + }).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add); + + var totalSpecialGameConsume = allConsumeList.stream().filter(v -> { + return TtAccountRecordSource.isSpecialGameConsume(v.getSource()); + }).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add); + + // 发奖 + // 0.02*总充值 + 0.03*普通游戏消费 + 0.01*特殊游戏消费 + BigDecimal prize = totalRecharge.multiply(new BigDecimal("0.02")).add(totalNormalGameConsume.multiply(new BigDecimal("0.03"))) + .add(totalSpecialGameConsume.multiply(new BigDecimal("0.01"))); + + // 更新账户 + userService.updateUserAccount(bossId, prize, P_WELFARE); + return true; + } + + // 奖励 + private void rankPrizeF(MoneyType prizeMoneyType, + LambdaUpdateWrapper wrapper, + Integer userId, + String value, + TtAccountRecordSource source) { + + userService.updateUserAccount(userId, new BigDecimal(value), source); + + } + +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/UserSourceReportTask.java b/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/UserSourceReportTask.java new file mode 100644 index 0000000..1a918d4 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/UserSourceReportTask.java @@ -0,0 +1,162 @@ +package com.ruoyi.task.scheduled; + +import com.ruoyi.domain.entity.TtUserSourceReport; +import com.ruoyi.task.mapper.TtUserSourceReportTaskMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 用户来源日报表定时任务 —— 每天凌晨 3 点执行。 + *

+ * 注册到 RuoYi 系统监控-定时任务: + * - 任务名称:用户来源日报表生成 + * - 任务组名:REPORT + * - 调用目标:userSourceReportTask.generateDailyReport + * - cron:0 0 3 * * ? + *

+ * 核心优化:增量递推 + * - 日常运行:今日 total = 昨日报表 total + 今日 delta(只扫 1 天流水) + * - 首次运行:全量扫描 report_date 之前的全部流水 + */ +@Slf4j +@Component("userSourceReportTask") +public class UserSourceReportTask { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Autowired + private TtUserSourceReportTaskMapper reportMapper; + + /** + * 生成日报(增量/全量自动切换) + */ + public void generateDailyReport() { + log.info("UserSourceReportTask start ----------"); + try { + LocalDate today = LocalDate.now(); + LocalDate reportDate = today.minusDays(1); // 报表日期 = 昨天 + LocalDate prevDate = reportDate.minusDays(1); // 前天 + + String reportDateStr = reportDate.format(DATE_FMT); + String prevDateStr = prevDate.format(DATE_FMT); + String yesterdayStart = reportDateStr + " 00:00:00"; + String todayStart = today.format(DATE_FMT) + " 00:00:00"; + + // 幂等检查:如果 reportDate 已有数据则跳过 + int existCount = reportMapper.countByReportDate(reportDateStr); + if (existCount > 0) { + log.info("reportDate={} 已有 {} 条记录,跳过(幂等)", reportDateStr, existCount); + return; + } + + // 检查是否有前日报表(决定增量还是全量) + List prevReports = reportMapper.selectByReportDate(prevDateStr); + boolean hasPrev = prevReports != null && !prevReports.isEmpty(); + + // 构建昨日报表 key -> 前日 total 的映射(增量递推用) + Map prevTotalMap = new HashMap<>(); + if (hasPrev) { + for (TtUserSourceReport prev : prevReports) { + String key = prev.getUserId() + "_" + prev.getSource(); + prevTotalMap.put(key, prev.getTotal() != null ? prev.getTotal() : BigDecimal.ZERO); + } + } + + List upsertList; + + if (hasPrev) { + // ===== 增量模式:昨日 delta + 前日 total ===== + log.info("增量模式:reportDate={}, 前日记录数={}", reportDateStr, prevReports.size()); + List> deltas = reportMapper.selectYesterdayDelta(yesterdayStart, todayStart); + log.info("昨日 delta 记录数={}", deltas.size()); + + upsertList = new ArrayList<>(); + for (Map delta : deltas) { + Integer userId = ((Number) delta.get("userId")).intValue(); + Integer source = ((Number) delta.get("source")).intValue(); + BigDecimal deltaTotal = delta.get("delta") != null + ? new BigDecimal(delta.get("delta").toString()) : BigDecimal.ZERO; + Integer parentId = delta.get("parentId") != null + ? ((Number) delta.get("parentId")).intValue() : null; + + String key = userId + "_" + source; + BigDecimal prevTotal = prevTotalMap.getOrDefault(key, BigDecimal.ZERO); + BigDecimal newTotal = prevTotal.add(deltaTotal); + + upsertList.add(TtUserSourceReport.builder() + .userId(userId) + .parentId(parentId) + .source(source) + .total(newTotal) + .build()); + } + + // 处理前日有记录但昨日无 delta 的 user+source(累计值不变,仍需要写入今日报表行) + Set deltaKeys = new HashSet<>(); + for (Map delta : deltas) { + Integer uid = ((Number) delta.get("userId")).intValue(); + Integer src = ((Number) delta.get("source")).intValue(); + deltaKeys.add(uid + "_" + src); + } + for (TtUserSourceReport prev : prevReports) { + String key = prev.getUserId() + "_" + prev.getSource(); + if (!deltaKeys.contains(key)) { + // 昨日无 delta,累计值 = 前日 total(原样递推) + upsertList.add(TtUserSourceReport.builder() + .userId(prev.getUserId()) + .parentId(prev.getParentId()) + .source(prev.getSource()) + .total(prev.getTotal() != null ? prev.getTotal() : BigDecimal.ZERO) + .build()); + } + } + } else { + // ===== 全量模式:首次运行,扫描全部历史 ===== + log.info("全量模式:reportDate={}, 无前日数据,扫描全部历史", reportDateStr); + List> fullData = reportMapper.selectFullCumulative(todayStart); + log.info("全量累计记录数={}", fullData.size()); + + upsertList = new ArrayList<>(); + for (Map row : fullData) { + Integer userId = ((Number) row.get("userId")).intValue(); + Integer source = ((Number) row.get("source")).intValue(); + BigDecimal total = row.get("total") != null + ? new BigDecimal(row.get("total").toString()) : BigDecimal.ZERO; + Integer parentId = row.get("parentId") != null + ? ((Number) row.get("parentId")).intValue() : null; + + upsertList.add(TtUserSourceReport.builder() + .userId(userId) + .parentId(parentId) + .source(source) + .total(total) + .build()); + } + } + + // 分批 UPSERT(每批 2000 条,避免单次 SQL 过大) + int batchSize = 2000; + int totalUpserted = 0; + for (int i = 0; i < upsertList.size(); i += batchSize) { + int end = Math.min(i + batchSize, upsertList.size()); + List batch = upsertList.subList(i, end); + reportMapper.batchUpsert(batch, reportDateStr); + totalUpserted += batch.size(); + log.info("UPSERT 批次 {}/{}, 本批 {} 条", (i / batchSize + 1), + (upsertList.size() + batchSize - 1) / batchSize, batch.size()); + } + + log.info("UserSourceReportTask done: reportDate={}, totalUpserted={} ----------", + reportDateStr, totalUpserted); + + } catch (Exception e) { + log.error("UserSourceReportTask error: {}", e.getMessage(), e); + } + } +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/VipFlowRebateTask.java b/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/VipFlowRebateTask.java new file mode 100644 index 0000000..f5e8c32 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/scheduled/VipFlowRebateTask.java @@ -0,0 +1,182 @@ +package com.ruoyi.task.scheduled; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.service.TtVipLevelService; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtVipLevel; +import com.ruoyi.message.service.MessageService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * VIP 流水返利每日定时任务 —— 每天凌晨执行,发放"昨日"游戏流水的 VIP 返利。 + *

+ * 由实时发放改造为定时发放:用户玩游戏时不再实时到账,改为每天统一按昨日游戏 + * 流水总额 × 当前 VIP 等级 flowRebatePercent 计算并一次性发放。 + *

+ * 注册到 RuoYi 系统监控-定时任务: + * - 任务名称:VIP流水返利发放 + * - 任务组名:VIP_REBATE + * - 调用目标:vipFlowRebateTask.grantDailyVipRebate() + * - cron:0 10 0 * * ? (每天凌晨 0 点 10 分) + */ +@Slf4j +@Component("vipFlowRebateTask") +public class VipFlowRebateTask { + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtVipLevelService vipLevelService; + + @Autowired + private MessageService messageService; + + /** + * 发放昨日游戏流水的 VIP 返利。 + */ + public void grantDailyVipRebate() { + long start = System.currentTimeMillis(); + log.info("[VIP流水返利] 每日定时发放任务开始 ----------"); + + LocalDate today = LocalDate.now(); + LocalDate yesterday = today.minusDays(1); + LocalDateTime beginTime = yesterday.atStartOfDay(); + LocalDateTime endTime = today.atStartOfDay().minusNanos(1); + + // 幂等:今日若已发放过(存在今日创建的 VIP流水奖励 记录),直接跳过,避免重复发放 + long grantedToday = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.VIP_CONSUME_AWARD.getCode()) + .ge(TtUserBlendErcash::getCreateTime, today.atStartOfDay()) + .count(); + if (grantedToday > 0) { + log.info("[VIP流水返利] 今日已发放过 {} 条返利记录,跳过(幂等)", grantedToday); + return; + } + + // 查询昨日全部游戏消费流水 + List records = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper) + .between(TtUserBlendErcash::getCreateTime, beginTime, endTime) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes()) + .list(); + + if (CollectionUtils.isEmpty(records)) { + log.info("[VIP流水返利] 昨日无游戏流水记录,无需发放"); + return; + } + + // 按用户分组 + Map> userRecordsMap = records.stream() + .collect(Collectors.groupingBy(TtUserBlendErcash::getUserId)); + + // 预加载 VIP 等级配置 + Map vipLevelMap = vipLevelService.list().stream() + .filter(v -> v != null && v.getId() != null) + .collect(Collectors.toMap(TtVipLevel::getId, v -> v, (a, b) -> a)); + + int successCount = 0; + BigDecimal totalRebate = BigDecimal.ZERO; + + for (Map.Entry> entry : userRecordsMap.entrySet()) { + Integer userId = entry.getKey(); + try { + TtUser user = userService.getById(userId); + if (user == null) continue; + // 白号不发放 + if (Integer.valueOf(1).equals(user.getIsBlankAccount())) continue; + if (user.getVipLevel() == null || user.getVipLevel() <= 0) continue; + + TtVipLevel vipLevel = vipLevelMap.get(user.getVipLevel()); + if (vipLevel == null) { + log.warn("[VIP流水返利] 用户【{}】VIP等级不存在![{}]", userId, user.getVipLevel()); + continue; + } + + // 优先使用 flowRebatePercent;若为 null 则回退到 commissions + BigDecimal percent = vipLevel.getFlowRebatePercent() != null + ? vipLevel.getFlowRebatePercent() + : (vipLevel.getCommissions() != null ? vipLevel.getCommissions() : BigDecimal.ZERO); + if (percent.compareTo(BigDecimal.ZERO) <= 0) continue; + + // 昨日游戏流水总额(金额为负,取绝对值) + BigDecimal consumeAmount = entry.getValue().stream() + .map(TtUserBlendErcash::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .abs(); + if (consumeAmount.compareTo(BigDecimal.ZERO) <= 0) continue; + + BigDecimal rebate = consumeAmount + .multiply(percent) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + if (rebate.compareTo(BigDecimal.ZERO) <= 0) continue; + + // 直接加金币(不动积分,不走 updateUserAccount 避免递归触发佣金/返利) + int count = userMapper.addOnlyAccountAmount(Long.valueOf(userId), rebate); + if (count > 0) { + // 重新读取最新余额,写入流水快照 + TtUser freshUser = userService.getById(userId); + BigDecimal actualBalance = (freshUser != null) + ? freshUser.getAccountAmount() + : user.getAccountAmount().add(rebate); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId) + .amount(rebate) + .finalAmount(actualBalance) + .total(rebate) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.VIP_CONSUME_AWARD.getCode()) + .remark(TtAccountRecordSource.VIP_CONSUME_AWARD.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttUserBlendErcashMapper.insert(blendErcash); + successCount++; + totalRebate = totalRebate.add(rebate); + + // 发送邮件通知 + int displayVipLevel = user.getVipLevel() != null ? user.getVipLevel() - 1 : 0; + String consumeStr = consumeAmount.stripTrailingZeros().toPlainString(); + String rebateStr = rebate.stripTrailingZeros().toPlainString(); + String content = "尊敬的 VIP" + displayVipLevel + " 用户,您昨日累计流水 ¥" + + consumeStr + ",获得返利 " + rebateStr + ",已到账。"; + messageService.sendNotice( + Long.valueOf(userId), + MessageCategoryEnum.SYS_VIP_REBATE.getTitle(), + content, + MessageCategoryEnum.SYS_VIP_REBATE.getCode()); + } + } catch (Exception e) { + log.error("[VIP流水返利] 发放失败,userId={}", userId, e); + } + } + + long stop = System.currentTimeMillis(); + log.info("[VIP流水返利] 每日定时发放任务完成,发放用户数={},返利总额={},用时={}ms ----------", + successCount, totalRebate, (stop - start)); + } +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/service/TtTaskDoingService.java b/skins-service/service-task/src/main/java/com/ruoyi/task/service/TtTaskDoingService.java new file mode 100644 index 0000000..f6554c8 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/service/TtTaskDoingService.java @@ -0,0 +1,11 @@ +package com.ruoyi.task.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.domain.task.TtTask; +import com.ruoyi.domain.task.TtTaskDoing; + +import java.util.List; + +public interface TtTaskDoingService extends IService { + TtTaskDoing isOwnUser(Integer userId, Integer tid); +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/service/TtTaskService.java b/skins-service/service-task/src/main/java/com/ruoyi/task/service/TtTaskService.java new file mode 100644 index 0000000..e1c4f76 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/service/TtTaskService.java @@ -0,0 +1,17 @@ +package com.ruoyi.task.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.task.TtTask; +import com.ruoyi.domain.task.TtTaskDoing; +import com.ruoyi.domain.task.VO.TtTaskDoingVO; + +import java.util.List; + +public interface TtTaskService extends IService { + List taskOfme(Integer page, Integer size, Integer userId); + + AjaxResult getAward(Integer userId, Integer taskDoingid); + + AjaxResult firsDownLoadTask(Integer userId); +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/service/impl/TtTaskDoingServiceImpl.java b/skins-service/service-task/src/main/java/com/ruoyi/task/service/impl/TtTaskDoingServiceImpl.java new file mode 100644 index 0000000..c6b2c49 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/service/impl/TtTaskDoingServiceImpl.java @@ -0,0 +1,21 @@ +package com.ruoyi.task.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.task.TtTaskDoing; +import com.ruoyi.task.mapper.TtTaskDoingMapper; +import com.ruoyi.task.service.TtTaskDoingService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class TtTaskDoingServiceImpl extends ServiceImpl implements TtTaskDoingService { + + @Autowired + private TtUserService userService; + + @Override + public TtTaskDoing isOwnUser(Integer userId, Integer tid) { + return baseMapper.isOwnUser(userId,tid); + } +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/service/impl/TtTaskServiceImpl.java b/skins-service/service-task/src/main/java/com/ruoyi/task/service/impl/TtTaskServiceImpl.java new file mode 100644 index 0000000..5ce817b --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/service/impl/TtTaskServiceImpl.java @@ -0,0 +1,498 @@ +package com.ruoyi.task.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ruoyi.admin.service.TtUserCreditsRecordsService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.task.TtTask; +import com.ruoyi.domain.task.TtTaskDoing; +import com.ruoyi.domain.task.VO.TtTaskDoingVO; +import com.ruoyi.domain.task.constant.*; +import com.ruoyi.task.mapper.TtTaskMapper; +import com.ruoyi.task.service.TtTaskDoingService; +import com.ruoyi.task.service.TtTaskService; +import com.ruoyi.task.utils.TasKUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class TtTaskServiceImpl extends ServiceImpl implements TtTaskService { + + @Autowired + private TtUserService userService; + + @Autowired + private TtTaskDoingService ttTaskDoingService; + + @Autowired + private TtUserService ttUserService; + + @Autowired + private TtUserCreditsRecordsService ttUserCreditsRecordsService; + + @Autowired + private TasKUtil tasKUtil; + + @Override + public List taskOfme(Integer page, Integer size, Integer userId) { + + // 用户信息 + TtUser userInfo = userService.getById(userId); + + // 尝试新接任务 + AjaxResult r1 = tryCreateNewTaskDoing(userInfo); + + // 维护任务 + AjaxResult r2 = maintainTask(userInfo); + + // 查询结果 + LambdaQueryWrapper ttTaskDoingQuery = new LambdaQueryWrapper<>(); + ttTaskDoingQuery + .eq(TtTaskDoing::getUserId,userInfo.getUserId()) + .eq(TtTaskDoing::getCompletionState,TaskState.UP.getCode()); + List ownTask = ttTaskDoingService.list(ttTaskDoingQuery); + return ownTask.stream().map((item)->{ + + TtTask task = this.getById(item.getTaskId()); + TtTaskDoingVO vo = new TtTaskDoingVO(); + BeanUtil.copyProperties(task,vo); + vo.setTaskId(task.getId()); + vo.setTaskDoingId(item.getId()); + vo.setUserId(item.getUserId()); + vo.setBeginTime(item.getBeginTime()); + vo.setCompeteTime(item.getCompeteTime()); + vo.setCompletionState(item.getCompletionState()); + vo.setProgress(item.getProgress()); + return vo; + + }).collect(Collectors.toList()); + + } + + private AjaxResult maintainTask(TtUser userInfo) { + + // 已接任务 + LambdaQueryWrapper ttTaskDoingQuery = new LambdaQueryWrapper<>(); + ttTaskDoingQuery + .eq(TtTaskDoing::getUserId,userInfo.getUserId()); + List ownTask = ttTaskDoingService.list(ttTaskDoingQuery); + + ownTask.stream().forEach((item)->{ + if (!tasKUtil.updateTask(item)){ + log.warn("任务{}-类型{} 数据更新失败,检查任务配置信息!",item.getId(),item.getTaskId()); + } + }); + + + // LambdaQueryWrapper ttTaskQuery = new LambdaQueryWrapper<>(); + // LambdaUpdateWrapper ttTaskDoingUpdate = new LambdaUpdateWrapper<>(); + // + // // 维护普通任务{首次下载奖励} + // ttTaskQuery + // .eq(TtTask::getId,1); + // TtTask task1 = ttTaskService.getOne(ttTaskQuery); + // + // ttTaskDoingUpdate + // .eq(TtTaskDoing::getUserId,userInfo.getUserId()) + // .eq(TtTaskDoing::getTaskId,1) + // .set(TtTaskDoing::getProgress,task1.getTargetValue()) + // .set(TtTaskDoing::getCompletionState,TaskCompletionState.COMPLETION.getCode()) + // .set(TtTaskDoing::getCompeteTime,new Timestamp(System.currentTimeMillis())); + // ttTaskDoingService.update(ttTaskDoingUpdate); + // + // + // // 维护每日任务{每日流水奖励} + // ttTaskQuery.clear(); + // ttTaskQuery + // .eq(TtTask::getId,2); + // TtTask task2 = ttTaskService.getOne(ttTaskQuery); + // + // Calendar nowDay = Calendar.getInstance(); + // nowDay.set(Calendar.HOUR_OF_DAY, 0); + // nowDay.set(Calendar.MINUTE, 0); + // nowDay.set(Calendar.SECOND, 0); + // nowDay.set(Calendar.MILLISECOND, 0); + // Timestamp toDay = new Timestamp(nowDay.getTimeInMillis()); + // + // LambdaQueryWrapper creditsRecordsQuery = new LambdaQueryWrapper<>(); + // creditsRecordsQuery + // .eq(TtUserCreditsRecords::getUserId,userInfo.getUserId()) + // .ge(TtUserCreditsRecords::getCreateTime,toDay); + // List todayRecords = ttUserCreditsRecordsService.list(creditsRecordsQuery); + // + // // 累计今日流水 + // BigDecimal credits = new BigDecimal("0"); + // todayRecords.stream().forEach((item)->{ + // if (item.getCredits().compareTo(BigDecimal.ZERO)>=0){ + // credits.add(item.getCredits()); + // } + // }); + // + // ttTaskDoingUpdate.clear(); + // ttTaskDoingUpdate + // .eq(TtTaskDoing::getUserId,userInfo.getUserId()) + // .eq(TtTaskDoing::getTaskId,2) + // .set(TtTaskDoing::getProgress,credits.intValue()) + // .set(credits.compareTo(new BigDecimal(task2.getTargetValue()))>=0,TtTaskDoing::getCompletionState,TaskCompletionState.COMPLETION.getCode()) + // .set(credits.compareTo(new BigDecimal(task2.getTargetValue()))>=0,TtTaskDoing::getCompeteTime,new Timestamp(System.currentTimeMillis())); + // ttTaskDoingService.update(ttTaskDoingUpdate); + + return AjaxResult.success(); + } + + private AjaxResult tryCreateNewTaskDoing(TtUser userInfo) { + + // 已接任务 + LambdaQueryWrapper ttTaskDoingQuery = new LambdaQueryWrapper<>(); + ttTaskDoingQuery + .eq(TtTaskDoing::getUserId,userInfo.getUserId()); + List ownTask = ttTaskDoingService.list(ttTaskDoingQuery); + + // 所有上架的任务 + List allTask = baseMapper.listByState(TaskState.UP.getCode()); + + // 筛选可接的新任务 + List filterT = filterTask(allTask,ownTask); + + // 自动接手新任务 + ArrayList newTaskDoings = new ArrayList<>(); + for (TtTask task:filterT){ + + TtTaskDoing newTaskDoing = TtTaskDoing.builder() + .taskId(task.getId()) + .userId(userInfo.getUserId()) + .completionState(TaskCompletionState.DOING.getCode()) + .beginTime(new Timestamp(System.currentTimeMillis())) + .progress(0) + .build(); + newTaskDoings.add(newTaskDoing); + + } + // 保存新接的任务 + for (TtTaskDoing taskDoing:newTaskDoings){ + ttTaskDoingService.save(taskDoing); + } + + return AjaxResult.success(); + } + + @Override + public AjaxResult getAward(Integer userId, Integer taskDoingid) { + + // 任务是否存在 + TtTaskDoing taskDoing = ttTaskDoingService.isOwnUser(userId,taskDoingid); + if (ObjectUtil.isEmpty(taskDoing)){ + return AjaxResult.error("任务不存在"); + } + + // 是否可领取 + AjaxResult r = taskIsComplete(taskDoing); + if (!r.isSuccess()){ + return r; + } + + // 领奖 + getPrize(taskDoing); + + return AjaxResult.success("操作成功"); + } + + @Override + public AjaxResult firsDownLoadTask(Integer userId) { + + // 是否已完成 + LambdaQueryWrapper ttTaskDoingQuery = new LambdaQueryWrapper<>(); + ttTaskDoingQuery + .eq(TtTaskDoing::getUserId,userId) + .eq(TtTaskDoing::getTaskId,1); + TtTaskDoing one = ttTaskDoingService.getOne(ttTaskDoingQuery); + if (ObjectUtil.isNotEmpty(one)){ + return AjaxResult.success("首次下载任务已经完成,不能重复执行。"); + } + + // 接手任务并直接完成 + TtTask task = baseMapper.byId(1); + TtTaskDoing taskDoing = new TtTaskDoing(); + BeanUtil.copyProperties(task,taskDoing); + taskDoing.setUserId(userId); + taskDoing.setTaskId(task.getId()); + taskDoing.setProgress(1); + taskDoing.setCompletionState(TaskCompletionState.COMPLETION.getCode()); + taskDoing.setCompeteTime(new Timestamp(System.currentTimeMillis())); + ttTaskDoingService.save(taskDoing); + + return AjaxResult.success("任务完成。"); + + } + + private AjaxResult getPrize(TtTaskDoing taskDoing) { + TtTask task = baseMapper.byId(taskDoing.getTaskId()); + if (task.getAwardType().equals(TaskAwardType.CREDITS.getCode())){ + + BigDecimal prize = BigDecimal.ZERO; + + if (ObjectUtil.isEmpty(task.getAwardValue())){ + // 暂时没有动态奖励的任务 + // 计算奖金 + // prize = computerPrize(task, taskDoing.getUserId()); + } + prize = new BigDecimal(task.getAwardValue()); + + TtUser user = ttUserService.getById(taskDoing.getUserId()); + BigDecimal add = user.getAccountCredits().add(prize); + user.setAccountCredits(add); + + LambdaUpdateWrapper ttUserUpdate = new LambdaUpdateWrapper<>(); + ttUserUpdate + .eq(TtUser::getUserId,user.getUserId()) + .set(TtUser::getAccountCredits,add); + ttUserService.update(ttUserUpdate); + userService.insertUserCreditsRecords(user.getUserId(), TtAccountRecordType.INPUT, TtAccountRecordSource.TASK, prize, add,taskDoing.getTaskId()); + + taskDoing.setCompletionState(TaskCompletionState.COMPLETION_PRIZE.getCode()); + ttTaskDoingService.updateById(taskDoing); + + return AjaxResult.success("领取奖励:积分"+add.toString()); + }else if (task.getAwardType().equals(TaskAwardType.OTHER.getCode())){ + System.out.println("其他奖励类型"); + return AjaxResult.error("其他奖励类型开发中。"); + }else { + System.out.println("非法的奖励类型"); + return AjaxResult.error("非法的奖励类型。"); + } + } + + private BigDecimal computerPrize(TtTask task,Integer userId){ + + // 流水奖励动态计算奖金 + if (task.getId().equals(2)){ + + Calendar nowDay = Calendar.getInstance(); + nowDay.set(Calendar.HOUR_OF_DAY, 0); + nowDay.set(Calendar.MINUTE, 0); + nowDay.set(Calendar.SECOND, 0); + nowDay.set(Calendar.MILLISECOND, 0); + Timestamp toDay = new Timestamp(nowDay.getTimeInMillis()); + + nowDay.add(Calendar.DAY_OF_MONTH,-1); + Timestamp yesterday = new Timestamp(nowDay.getTimeInMillis()); + + LambdaQueryWrapper creditsRecordsQuery = new LambdaQueryWrapper<>(); + creditsRecordsQuery + .eq(TtUserCreditsRecords::getUserId,userId) + .ge(TtUserCreditsRecords::getCreateTime,yesterday) + .le(TtUserCreditsRecords::getCreateTime,toDay); + List list = ttUserCreditsRecordsService.list(creditsRecordsQuery); + + BigDecimal prize = new BigDecimal("0"); + list.stream().forEach((item)->{ + if (item.getCredits().compareTo(BigDecimal.ZERO)>=0){ + prize.add(item.getCredits()); + } + }); + + prize.multiply(new BigDecimal("0.01")); + return prize; + + }else { + return BigDecimal.ZERO; + } + + } + + private AjaxResult taskIsComplete(TtTaskDoing taskDoing) { + + TtTask task = baseMapper.byId(taskDoing.getTaskId()); + if (task.getType().equals(TaskType.COMMON.getCode())){ + + if (taskDoing.getCompletionState().equals(TaskCompletionState.COMPLETION_PRIZE.getCode())){ + return AjaxResult.error("不可重复领取奖励。"); + } + if (taskDoing.getCompletionState().equals(TaskCompletionState.COMPLETION.getCode())){ + return AjaxResult.success(); + } + + // if (taskDoing.getProgress()>=task.getTargetValue()){ + // return AjaxResult.success(); + // } + return AjaxResult.error("任务未完成,不可领奖。"); + }else if (task.getType().equals(TaskType.DAY.getCode())){ + + if (taskDoing.getCompletionState().equals(TaskCompletionState.COMPLETION.getCode())){ + return AjaxResult.success(); + } + return AjaxResult.error("未完成/已领取"); + + }else if (task.getType().equals(TaskType.WEEK.getCode())){ + System.out.println("WEEK"); + return AjaxResult.error("非法taskType"); + }else if (task.getType().equals(TaskType.MONTH.getCode())){ + System.out.println("MONTH"); + return AjaxResult.error("非法taskType"); + }else { + System.out.println("非法任务类型"); + return AjaxResult.error("非法taskType"); + } + + } + + // 废弃方法 + private List getDoingTask(List filterT,List ownTask, TtUser userInfo) { + + ArrayList result = new ArrayList<>(); + + // 自动接手新任务 + ArrayList newTaskDoings = new ArrayList<>(); + for (TtTask task:filterT){ + + TtTaskDoing newTaskDoing = TtTaskDoing.builder() + .taskId(task.getId()) + .userId(userInfo.getUserId()) + .completionState(TaskCompletionState.DOING.getCode()) + .beginTime(new Timestamp(System.currentTimeMillis())) + .progress(0) + .build(); + newTaskDoings.add(newTaskDoing); + + TtTaskDoingVO ttTaskDoingVO = new TtTaskDoingVO(); + BeanUtil.copyProperties(task,ttTaskDoingVO); + ttTaskDoingVO.setCompletionState(newTaskDoing.getCompletionState()); + ttTaskDoingVO.setProgress(newTaskDoing.getProgress()); + result.add(ttTaskDoingVO); + + } + // 保存新接的任务 + for (TtTaskDoing taskDoing:newTaskDoings){ + ttTaskDoingService.save(taskDoing); + } + + List collect = ownTask.stream().map((item) -> { + TtTaskDoingVO vo = new TtTaskDoingVO(); + TtTask byId = baseMapper.byId(item.getTaskId()); + BeanUtil.copyProperties(byId, vo); + vo.setTaskId(byId.getId()); + vo.setTaskDoingId(item.getId()); + vo.setCompletionState(item.getCompletionState()); + vo.setProgress(item.getProgress()); + vo.setBeginTime(item.getBeginTime()); + vo.setCompeteTime(item.getCompeteTime()); + return vo; + }).collect(Collectors.toList()); + // 维护旧任务 + collect.stream().forEach((item)->{ + if (item.getType().equals(TaskType.DAY.getCode())){ + + // 当天零点时间戳 + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + Timestamp currenDay = new Timestamp(c.getTimeInMillis()); + + // 任务开始时间 + Timestamp beginTime = item.getBeginTime(); + // 任务完成时间 + Timestamp competeTime = item.getCompeteTime(); + + // 尝试重置当日任务 + // 完成时间为空或小于当天零点 1刷新开始时间,2状态更新为完成 + if (ObjectUtil.isEmpty(competeTime) || competeTime.compareTo(currenDay)<0){ + UpdateWrapper updateWrapper = new UpdateWrapper(); + updateWrapper + .eq("id",item.getTaskDoingId()) + .set("begin_time",new Timestamp(System.currentTimeMillis())) + .set("completion_state",TaskCompletionState.COMPLETION.getCode()); + ttTaskDoingService.update(); + } + // 开始时间小于当天零点 并 未完成 刷新开始时间,状态更新为完成 + // 开始时间大于当天零点 并 进行中 1状态更新为完成,2更新完成时间 + // if (beginTime.compareTo(currenDay)>=0 && item.getCompletionState().equals(TaskCompletionState.DOING.getCode())){ + // UpdateWrapper updateWrapper = new UpdateWrapper(); + // updateWrapper + // .eq("id",item.getTaskDoingId()) + // .set("completion_state",TaskCompletionState.COMPLETION.getCode()) + // .set("compete_time",new Timestamp(System.currentTimeMillis())); + // ttTaskDoingService.update(); + // } + // 开始时间大于当天零点 并 已完成 不处理 + + }else if (item.getType().equals(TaskType.WEEK.getCode())){ + + }else if (item.getType().equals(TaskType.MONTH.getCode())){ + + }else { + + } + }); + + // 合并所有正在进行的任务 + result.addAll(collect); + + return result; + } + + //筛选可接的新任务 + private List filterTask(List allTask,List ownTask) { + + ArrayList result = new ArrayList<>(); + + // 条件过滤 + for (TtTask task:allTask){ + + String condition = task.getTaskCondition(); + if (StringUtils.isBlank(condition)||condition.equals("{}")){ + result.add(task); + continue; + } + + JSONObject conditionObj = JSON.parseObject(condition); + if (conditionObj.get("key1").equals("key1")){ + System.out.println("key1"); + }else if (conditionObj.get("key2").equals("key2")){ + System.out.println("key2"); + } + + } + + // 筛选可接的新任务 + ArrayList canDoingTasks = new ArrayList<>(); + for (TtTask task:result){ + + TtTaskDoing flag = null; + for (TtTaskDoing taskDoing:ownTask){ + if (taskDoing.getTaskId().equals(task.getId())){ + flag = taskDoing; + break; + } + } + + if (ObjectUtil.isEmpty(flag)){ + canDoingTasks.add(task); + } + } + + return canDoingTasks; + } +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/utils/TasKUtil.java b/skins-service/service-task/src/main/java/com/ruoyi/task/utils/TasKUtil.java new file mode 100644 index 0000000..a72ddcf --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/utils/TasKUtil.java @@ -0,0 +1,100 @@ +package com.ruoyi.task.utils; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.ruoyi.admin.service.TtUserCreditsRecordsService; +import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords; +import com.ruoyi.domain.task.TtTask; +import com.ruoyi.domain.task.TtTaskDoing; +import com.ruoyi.domain.task.constant.TaskCompletionState; +import com.ruoyi.task.mapper.TtTaskMapper; +import com.ruoyi.task.service.TtTaskDoingService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.List; + +@Component +public class TasKUtil { + + @Autowired + private TtTaskMapper ttTaskMapper; + + @Autowired + private TtTaskDoingService ttTaskDoingService; + + @Autowired + private TtUserCreditsRecordsService ttUserCreditsRecordsService; + + // 任务更新 + public boolean updateTask(TtTaskDoing ttTaskDoing) { + + Integer taskId = ttTaskDoing.getTaskId(); + + LambdaQueryWrapper ttTaskQuery = new LambdaQueryWrapper<>(); + LambdaUpdateWrapper ttTaskDoingUpdate = new LambdaUpdateWrapper<>(); + if (taskId.equals(1)) { + + // {首次下载奖励} + ttTaskQuery + .eq(TtTask::getId, 1); + TtTask task1 = ttTaskMapper.selectOne(ttTaskQuery); + + ttTaskDoingUpdate + .eq(TtTaskDoing::getUserId, ttTaskDoing.getUserId()) + .eq(TtTaskDoing::getTaskId, 1) + .set(TtTaskDoing::getProgress, task1.getTargetValue()) + .set(TtTaskDoing::getCompletionState, TaskCompletionState.COMPLETION.getCode()) + .set(TtTaskDoing::getCompeteTime, new Timestamp(System.currentTimeMillis())); + ttTaskDoingService.update(ttTaskDoingUpdate); + + return true; + } else if (taskId.equals(2)) { + + // {每日流水奖励} + ttTaskQuery.clear(); + ttTaskQuery + .eq(TtTask::getId, 2); + TtTask task2 = ttTaskMapper.selectOne(ttTaskQuery); + + Calendar nowDay = Calendar.getInstance(); + nowDay.set(Calendar.HOUR_OF_DAY, 0); + nowDay.set(Calendar.MINUTE, 0); + nowDay.set(Calendar.SECOND, 0); + nowDay.set(Calendar.MILLISECOND, 0); + Timestamp toDay = new Timestamp(nowDay.getTimeInMillis()); + + LambdaQueryWrapper creditsRecordsQuery = new LambdaQueryWrapper<>(); + creditsRecordsQuery + .eq(TtUserCreditsRecords::getUserId, ttTaskDoing.getUserId()) + .ge(TtUserCreditsRecords::getCreateTime, toDay); + List todayRecords = ttUserCreditsRecordsService.list(creditsRecordsQuery); + + // 累计今日流水 + BigDecimal credits = new BigDecimal("0"); + todayRecords.stream().forEach((item) -> { + if (item.getCredits().compareTo(BigDecimal.ZERO) >= 0) { + credits.add(item.getCredits()); + } + }); + + ttTaskDoingUpdate.clear(); + ttTaskDoingUpdate + .eq(TtTaskDoing::getUserId, ttTaskDoing.getUserId()) + .eq(TtTaskDoing::getTaskId, 2) + .set(TtTaskDoing::getProgress, credits.intValue()) + .set(credits.compareTo(new BigDecimal(task2.getTargetValue())) >= 0, TtTaskDoing::getCompletionState, TaskCompletionState.COMPLETION.getCode()) + .set(credits.compareTo(new BigDecimal(task2.getTargetValue())) >= 0, TtTaskDoing::getCompeteTime, new Timestamp(System.currentTimeMillis())); + ttTaskDoingService.update(ttTaskDoingUpdate); + + return true; + } else { + return false; + } + + } + +} diff --git a/skins-service/service-task/src/main/java/com/ruoyi/task/utils/task/PromotionWelfareTask.java b/skins-service/service-task/src/main/java/com/ruoyi/task/utils/task/PromotionWelfareTask.java new file mode 100644 index 0000000..1cea0d2 --- /dev/null +++ b/skins-service/service-task/src/main/java/com/ruoyi/task/utils/task/PromotionWelfareTask.java @@ -0,0 +1,69 @@ +package com.ruoyi.task.utils.task; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.task.DTO.pWelfareMQData; +import lombok.AllArgsConstructor; + +import java.math.BigDecimal; +import java.sql.Timestamp; + +// + +@AllArgsConstructor +public class PromotionWelfareTask implements Runnable { + + private TtUserService userService; + + private TtUser user; + + // 消费金额 + private BigDecimal price; + + @Override + public void run() { + // 统计推广福利 + pWelfareMQData msgDate = pWelfareMQData.builder() + .userId(user.getUserId()) + .account(price) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + + TtUser user = userService.getById(msgDate.getUserId()); + TtUser parent = userService.getById(user.getParentId()); + if (ObjectUtil.isNotEmpty(parent)){ + BigDecimal credits = null; + if ("01".equals(parent.getUserType())) { + credits = msgDate.getAccount().multiply(new BigDecimal("0.045")); + } else { + credits = msgDate.getAccount().multiply(new BigDecimal("0.01")); + } + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper + .eq(TtUser::getUserId,parent.getUserId()) + .set(TtUser::getAccountCredits,parent.getAccountCredits().add(credits)); + // .set(TtUser::getAccountAmount,parent.getAccountAmount().add(credits)); + userService.update(wrapper); + // userService.insertUserCreditsRecords(parent.getUserId(), + // TtAccountRecordType.INPUT, + // TtAccountRecordSource.P_WELFARE, + // credits, + // parent.getAccountCredits().add(credits), + // user.getUserId(), + // user.getNickName(), + // msgDate.getAccount()); + userService.insertUserAmountRecords(parent.getUserId(), + TtAccountRecordType.INPUT, + TtAccountRecordSource.P_WELFARE, + credits, + parent.getAccountCredits().add(credits), + user.getUserId(), + user.getNickName(), + msgDate.getAccount()); + } + } +} diff --git a/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtTaskDoingMapper.xml b/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtTaskDoingMapper.xml new file mode 100644 index 0000000..a6acdaa --- /dev/null +++ b/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtTaskDoingMapper.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtTaskMapper.xml b/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtTaskMapper.xml new file mode 100644 index 0000000..356c5d5 --- /dev/null +++ b/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtTaskMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtUserBlendErcashArchiveMapper.xml b/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtUserBlendErcashArchiveMapper.xml new file mode 100644 index 0000000..616f696 --- /dev/null +++ b/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtUserBlendErcashArchiveMapper.xml @@ -0,0 +1,43 @@ + + + + + + + + + + INSERT INTO tt_user_blend_ercash_archive + (id, user_id, anchor_id, business_id, amount, final_amount, credits, final_credits, total, source, type, remark, create_time, update_time, archive_time) + SELECT id, user_id, anchor_id, business_id, amount, final_amount, credits, final_credits, total, source, type, remark, create_time, update_time, NOW() + FROM tt_user_blend_ercash + WHERE id IN + + #{id} + + ON DUPLICATE KEY UPDATE id = tt_user_blend_ercash_archive.id + + + + + DELETE FROM tt_user_blend_ercash + WHERE id IN + + #{id} + + + + diff --git a/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtUserSourceReportTaskMapper.xml b/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtUserSourceReportTaskMapper.xml new file mode 100644 index 0000000..4a4a40d --- /dev/null +++ b/skins-service/service-task/src/main/resources/com/ruoyi/task/mapper/TtUserSourceReportTaskMapper.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + INSERT INTO tt_user_source_report (user_id, parent_id, source, total, report_date) + VALUES + + (#{item.userId}, #{item.parentId}, #{item.source}, #{item.total}, #{reportDate}) + + ON DUPLICATE KEY UPDATE + total = VALUES(total), + parent_id = VALUES(parent_id), + update_time = NOW() + + + + + + + + + diff --git a/skins-service/service-thirdparty/pom.xml b/skins-service/service-thirdparty/pom.xml new file mode 100644 index 0000000..69269f9 --- /dev/null +++ b/skins-service/service-thirdparty/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + com.ruoyi + skins-service + 4.8.2 + + + service-thirdparty + + + + + com.ruoyi + ruoyi-framework + + + + + com.ruoyi + message-service + 4.8.2 + + + + + com.ruoyi + service-admin + 4.8.2 + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + + + com.google.zxing + javase + 3.3.3 + + + + + com.alipay.sdk + alipay-sdk-java + 4.38.161.ALL + + + + org.apache.httpcomponents + httpclient + 4.5.1 + + + + + com.aliyun + aliyun-java-sdk-core + 4.6.4 + + + com.aliyun + aliyun-java-sdk-dysmsapi + 2.2.1 + + + + \ No newline at end of file diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/config/MYConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/config/MYConfig.java new file mode 100644 index 0000000..27628df --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/config/MYConfig.java @@ -0,0 +1,26 @@ +package com.ruoyi.thirdparty.MaYi.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "mayi") +public class MYConfig { + + private String apiKey; + private String gateway; + private String memberid; + private String notifyBaseUrl; + private String callBackUrl; + // (银行)通道编码 + private String payBankCode; + + // 下单接口 + public static final String ApiPayAddOrder = "/Pay_AddOrder"; + + // 查询订单接口 + public static final String ApiQueryTrans = "/Pay_Trade_query.html"; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/controller/MYController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/controller/MYController.java new file mode 100644 index 0000000..c133995 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/controller/MYController.java @@ -0,0 +1,90 @@ +package com.ruoyi.thirdparty.MaYi.controller; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.mayi.PayNotifyRequest; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.thirdparty.MaYi.config.MYConfig; +import com.ruoyi.thirdparty.MaYi.service.MYService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; + +@Api(tags = "聚合支付") +@Slf4j +@RestController +@RequestMapping("api/mayi") +public class MYController extends BaseController { + + @Autowired + private MYService myService; + + @Autowired + private TtUserService userService; + + // 下单 + @ApiOperation("预下单") + @PostMapping("/ApiAddTrans") + @UpdateUserCache + public R ApiAddTrans(HttpServletRequest request, @RequestBody @Validated CreateOrderParam param) { + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + Integer userId = ((Long) checkLogin.getData()).intValue(); + + TtUser user = userService.getById(userId); + // 是否实名认证0未认证,1已认证' + if (user.getIsRealCheck().equals("0")) { + return R.realNameRequired("请实名认证后再进行充值!"); + } + return myService.ApiAddTrans(param, user, request); + } + + // 支付回调 + @ApiOperation("支付回调") + // @PostMapping("/notify") + public String addTransNotify(@RequestBody PayNotifyRequest data) { + log.info("聚合支付支付回调 {}", JSONUtil.toJsonStr(data)); + return myService.payNotify(data); + } + + // 查询订单接口 + // @PostMapping("/ApiQueryTrans") + // public String ApiQueryTrans(HttpServletRequest request, Map param) { + // return "success"; + // } + // + // // 查询余额接口 + // @PostMapping("/ApiQueryBalancePHP") + // public String ApiQueryBalancePHP(HttpServletRequest request, Map param) { + // return "success"; + // } + + public R checkLogin() { + Long userId; + try { + userId = getUserId(); + if (ObjectUtil.isEmpty(userId)) return R.fail(401,"登录过期,请重新登录。"); + return R.ok(userId); + } catch (Exception e) { + return R.fail("登录过期,请重新登录。"); + } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/service/Impl/MYServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/service/Impl/MYServiceImpl.java new file mode 100644 index 0000000..456d87f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/service/Impl/MYServiceImpl.java @@ -0,0 +1,408 @@ +package com.ruoyi.thirdparty.MaYi.service.Impl; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.dto.mayi.ApiPayAddOrderResponse; +import com.ruoyi.domain.dto.mayi.PayNotifyRequest; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtPromotionLevel; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.MaYi.config.MYConfig; +import com.ruoyi.thirdparty.MaYi.service.MYService; +import com.ruoyi.thirdparty.MaYi.utils.signUtil; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import jakarta.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +@Slf4j +@Service +public class MYServiceImpl implements MYService { + + @Autowired + private MYConfig myConfig; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private Executor customThreadPoolExecutor; + + @Autowired + private TtBonusService bonusService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtRechargeProdMapper rechargeListMapper; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private TtPromotionLevelMapper ttPromotionLevelMapper; + + @Autowired + private TtFirstRechargeMapper ttFirstRechargeMapper; + + @Override + public R ApiAddTrans(@Validated CreateOrderParam param, TtUser user, HttpServletRequest request) { + + // 查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, param.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) return R.fail("不存在的商品。"); + if (goods.getPrice().compareTo(param.getGoodsPrice()) != 0) return R.fail("商品价格不一致。"); + + // 总价值 + BigDecimal totalAmount = param.getGoodsPrice().multiply(new BigDecimal(param.getGoodsNum())); + + // 构建完整参数 + Map map = buildBaseParam(new HashMap<>(), request); + + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + map.put("pay_orderid", orderId); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String tranDateTime = dateFormat.format(new Date()); + map.put("pay_applydate", tranDateTime); + + map.put("pay_bankcode", myConfig.getPayBankCode()); + + // 回调地址 + map.put("pay_notifyurl", myConfig.getNotifyBaseUrl()); + + // 同步跳转 + map.put("pay_callbackurl", myConfig.getCallBackUrl() + "/api/mayi/notify"); + + map.put("pay_amount", totalAmount.toString()); + + // 签名 + String sign = signUtil.getSign(map, myConfig.getApiKey()); + map.put("pay_md5sign", sign); + map.put("pay_clientip", IpUtils.getIpAddr()); + map.put("pay_productname", goods.getName()); + map.put("type", "json"); + + // System.out.println(JSONUtil.toJsonStr(map)); + + // 发送请求 + HttpRequest post = HttpUtil.createPost(myConfig.getGateway() + MYConfig.ApiPayAddOrder); + post.header("Content-Type","application/x-www-form-urlencoded"); + post.formStr(map); + HttpResponse res = post.execute(); + + // 解析响应 + ApiPayAddOrderResponse resBody = JSONUtil.toBean(res.body(), ApiPayAddOrderResponse.class); + if (!resBody.getStatus().equals("1")) { + return R.fail(resBody); + } + + // 创建订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(resBody.getPay_orderid()); + + order.setUserId(user.getUserId()); + order.setType(PayType.JU_HE_ZHI_FU.getCode()); + + order.setGoodsId(param.getGoodsId()); + order.setGoodsPrice(param.getGoodsPrice()); + order.setGoodsNum(param.getGoodsNum()); + order.setTotalAmount(totalAmount); + + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + + order.setPayUrl(resBody.getPayUrl()); + + order.setCreateTime(new Date()); + order.setUpdateTime(new Date()); + + orderMapper.insert(order); + return R.ok(order); + } + + @Override + public String payNotify(PayNotifyRequest data) { + TtOrder order = null; + // TODO: 2024/4/12 最好再查询一下第三方平台的订单信息 + try { + // 查询订单信息 + order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, data.getOrderid()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .one(); + + if (ObjectUtil.isEmpty(order)) { + log.warn("支付回调异常,不存在的有效订单。"); + return "fail"; + } + + // 查询用户信息 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + if (ObjectUtil.isEmpty(user)) { + log.warn("支付回调异常,不存在的有效用户。"); + return "fail"; + } + + if (order.getStatus().equals(PayOrderStatus.PAY_COMPLE.getCode()) || order.getStatus().equals(PayOrderStatus.PAY_YET.getCode())) { + log.warn("重复的回调!该订单已完成。"); + return "success"; + } + + // 查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeListMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + log.warn("支付回调异常,不存在的商品。"); + return "fail"; + } + + // 账户结算 + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + + // 首充赠送 + firstChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广等级充值赠送 + promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广充值返佣 + promotionChargeCommission(user, goods, order.getGoodsNum()); + + // 更新订单 + new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .update(); + + // 发送充值成功通知 + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(user.getUserId().toString(), order.getGoodsPrice()); + + return "success"; + } catch (Exception e) { + e.printStackTrace(); + + log.warn("聚合支付,回调异常"); + + if (ObjectUtil.isNotNull(order)) { + new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .set(TtOrder::getStatus, PayOrderStatus.CALL_BACK_ERRO.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .update(); + } + return "fail"; + } + } + + @Override + public R ApiQueryTrans(Map param, TtUser user) { + return null; + } + + @Override + public R ApiPropayTrans(Map param, TtUser user) { + return null; + } + + @Override + public R ApiQueryBalancePHP(Map param, TtUser user) { + return null; + } + + // 构建基本请求信息 + private Map buildBaseParam(Map map, HttpServletRequest request) { + map.put("pay_memberid", myConfig.getMemberid()); + return map; + } + + // 账户结算 + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + + // 加钱(充值不加积分:productA + productC 全部计入 account_amount) + // BigDecimal totalAmount = order.getTotalAmount(); + BigDecimal productAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal productGift = goods.getProductC().multiply(new BigDecimal(goodsNumber)); + BigDecimal totalAmount = productAmount.add(productGift); + BigDecimal totalCredits = BigDecimal.ZERO; + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount.toString()); + userService.update(userUpdate); + + user = userService.getById(user.getUserId()); + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + + .amount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? totalAmount : null) + .finalAmount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountAmount().add(totalAmount) : null) + + .credits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? totalCredits : null) + .finalCredits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountCredits().add(totalCredits) : null) + + .total(totalAmount.add(totalCredits)) // 收支合计 + + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + userBlendErcashMapper.insert(blendErcash); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } + + /** + * 首充赠送 + */ + public void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.判断是否为首充 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtUserBlendErcash::getSource, 1) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()); + List ttUserBlendErcashes = userBlendErcashMapper.selectList(wrapper); + if (ttUserBlendErcashes.size() > 0) { + return; + } + + // 2.加钱 + // 充值金额 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + // 查询赠送比例 + BigDecimal firstChargeAmountRatio = ttFirstRechargeMapper.selectRatioByMinAmount(totalAmount); + if (Objects.isNull(firstChargeAmountRatio)) { + return; + } + // 计算赠送金额 + BigDecimal giftAmount = totalAmount.multiply(firstChargeAmountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.FIRST_CHARGE.getCode()) + .remark(TtAccountRecordSource.FIRST_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广等级充值赠送 + */ + public void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.查询用户推广等级对应的赠送比例 + TtPromotionLevel ttPromotionLevel = ttPromotionLevelMapper.selectById(ttUser.getPromotionLevel()); + + // 2.加钱 + BigDecimal amountRatio = ttPromotionLevel.getRechargeGiftRatio(); + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal giftAmount = totalAmount.multiply(amountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getCode()) + .remark(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广充值返佣 + */ + private void promotionChargeCommission(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + if (Objects.isNull(ttUser.getParentId())) { + return; + } + // 充值返佣统一走 TtBonusService:按各级上级 commissionRate 逐级向上实时返佣、招商周结, + // 写 source=13,与卡密充值(ApiRechargeServiceImpl→bonus)口径一致,避免使用推广等级费率造成口径不符。 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + bonusService.distributeRechargeCommission(ttUser.getUserId(), totalAmount); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/service/MYService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/service/MYService.java new file mode 100644 index 0000000..b28b64b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/service/MYService.java @@ -0,0 +1,35 @@ +package com.ruoyi.thirdparty.MaYi.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.mayi.PayNotifyRequest; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; + +public interface MYService { + + R ApiAddTrans(CreateOrderParam param, TtUser user, HttpServletRequest request); + R ApiQueryTrans(Map param, TtUser user); + R ApiPropayTrans(Map param, TtUser user); + R ApiQueryBalancePHP(Map param, TtUser user); + + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber); + + String payNotify(PayNotifyRequest data); + + /** + * �׳����� + */ + public void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber); + + /** + * �ƹ�ȼ���ֵ���� + */ + public void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber); + + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/utils/ZYHTTPRes.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/utils/ZYHTTPRes.java new file mode 100644 index 0000000..941fc44 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/utils/ZYHTTPRes.java @@ -0,0 +1,47 @@ +package com.ruoyi.thirdparty.MaYi.utils; + +// zy返回值 + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ZYHTTPRes { + + public enum resCode{ + + success("0000"), + DOING("0001"); + + private String code; + + resCode(String code){ + this.code = code; + } + public String getCode(){ + return this.code; + } + } + + private String respCode; + //返回码说明 + private String respDesc; + //返回码 + private String payCode; + private String payDesc; + private String amt; + private String tranTime; + private String tranType; + //扩展信息 + private String msgExt; + private String payUrl; + // 交易流水号 + private String merReqNo; + //服务端响应流水号 + private String serverRspNo; + private String sign; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/utils/signUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/utils/signUtil.java new file mode 100644 index 0000000..7e08625 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/MaYi/utils/signUtil.java @@ -0,0 +1,39 @@ +package com.ruoyi.thirdparty.MaYi.utils; + +import cn.hutool.crypto.digest.DigestAlgorithm; +import cn.hutool.crypto.digest.Digester; +import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; + +import java.security.MessageDigest; +import java.util.*; + +public class signUtil { + + public static String getSign(Map map, String key) { + + // 排序 + List> entryList = new ArrayList<>(map.entrySet()); + Collections.sort(entryList, new Comparator>() { + @Override + public int compare(Map.Entry o1, Map.Entry o2) { + return o1.getKey().compareTo(o2.getKey()); + } + }); + + String str = ""; + for (int i = 0; i < entryList.size(); i++) { + // if (i == entryList.size()-1) { + // str = str + entryList.get(i).getKey() + "=" + entryList.get(i).getValue(); + // continue; + // } + str = str + entryList.get(i).getKey() + "=" + entryList.get(i).getValue() + "&"; + } + str = str + "key=" +key; + + // MD5加密 + Digester md5 = new Digester(DigestAlgorithm.MD5); + return md5.digestHex(str).toUpperCase(); + + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/config/AbkPayProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/config/AbkPayProperties.java new file mode 100644 index 0000000..4ef77fc --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/config/AbkPayProperties.java @@ -0,0 +1,43 @@ +package com.ruoyi.thirdparty.abkpay.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * abkpay 支付渠道配置,对应 application.yml 中 abkpay 节点。 + * 仅代收(充值),代收下单/查单/余额分别拼接 baseUrl + /Pay_SG、/Pay_Query、/Pay_Querybalance。 + */ +@Data +@ConfigurationProperties(prefix = "abkpay") +public class AbkPayProperties { + + /** API 基础地址(代收),例如 https://t5556qpd.abkoiuhnbrqwkjngpay.xyz */ + private String baseUrl; + + /** 商户号(mchId),由平台提供 —— 部署时填写 */ + private String mchId; + + /** 商户密钥(secret),由平台提供 —— 部署时填写 */ + private String secret; + + /** + * 默认产品编码(wayCode),金额<800 时使用。 + * 常规流量为 300<=金额<800 区间(<300 已由统一充值路由默认分配给 itpay)。 + */ + private String wayCode; + + /** 高额产品编码(wayCodeHigh),金额>=800 时使用;为空则统一使用 wayCode */ + private String wayCodeHigh; + + /** + * 客户端 IP(clientIp),下单必填。 + * 一般填写平台已加白的服务器出口 IP(白名单 IP)。 + */ + private String clientIp; + + /** 支付回调通知地址(notifyUrl),只有传该值平台才发起回调 */ + private String notifyUrl; + + /** 前端支付完成跳转地址(returnUrl,可空) */ + private String returnUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/controller/AbkPayController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/controller/AbkPayController.java new file mode 100644 index 0000000..f4bd449 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/controller/AbkPayController.java @@ -0,0 +1,125 @@ +package com.ruoyi.thirdparty.abkpay.controller; + +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.abkpay.model.AbkPayResult; +import com.ruoyi.thirdparty.abkpay.service.AbkPayPaymentService; +import com.ruoyi.thirdparty.chengduhipay.controller.OrderStatusRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * abkpay 支付渠道接口(仅代收)。 + * 接口清单: + * pay —— 创建充值订单(一般由统一充值按权重路由调用) + * notify —— 接收平台异步回调(平台调用,需返回字符串 SUCCESS) + * orderStatus —— 查询本地订单支付状态(供前端轮询) + * queryPay —— 主动查询平台订单 + */ +@Api(tags = "abkpay充值") +@Slf4j +@RestController +@RequestMapping("/api/abkpay") +public class AbkPayController { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private AbkPayPaymentService abkPayPaymentService; + + /** + * 创建充值订单(默认由统一充值路由调用,独立入口默认关闭) + */ + @ApiOperation("创建充值订单") + @UserPermission + @PostMapping("/pay") + public R pay(@RequestBody @Validated PayRequest param) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + if (ttUser == null) { + return R.fail("用户不存在"); + } + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired("未实名认证"); + } + return abkPayPaymentService.pay(userId, param); + } + + /** + * 异步回调通知(application/json)。 + * 平台订单成功或失败都会回调,商户须返回字符串 "SUCCESS"(大小写均可)。 + * 处理失败(验签失败/订单不存在等)返回非 SUCCESS,促使平台重推。 + */ + @ApiOperation("充值回调通知") + @Anonymous + @PostMapping("/notify") + @ResponseBody + public String notify(@RequestBody Map params) { + log.info("AbkPay notify params: {}", JSONUtil.toJsonStr(params)); + R result = abkPayPaymentService.notifyCallback(toStringMap(params)); + if (!R.isSuccess(result)) { + log.error("AbkPay notify 处理失败: {}", result.getMsg()); + return "fail"; + } + return "SUCCESS"; + } + + /** + * 查询本地订单支付状态(供前端轮询) + */ + @ApiOperation("查询本地订单支付状态") + @UserPermission + @PostMapping("/orderStatus") + public R> orderStatus(@RequestBody @Validated OrderStatusRequest param) { + Long userId = SecurityUtils.getUserId(); + return abkPayPaymentService.queryOrderStatus(userId, param.getOrderId()); + } + + /** + * 主动查询平台订单(仅允许查询本人订单) + */ + @ApiOperation("查询充值订单") + @UserPermission + @GetMapping("/queryPay") + public R queryPay(@RequestParam String outTradeNo) { + Long userId = SecurityUtils.getUserId(); + return abkPayPaymentService.queryPay(userId, outTradeNo); + } + + /** + * 将 JSON 回调参数(含数字类型)统一转为字符串 Map,供签名校验使用。 + */ + private Map toStringMap(Map params) { + Map result = new LinkedHashMap<>(); + if (params == null) { + return result; + } + for (Map.Entry entry : params.entrySet()) { + if (entry.getValue() != null) { + result.put(entry.getKey(), String.valueOf(entry.getValue())); + } + } + return result; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/model/AbkPayData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/model/AbkPayData.java new file mode 100644 index 0000000..29c6894 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/model/AbkPayData.java @@ -0,0 +1,71 @@ +package com.ruoyi.thirdparty.abkpay.model; + +import lombok.Data; + +/** + * abkpay 响应 data 业务数据(下单 / 查单 / 查余额共用,按接口取对应字段)。 + * 金额单位:分(例如 10000 表示 100.00 元)。 + */ +@Data +public class AbkPayData { + + /** 商户号 */ + private String mchId; + + /** 通道类型 / 产品编码(查单返回) */ + private String wayCode; + + /** 支付订单号(平台单号) */ + private String tradeNo; + + /** 商户订单号 */ + private String outTradeNo; + + /** 通道订单号 */ + private String originTradeNo; + + /** 订单金额(单位:分) */ + private String amount; + + /** 商品标题 */ + private String subject; + + /** 商品描述 */ + private String body; + + /** 商户扩展参数 */ + private String extParam; + + /** 异步通知地址 */ + private String notifyUrl; + + /** 支付地址(下单返回,展示给付款用户) */ + private String payUrl; + + /** 订单过期时间(13 位时间戳) */ + private String expiredTime; + + /** 支付成功时间(13 位时间戳) */ + private String successTime; + + /** 下单时间(13 位时间戳) */ + private String createTime; + + /** 订单状态:0=待支付,1=支付成功,2=支付失败 */ + private Integer state; + + /** 通知状态:0=未通知,1=通知成功,2=通知失败 */ + private Integer notifyState; + + /** SDK 参数内容(部分通道返回) */ + private String sdkData; + + /** 商户余额 / 跑量(查余额返回,单位:元) */ + private String balance; + + /** 商户可用额度 / 剩余预付(查余额返回,单位:元) */ + private String bond; + + /** 商户总预付(查余额返回,单位:元) */ + private String zbond; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/model/AbkPayNotifyData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/model/AbkPayNotifyData.java new file mode 100644 index 0000000..39b5677 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/model/AbkPayNotifyData.java @@ -0,0 +1,33 @@ +package com.ruoyi.thirdparty.abkpay.model; + +import lombok.Data; + +/** + * abkpay 代收异步回调通知数据(对接文档回调参数,仅作字段参考)。 + * 实际验签与处理以回调原始参数 Map 为准。金额单位:分。 + */ +@Data +public class AbkPayNotifyData { + /** 商户号 */ + private String mchId; + /** 支付订单号(平台单号) */ + private String tradeNo; + /** 商户订单号 */ + private String outTradeNo; + /** 通道订单号 */ + private String originTradeNo; + /** 订单金额(单位:分) */ + private String amount; + /** 商品标题 */ + private String subject; + /** 商品描述 */ + private String body; + /** 商户扩展参数(回调原样返回) */ + private String extParam; + /** 订单状态:0=待支付,1=支付成功,2=支付失败 */ + private String state; + /** 通知时间(13 位时间戳) */ + private String notifyTime; + /** 签名 */ + private String sign; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/model/AbkPayResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/model/AbkPayResult.java new file mode 100644 index 0000000..cc23270 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/model/AbkPayResult.java @@ -0,0 +1,19 @@ +package com.ruoyi.thirdparty.abkpay.model; + +import lombok.Data; + +/** + * abkpay 统一响应对象(下单 / 查单 / 查余额通用)。 + * code=0 表示网关成功,其它为失败;data 为业务数据。 + */ +@Data +public class AbkPayResult { + /** 返回状态码:0=成功,其它失败 */ + private Integer code; + /** 返回信息(失败原因,如:签名失败) */ + private String message; + /** 对 data 内数据的签名,如 data 为空则不返回 */ + private String sign; + /** 业务数据 */ + private AbkPayData data; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/service/AbkPayPaymentService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/service/AbkPayPaymentService.java new file mode 100644 index 0000000..31c91ee --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/service/AbkPayPaymentService.java @@ -0,0 +1,476 @@ +package com.ruoyi.thirdparty.abkpay.service; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.thirdparty.abkpay.config.AbkPayProperties; +import com.ruoyi.thirdparty.abkpay.model.AbkPayResult; +import com.ruoyi.thirdparty.abkpay.utils.AbkPaySignUtil; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse; +import com.ruoyi.thirdparty.common.service.PayFrequencyGuard; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * abkpay 支付渠道充值服务(仅代收)。 + * 实现:创建订单(Pay_SG)、异步回调(notify)、订单查询(Pay_Query)、余额查询(Pay_Querybalance)、本地订单状态查询。 + * 安全:回调只在 state==1 加币、校验回调金额与订单金额一致、DB条件CAS防重复加币、验签。 + * 请求:POST application/json;金额单位:分。 + */ +@Slf4j +@Service +@EnableConfigurationProperties(AbkPayProperties.class) +public class AbkPayPaymentService { + + @Autowired + private AbkPayProperties properties; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private PayFrequencyGuard payFrequencyGuard; + + @Autowired + private PlatformTransactionManager transactionManager; + + // ======================================================================== + // 创建订单:Pay_SG + // ======================================================================== + + public R pay(Long uid, PayRequest request) { + // 1. 查询商品 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, request.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品"); + } + + // 2. 生成商户单号(雪花ID,纯数字) + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + + // 3. 计算金额(元)与提交金额(分) + BigDecimal totalAmount = goods.getPrice(); + String amountFen = yuanToFen(totalAmount); + // subject 使用下单用户ID,便于渠道侧对账定位用户 + String subject = String.valueOf(uid); + + // 4. 组装参与签名的参数(wayCode 按金额分层:>=800 用 wayCodeHigh,否则用 wayCode) + Map params = new LinkedHashMap<>(); + params.put("mchId", properties.getMchId()); + params.put("wayCode", selectWayCode(totalAmount)); + params.put("subject", subject); + params.put("outTradeNo", orderId); + params.put("amount", amountFen); + if (StrUtil.isNotBlank(properties.getClientIp())) { + params.put("clientIp", properties.getClientIp()); + } + params.put("notifyUrl", properties.getNotifyUrl()); + if (StrUtil.isNotBlank(properties.getReturnUrl())) { + params.put("returnUrl", properties.getReturnUrl()); + } + params.put("reqTime", String.valueOf(System.currentTimeMillis())); + + // 5. 计算签名 + String sign = AbkPaySignUtil.sign(params, properties.getSecret()); + params.put("sign", sign); + + // 6. 发起下单请求(application/json) + String createUrl = properties.getBaseUrl() + "/Pay_SG.html"; + log.info("AbkPay createOrder url={}, params={}", createUrl, JSONUtil.toJsonStr(params)); + try (HttpResponse response = HttpRequest.post(createUrl) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(params)) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("AbkPay createOrder response status={}, body={}", response.getStatus(), body); + + AbkPayResult resBody = JSONUtil.toBean(body, AbkPayResult.class); + if (resBody.getCode() == null || resBody.getCode() != 0 || resBody.getData() == null) { + log.error("AbkPay 下单失败, code={}, message={}", resBody.getCode(), resBody.getMessage()); + return R.fail(StrUtil.isNotBlank(resBody.getMessage()) ? resBody.getMessage() : "下单失败"); + } + + // 7. 保存订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(resBody.getData().getTradeNo()); + order.setUserId(Math.toIntExact(uid)); + order.setType(PayType.ABK_PAY.getCode()); + order.setGoodsId(request.getGoodsId()); + order.setGoodsPrice(goods.getPrice()); + order.setGoodsNum(1); + order.setTotalAmount(totalAmount); + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + + // 8. 返回支付链接 + PayResponse resp = new PayResponse(); + resp.setPayUrl(resBody.getData().getPayUrl()); + resp.setOrderId(orderId); + resp.setPlatformOrderId(resBody.getData().getTradeNo()); + + log.info("AbkPay 下单成功, orderId={}, tradeNo={}", orderId, resBody.getData().getTradeNo()); + return R.ok(resp); + } catch (Exception e) { + log.error("AbkPay 下单异常", e); + return R.fail("支付通道异常,请稍后重试"); + } + } + + // ======================================================================== + // 异步回调:notify + // ======================================================================== + + /** + * 处理 abkpay 异步回调。 + * 防错加币:验签通过且 state==1(成功) 才结算;其它状态仅应答不加币。 + * 防金额篡改:校验回调金额(分)与本地订单金额一致。 + * 防重复加币:DB 条件CAS(NO_PAY -> PAY_COMPLE)取影响行数,仅抢占成功者加币,与结算同事务。 + */ + public R notifyCallback(Map params) { + log.info("AbkPay notify callback: {}", JSONUtil.toJsonStr(params)); + + // 1. 验签,失败直接拒绝,不加币 + if (!AbkPaySignUtil.verify(params, properties.getSecret())) { + log.error("AbkPay 回调签名验证失败"); + return R.fail("签名验证失败"); + } + + String outTradeNo = params.get("outTradeNo"); + String state = params.get("state"); + + // 2. 只有明确的成功状态(state==1)才加币 + if (!"1".equals(state)) { + log.warn("AbkPay 回调非成功状态,不加币, outTradeNo={}, state={}", outTradeNo, state); + return R.ok(); + } + + // 3. 查询本地订单(orderId == outTradeNo) + if (StrUtil.isBlank(outTradeNo)) { + log.error("AbkPay 回调缺少 outTradeNo"); + return R.fail("回调数据不完整"); + } + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, outTradeNo) + .eq(TtOrder::getType, PayType.ABK_PAY.getCode()) + .one(); + if (order == null) { + log.error("AbkPay 回调订单不存在, outTradeNo={}", outTradeNo); + return R.fail("订单不存在"); + } + + // 3.1 校验回调金额(分)与本地订单金额一致,防止金额篡改;金额缺失直接拒绝 + String callbackAmount = params.get("amount"); + String orderAmountFen = yuanToFen(order.getTotalAmount()); + if (StrUtil.isBlank(callbackAmount) || !callbackAmount.equals(orderAmountFen)) { + log.error("AbkPay 回调金额缺失或与订单金额不一致, outTradeNo={}, callback={}, order={}", + outTradeNo, callbackAmount, orderAmountFen); + return R.fail("回调金额与订单金额不一致"); + } + + // 4. DB条件CAS + 事务,保证同一成功回调重复推送只加一次币 + Boolean credited = new TransactionTemplate(transactionManager).execute(txStatus -> { + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(TtOrder::getId, order.getId()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()); + int rows = orderMapper.update(null, uw); + if (rows <= 0) { + // 已被处理,幂等跳过 + return Boolean.FALSE; + } + + // 抢占成功,执行结算 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + if (user == null) { + throw new IllegalStateException("AbkPay 回调用户不存在, userId=" + order.getUserId()); + } + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (goods == null) { + throw new IllegalStateException("AbkPay 回调商品不存在, goodsId=" + order.getGoodsId()); + } + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + return Boolean.TRUE; + }); + + if (Boolean.TRUE.equals(credited)) { + // 支付成功:清空该用户的未支付次数缓存 + payFrequencyGuard.clearOnPaid(order.getUserId().longValue()); + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice( + order.getUserId().toString(), order.getGoodsPrice()); + log.info("AbkPay 回调处理成功, orderId={}", order.getOrderId()); + } else { + log.info("AbkPay 回调幂等跳过(订单已处理), orderId={}", order.getOrderId()); + } + return R.ok(); + } + + // ======================================================================== + // 本地订单状态查询(供前端轮询) + // ======================================================================== + + public R> queryOrderStatus(Long userId, String orderId) { + if (ObjectUtil.isEmpty(orderId)) { + return R.fail("订单号不能为空"); + } + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, orderId) + .one(); + if (order == null) { + return R.fail("订单不存在"); + } + if (!Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) { + return R.fail("无权查询该订单"); + } + + String status = order.getStatus(); + String message; + if (PayOrderStatus.PAY_COMPLE.getCode().equals(status)) { + message = "充值成功"; + } else if (PayOrderStatus.NO_PAY.getCode().equals(status)) { + message = "支付中"; + } else if (PayOrderStatus.CANCEL.getCode().equals(status)) { + message = "订单已取消"; + } else if (PayOrderStatus.CALL_BACK_ERRO.getCode().equals(status)) { + message = "回调处理异常"; + } else { + message = "支付失败"; + } + + Map data = new HashMap<>(); + data.put("status", status); + data.put("amount", order.getGoodsPrice()); + data.put("message", message); + return R.ok(data); + } + + // ======================================================================== + // 订单查询:Pay_Query + // ======================================================================== + + public R queryPay(Long userId, String outTradeNo) { + if (StrUtil.isBlank(outTradeNo)) { + return R.fail("订单号不能为空"); + } + // 归属校验:只允许查询本人的 abkpay 订单 + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, outTradeNo) + .eq(TtOrder::getType, PayType.ABK_PAY.getCode()) + .one(); + if (order == null) { + return R.fail("订单不存在"); + } + if (userId == null || !Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) { + return R.fail("无权查询该订单"); + } + + Map params = new LinkedHashMap<>(); + params.put("mchId", properties.getMchId()); + params.put("outTradeNo", outTradeNo); + params.put("reqTime", String.valueOf(System.currentTimeMillis())); + String sign = AbkPaySignUtil.sign(params, properties.getSecret()); + params.put("sign", sign); + + String queryUrl = properties.getBaseUrl() + "/Pay_Query.html"; + log.info("AbkPay queryOrder url={}, params={}", queryUrl, JSONUtil.toJsonStr(params)); + try (HttpResponse response = HttpRequest.post(queryUrl) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(params)) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("AbkPay queryOrder response status={}, body={}", response.getStatus(), body); + AbkPayResult resBody = JSONUtil.toBean(body, AbkPayResult.class); + if (resBody.getCode() == null || resBody.getCode() != 0) { + return R.fail(StrUtil.isNotBlank(resBody.getMessage()) ? resBody.getMessage() : "查询失败"); + } + return R.ok(resBody); + } catch (Exception e) { + log.error("AbkPay 查单异常", e); + return R.fail("查询失败"); + } + } + + // ======================================================================== + // 余额查询:Pay_Querybalance + // ======================================================================== + + public R getBalance() { + Map params = new LinkedHashMap<>(); + params.put("mchId", properties.getMchId()); + params.put("reqTime", String.valueOf(System.currentTimeMillis())); + String sign = AbkPaySignUtil.sign(params, properties.getSecret()); + params.put("sign", sign); + + String balanceUrl = properties.getBaseUrl() + "/Pay_Querybalance.html"; + log.info("AbkPay getBalance url={}, params={}", balanceUrl, JSONUtil.toJsonStr(params)); + try (HttpResponse response = HttpRequest.post(balanceUrl) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(params)) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("AbkPay getBalance response status={}, body={}", response.getStatus(), body); + AbkPayResult resBody = JSONUtil.toBean(body, AbkPayResult.class); + if (resBody.getCode() == null || resBody.getCode() != 0 || resBody.getData() == null) { + return R.fail(StrUtil.isNotBlank(resBody.getMessage()) ? resBody.getMessage() : "查询失败"); + } + return R.ok(resBody.getData().getBalance()); + } catch (Exception e) { + log.error("AbkPay 余额查询异常", e); + return R.fail("查询失败"); + } + } + + // ======================================================================== + // 账户结算(与现有 hipay/richku/itpay 逻辑保持一致) + // ======================================================================== + + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + + BigDecimal oldAmount = user.getAccountAmount(); + if (oldAmount == null) { + oldAmount = BigDecimal.ZERO; + } + BigDecimal newAmount = oldAmount.add(totalAmount); + + // 更新用户余额 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount); + userService.update(userUpdate); + + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + .amount(totalAmount) + .finalAmount(newAmount) + .total(totalAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + + // 充值记录 + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(order.getUserId()); + ttRechargeRecord.setParentId(user.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(newAmount); + ttRechargeRecord.setOrderId(order.getOrderId()); + ttRechargeRecord.setOutTradeNo(order.getOutTradeNo()); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("1"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + ttRechargeRecordMapper.insert(ttRechargeRecord); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } + + // ======================================================================== + // 工具方法 + // ======================================================================== + + /** + * 按订单金额(元)选择产品编码: + * 金额>=800 且配置了 wayCodeHigh -> wayCodeHigh(如 8003);否则 -> 默认 wayCode(如 8012,覆盖 300~800)。 + * 说明:<300 的订单由统一充值路由默认分配给 itpay,正常不会到达本渠道; + * 若 itpay 停用导致 <300 回落到本渠道,也用默认 wayCode 下单,由平台侧限额规则决定是否受理。 + */ + private String selectWayCode(BigDecimal amount) { + if (StrUtil.isNotBlank(properties.getWayCodeHigh()) + && amount != null + && amount.compareTo(new BigDecimal("800")) >= 0) { + return properties.getWayCodeHigh(); + } + return properties.getWayCode(); + } + + /** + * 元转分(字符串整数)。例如 100.00 -> "10000"。 + */ + private String yuanToFen(BigDecimal yuan) { + if (yuan == null) { + return "0"; + } + return yuan.multiply(new BigDecimal(100)).setScale(0, RoundingMode.HALF_UP).toPlainString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/utils/AbkPaySignUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/utils/AbkPaySignUtil.java new file mode 100644 index 0000000..c8c704e --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/abkpay/utils/AbkPaySignUtil.java @@ -0,0 +1,71 @@ +package com.ruoyi.thirdparty.abkpay.utils; + +import cn.hutool.crypto.digest.DigestUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.TreeMap; + +/** + * abkpay 签名工具。 + * 签名算法(对接文档):所有非空且不为 "0" 的参数按参数名 ASCII 升序(字典序)-> + * 拼成 key1=value1&key2=value2&... -> 末尾追加 &key={商户密钥} -> MD5 -> 32 位小写。 + * 例:sign = md5( key1=value1&key2=value2...&key=商户密钥 ) + */ +@Slf4j +public class AbkPaySignUtil { + + /** + * 生成签名 + * + * @param params 参与签名的参数(不包含 sign 本身) + * @param secret 商户密钥 + * @return MD5 小写签名字符串 + */ + public static String sign(Map params, String secret) { + TreeMap sorted = new TreeMap<>(); + for (Map.Entry entry : params.entrySet()) { + String value = entry.getValue(); + // 对接文档要求:值为空、空字符串或 "0" 时均不参与签名 + if (value != null && !value.isEmpty() && !"0".equals(value)) { + sorted.put(entry.getKey(), value); + } + } + + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : sorted.entrySet()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + // 末尾追加商户密钥,格式为 &key={secret} + sb.append("&key=").append(secret); + + String signStr = sb.toString(); + // 注意:签名原文含商户密钥,禁止打印到日志 + return DigestUtil.md5Hex(signStr).toLowerCase(); + } + + /** + * 验证回调签名(大小写不敏感) + * + * @param params 完整回调参数(包含 sign) + * @param secret 商户密钥 + * @return 签名是否匹配 + */ + public static boolean verify(Map params, String secret) { + String receivedSign = params.get("sign"); + if (receivedSign == null || receivedSign.isEmpty()) { + return false; + } + TreeMap signParams = new TreeMap<>(params); + signParams.remove("sign"); + String calculatedSign = sign(signParams, secret); + boolean valid = calculatedSign.equalsIgnoreCase(receivedSign); + if (!valid) { + log.error("AbkPay 签名验证失败, expected={}, actual={}", calculatedSign, receivedSign); + } + return valid; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/config/AliCilent.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/config/AliCilent.java new file mode 100644 index 0000000..7610fa6 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/config/AliCilent.java @@ -0,0 +1,32 @@ +package com.ruoyi.thirdparty.alipay.config; + +import com.alipay.api.AlipayClient; +import com.alipay.api.DefaultAlipayClient; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@EnableConfigurationProperties(value = AliProperties.class) +public class AliCilent { + + @Bean + @Qualifier("RealNameAuthentication") + public AlipayClient RealNameAuthentication(AliProperties aliProperties) { + List merchants = aliProperties.getMerchants(); + if (merchants.isEmpty()) throw new IllegalArgumentException("No merchant configuration provided"); + MerchantConfig merchantConfig = merchants.get(1); + return new DefaultAlipayClient( + aliProperties.getServerUrl(), + merchantConfig.getAppId(), + merchantConfig.getPrivateKey(), + aliProperties.getFormat(), + aliProperties.getCharset(), + merchantConfig.getAlipayPublicKey(), + aliProperties.getSignType() + ); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/config/AliProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/config/AliProperties.java new file mode 100644 index 0000000..46ebcdb --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/config/AliProperties.java @@ -0,0 +1,32 @@ +package com.ruoyi.thirdparty.alipay.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@Data +@ConfigurationProperties(prefix = "alipay") +public class AliProperties { + + private String serverUrl; + + private String format; + + private String charset; + + private String signType; + + private List merchants; + + private String notifyUrl; + + private String realNameAuthenticationReturnUrl; + + private String authenticationHost; + + private String authenticationPath; + + private String authenticationAppcode; + +} \ No newline at end of file diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/config/MerchantConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/config/MerchantConfig.java new file mode 100644 index 0000000..5a29e84 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/config/MerchantConfig.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.alipay.config; + +import lombok.Data; + +@Data +public class MerchantConfig { + + private String appId; + + private String privateKey; + + private String alipayPublicKey; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/controller/AliPayController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/controller/AliPayController.java new file mode 100644 index 0000000..f630efc --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/controller/AliPayController.java @@ -0,0 +1,55 @@ +package com.ruoyi.thirdparty.alipay.controller; + +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.thirdparty.alipay.service.AliPayService; +import com.ruoyi.thirdparty.wechat.service.ApiTokenService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Api(tags = "支付宝") +@RestController +@RequestMapping("/api/alipay") +public class AliPayController { + + private final ApiTokenService tokenService; + private final AliPayService aliPayService; + private final TtUserService ttUserService; + + public AliPayController(ApiTokenService apiTokenService, + AliPayService aliPayService, + TtUserService ttUserService) { + this.tokenService = apiTokenService; + this.aliPayService = aliPayService; + this.ttUserService = ttUserService; + } + + @ApiOperation("支付") + @UserPermission + @PostMapping(value = "/trade/page/pay") + public R pay(@RequestBody @Validated CreateOrderParam param) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + // 是否实名认证(0未认证 1已认证) + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired(); + } + String ip = IpUtils.getIpAddr(); + return aliPayService.pay(param, ttUser, ip); + } + + // @PostMapping(value = "/callBack") + public String callBack(@RequestParam Map params) { + return aliPayService.callBack(params); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/AliPayService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/AliPayService.java new file mode 100644 index 0000000..02d0377 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/AliPayService.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.alipay.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; + +import java.util.Map; + +public interface AliPayService { + + R pay(CreateOrderParam param, TtUser ttUser, String ip); + + String callBack(Map params); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/RealNameAuthenticationService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/RealNameAuthenticationService.java new file mode 100644 index 0000000..bef6154 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/RealNameAuthenticationService.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.alipay.service; + +public interface RealNameAuthenticationService { + + String authInitialize(String realName, String idNum); + + String startCertify(String certifyId); + + String queryCertifyResult(String certifyId); + + String authentication(String realName, String idNum); + + String authentication2(String realName, String idNum, String phoneNum); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/impl/AliPayServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/impl/AliPayServiceImpl.java new file mode 100644 index 0000000..1a59386 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/impl/AliPayServiceImpl.java @@ -0,0 +1,373 @@ +package com.ruoyi.thirdparty.alipay.service.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayClient; +import com.alipay.api.AlipayConstants; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.domain.AlipayTradePrecreateModel; +import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.request.AlipayTradePrecreateRequest; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtPromotionLevel; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.alipay.config.AliProperties; +import com.ruoyi.thirdparty.alipay.service.AliPayService; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Slf4j +@Service +public class AliPayServiceImpl implements AliPayService { + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private AliProperties aliProperties; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private TtBonusService bonusService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtRechargeProdMapper rechargeListMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private TtPromotionLevelMapper ttPromotionLevelMapper; + + @Autowired + private TtFirstRechargeMapper ttFirstRechargeMapper; + + @Override + public R pay(CreateOrderParam param, TtUser ttUser, String ip) { + // 查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, param.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品。"); + } + if (goods.getPrice().compareTo(param.getGoodsPrice()) != 0) { + return R.fail("商品价格不一致。"); + } + + // 计算总价值 + BigDecimal totalAmount = param.getGoodsPrice().multiply(new BigDecimal(param.getGoodsNum())); + + // 构建支付 + String serverUrl = aliProperties.getServerUrl(); + String appId = aliProperties.getMerchants().get(0).getAppId(); + String privateKey = aliProperties.getMerchants().get(0).getPrivateKey(); + String format = aliProperties.getFormat(); + String charset = aliProperties.getCharset(); + String alipayPublicKey = aliProperties.getMerchants().get(0).getAlipayPublicKey(); + String signType = aliProperties.getSignType(); + String notifyUrl = aliProperties.getNotifyUrl(); + + // 初始化SDK + AlipayClient alipayClient = new DefaultAlipayClient(serverUrl, appId, privateKey, format, charset, alipayPublicKey, signType); + + // 构造请求参数以调用接口 + AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); + AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); + + // 设置通知地址 + request.setNotifyUrl(notifyUrl); + + // 设置订单标题 + model.setSubject(goods.getName()); + // 设置商户订单号 + String orderId = String.valueOf(IdUtil.getSnowflake(1, 1).nextId()); + model.setOutTradeNo(orderId); + // 设置订单总金额 + model.setTotalAmount(totalAmount.toString()); + request.setBizModel(model); + + try { + AlipayTradePrecreateResponse response = alipayClient.execute(request); + if (response.isSuccess()) { + // 创建订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(response.getOutTradeNo()); + order.setUserId(ttUser.getUserId()); + order.setType(PayType.ZFB.getCode()); + order.setGoodsId(param.getGoodsId()); + order.setGoodsPrice(param.getGoodsPrice()); + order.setGoodsNum(param.getGoodsNum()); + order.setTotalAmount(totalAmount); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + JSONObject jsonObject = JSON.parseObject(response.getBody()); + return R.ok(jsonObject); + } + } catch (AlipayApiException e) { + e.printStackTrace(); + } + return R.fail("支付失败"); + } + + @Override + public String callBack(Map params) { + log.info("支付成功通知"); + String result = "failure"; + String alipayPublicKey = aliProperties.getMerchants().get(0).getAlipayPublicKey(); + try { + // 异步通知校验 + boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayPublicKey, + AlipayConstants.CHARSET_UTF8, + AlipayConstants.SIGN_TYPE_RSA2); + if (!signVerified) { + log.error("支付成功,异步通知验签失败!"); + return result; + } + // 进行二次校验 + String tradeStatus = params.get("trade_status"); + if (!"TRADE_SUCCESS".equals(tradeStatus)) { + log.error("交易失败"); + return result; + } + // 处理业务 + // 查询订单信息 + String outTradeNo = params.get("out_trade_no"); + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOutTradeNo, outTradeNo) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .one(); + // 防止重复通知 + if (!PayOrderStatus.NO_PAY.getCode().equals(order.getStatus())) { + return result; + } + // 查询用户信息 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + // 查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeListMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + // 账户结算 + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + + try { + // 首充赠送 + firstChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广等级充值赠送 + promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广充值返佣 + promotionChargeCommission(user, goods, order.getGoodsNum()); + } catch (Exception e) { + e.printStackTrace(); + } + + // 更新订单 + boolean update = new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .update(); + if (update) { + result = "success"; + } + + // 发送充值成功通知 + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(order.getUserId().toString(), order.getGoodsPrice()); + } catch (AlipayApiException e) { + e.printStackTrace(); + } + return result; + } + + // 账户结算 + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + + // 加钱(充值不加积分:productA + productC 全部计入 account_amount) + // BigDecimal totalAmount = order.getTotalAmount(); + BigDecimal productAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal productGift = goods.getProductC().multiply(new BigDecimal(goodsNumber)); + BigDecimal totalAmount = productAmount.add(productGift); + BigDecimal totalCredits = BigDecimal.ZERO; + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount.toString()); + userService.update(userUpdate); + + user = userService.getById(user.getUserId()); + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + + .amount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? totalAmount : null) + .finalAmount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountAmount().add(totalAmount) : null) + + .credits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? totalCredits : null) + .finalCredits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountCredits().add(totalCredits) : null) + + .total(totalAmount.add(totalCredits)) // 收支合计 + + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + userBlendErcashMapper.insert(blendErcash); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } + + /** + * 首充赠送 + */ + private void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.判断是否为首充 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtUserBlendErcash::getSource, 1) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()); + List ttUserBlendErcashes = userBlendErcashMapper.selectList(wrapper); + if (ttUserBlendErcashes.size() > 0) { + return; + } + + // 2.加钱 + // 充值金额 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + // 查询赠送比例 + BigDecimal firstChargeAmountRatio = ttFirstRechargeMapper.selectRatioByMinAmount(totalAmount); + if (Objects.isNull(firstChargeAmountRatio)) { + return; + } + // 计算赠送金额 + BigDecimal giftAmount = totalAmount.multiply(firstChargeAmountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.FIRST_CHARGE.getCode()) + .remark(TtAccountRecordSource.FIRST_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广等级充值赠送 + */ + private void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.查询用户推广等级对应的赠送比例 + TtPromotionLevel ttPromotionLevel = ttPromotionLevelMapper.selectById(ttUser.getPromotionLevel()); + if (Objects.isNull(ttPromotionLevel)) + return; + + // 2.加钱 + BigDecimal amountRatio = ttPromotionLevel.getRechargeGiftRatio(); + if (Objects.isNull(amountRatio)) + return; + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal giftAmount = totalAmount.multiply(amountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getCode()) + .remark(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广充值返佣 + */ + private void promotionChargeCommission(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + if (Objects.isNull(ttUser.getParentId())) { + return; + } + // 充值返佣统一走 TtBonusService:按各级上级 commissionRate 逐级向上实时返佣、招商周结, + // 写 source=13,与卡密充值(ApiRechargeServiceImpl→bonus)口径一致,避免使用推广等级费率造成口径不符。 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + bonusService.distributeRechargeCommission(ttUser.getUserId(), totalAmount); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/impl/RealNameAuthenticationServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/impl/RealNameAuthenticationServiceImpl.java new file mode 100644 index 0000000..4efee45 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/service/impl/RealNameAuthenticationServiceImpl.java @@ -0,0 +1,225 @@ +package com.ruoyi.thirdparty.alipay.service.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSONObject; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayClient; +import com.alipay.api.request.AlipayUserCertifyOpenCertifyRequest; +import com.alipay.api.request.AlipayUserCertifyOpenInitializeRequest; +import com.alipay.api.request.AlipayUserCertifyOpenQueryRequest; +import com.alipay.api.response.AlipayUserCertifyOpenCertifyResponse; +import com.alipay.api.response.AlipayUserCertifyOpenInitializeResponse; +import com.alipay.api.response.AlipayUserCertifyOpenQueryResponse; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.thirdparty.alipay.config.AliProperties; +import com.ruoyi.thirdparty.alipay.service.RealNameAuthenticationService; +import com.ruoyi.thirdparty.apispace.config.ApiSpaceProperties; +import com.ruoyi.thirdparty.alipay.utils.HttpUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Service +@Slf4j +@EnableConfigurationProperties(ApiSpaceProperties.class) +public class RealNameAuthenticationServiceImpl implements RealNameAuthenticationService { + + @Qualifier("RealNameAuthentication") + private final AlipayClient RealNameAuthentication; + private final AliProperties aliProperties; + private final ApiSpaceProperties apiSpaceProperties; + + @Autowired + private RestTemplate restTemplate; + + public RealNameAuthenticationServiceImpl(AlipayClient realNameAuthentication, + AliProperties aliProperties, + ApiSpaceProperties apiSpaceProperties) { + RealNameAuthentication = realNameAuthentication; + this.aliProperties = aliProperties; + this.apiSpaceProperties = apiSpaceProperties; + } + + @Override + public String authInitialize(String realName, String idNum) { + AlipayUserCertifyOpenInitializeRequest request = new AlipayUserCertifyOpenInitializeRequest(); + JSONObject identityObj = new JSONObject(); + identityObj.put("identity_type", "CERT_INFO"); + identityObj.put("cert_type", "IDENTITY_CARD"); + identityObj.put("cert_name", realName); + identityObj.put("cert_no", idNum); + JSONObject merchantConfigObj = new JSONObject(); + try { + merchantConfigObj.put("return_url", aliProperties.getRealNameAuthenticationReturnUrl()); + JSONObject bizContentObj = new JSONObject(); + bizContentObj.put("outer_order_no", IdUtil.simpleUUID()); + bizContentObj.put("biz_code", "FACE"); + bizContentObj.put("identity_param", identityObj); + bizContentObj.put("merchant_config", merchantConfigObj); + request.setBizContent(bizContentObj.toString()); + AlipayUserCertifyOpenInitializeResponse response = RealNameAuthentication.execute(request); + if (response.isSuccess()) { + return response.getCertifyId(); + } else { + return null; + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + @Override + public String startCertify(String certifyId) { + AlipayUserCertifyOpenCertifyRequest request = new AlipayUserCertifyOpenCertifyRequest(); + JSONObject bizContentObj = new JSONObject(); + bizContentObj.put("certify_id", certifyId); + request.setBizContent(bizContentObj.toString()); + try { + AlipayUserCertifyOpenCertifyResponse response = RealNameAuthentication.pageExecute(request, "GET"); + if (response.isSuccess()) { + return "alipays://platformapi/startapp?appId=20000067&url=" + URLEncoder.encode(response.getBody(), "UTF-8"); + } else { + return null; + } + } catch (AlipayApiException | UnsupportedEncodingException e) { + return null; + } + } + + @Override + public String queryCertifyResult(String certifyId) { + AlipayUserCertifyOpenQueryRequest request = new AlipayUserCertifyOpenQueryRequest(); + JSONObject bizContentObj = new JSONObject(); + bizContentObj.put("certify_id", certifyId); + request.setBizContent(bizContentObj.toString()); + try { + AlipayUserCertifyOpenQueryResponse response = RealNameAuthentication.execute(request); + if (StrUtil.equals(response.getCode(), "40004")) { + return response.getSubMsg(); + } + if (response.isSuccess()) { + String body = response.getBody(); + log.info("返回的结构体:{}", body); + if (StringUtils.isNotBlank(body)) { + JSONObject jsonObject = JSONObject.parseObject(body); + JSONObject queryResponse = jsonObject.getJSONObject("alipay_user_certify_open_query_response"); + if (StrUtil.equals(queryResponse.getString("code"), "10000") && StrUtil.equals(queryResponse.getString("passed"), "T")) { + return ""; + } else return "认证失败,请重新认证!"; + } + } else return "认证查询服务调用失败!"; + } catch (AlipayApiException e) { + return "开始认证服务调用失败!"; + } + return "认证失败,请重新认证!"; + } + + @Override + public String authentication(String realName, String idNum) { + log.info("realName is {}, idNum is {}", realName, idNum); + String method = "POST"; + Map headers = new HashMap(); + //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105 + headers.put("Authorization", "APPCODE " + aliProperties.getAuthenticationAppcode()); + Map querys = new HashMap(); + Map bodys = new HashMap(); + bodys.put("idCardNo", idNum); + bodys.put("name", realName); + try { + /** + * 重要提示如下: + * HttpUtils请从 + * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java + * 下载 + * + * 相应的依赖请参照 + * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml + */ + HttpResponse response = HttpUtils.doPost(aliProperties.getAuthenticationHost(), aliProperties.getAuthenticationPath(), method, headers, querys, bodys); + log.info("authentication response code is {}", response.getStatusLine().getStatusCode()); + //获取response的body + String result = EntityUtils.toString(response.getEntity()); + log.info("authentication result is {}", result); + JSONObject jsonObject = JSONObject.parseObject(result); + int code = jsonObject.getIntValue("code"); + if (code == 400) { + return "身份证格式不正确!"; + } + if (code != 200) { + return "认证失败,请重新认证!"; + } + JSONObject data = jsonObject.getJSONObject("data"); + int res = data.getIntValue("result"); + if (res == 1) { + return "认证失败,请重新认证!"; + } + } catch (Exception e) { + log.error("authentication is {}", e.getMessage(), e); + return "认证失败,请重新认证!"; + } + return ""; + } + + + @Override + public String authentication2(String realName, String idNum, String phoneNum) { + log.info("realName is {}, idNum is {}, phoneNum is {}", realName, idNum, phoneNum); + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-APISpace-Token", apiSpaceProperties.getCarriersAuthToken()); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("name", realName); + map.add("idNum", idNum); + map.add("mobile", phoneNum); + + // 5. 组装请求实体 + HttpEntity> request = new HttpEntity<>(map, headers); + + // 6. 发送 POST 请求并获取响应 + ResponseEntity response = restTemplate.postForEntity(apiSpaceProperties.getCarriersAuthUrl(), + request, String.class); + + log.info("authentication response code is {}", response.getStatusCode()); + log.info("authentication result is {}", response.getBody()); + + JSONObject jsonObject = JSONObject.parseObject(response.getBody()); + if (jsonObject == null) { + return "认证失败,请重新认证!"; + } + String code = jsonObject.getString("code"); + if (!Objects.equals(code, "200000")) { + return "认证失败,请重新认证!"; + } + JSONObject data = jsonObject.getJSONObject("data"); + String res = data.getString("result"); + if (!Objects.equals(res, "01")) { + return "认证失败,请重新认证!"; + } + } catch (Exception e) { + log.error("authentication is {}", e.getMessage(), e); + return "认证失败,请重新认证!"; + } + return ""; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/utils/HttpUtils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/utils/HttpUtils.java new file mode 100644 index 0000000..866dac4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/alipay/utils/HttpUtils.java @@ -0,0 +1,311 @@ +package com.ruoyi.thirdparty.alipay.utils; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class HttpUtils { + + /** + * get + * + * @param host + * @param path + * @param method + * @param headers + * @param querys + * @return + * @throws Exception + */ + public static HttpResponse doGet(String host, String path, String method, + Map headers, + Map querys) + throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpGet request = new HttpGet(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + return httpClient.execute(request); + } + + /** + * post form + * + * @param host + * @param path + * @param method + * @param headers + * @param querys + * @param bodys + * @return + * @throws Exception + */ + public static HttpResponse doPost(String host, String path, String method, + Map headers, + Map querys, + Map bodys) + throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPost request = new HttpPost(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (bodys != null) { + List nameValuePairList = new ArrayList(); + + for (String key : bodys.keySet()) { + nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key))); + } + UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8"); + formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8"); + request.setEntity(formEntity); + } + + return httpClient.execute(request); + } + + /** + * Post String + * + * @param host + * @param path + * @param method + * @param headers + * @param querys + * @param body + * @return + * @throws Exception + */ + public static HttpResponse doPost(String host, String path, String method, + Map headers, + Map querys, + String body) + throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPost request = new HttpPost(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (StringUtils.isNotBlank(body)) { + request.setEntity(new StringEntity(body, "utf-8")); + } + + return httpClient.execute(request); + } + + /** + * Post stream + * + * @param host + * @param path + * @param method + * @param headers + * @param querys + * @param body + * @return + * @throws Exception + */ + public static HttpResponse doPost(String host, String path, String method, + Map headers, + Map querys, + byte[] body) + throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPost request = new HttpPost(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (body != null) { + request.setEntity(new ByteArrayEntity(body)); + } + + return httpClient.execute(request); + } + + /** + * Put String + * @param host + * @param path + * @param method + * @param headers + * @param querys + * @param body + * @return + * @throws Exception + */ + public static HttpResponse doPut(String host, String path, String method, + Map headers, + Map querys, + String body) + throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPut request = new HttpPut(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (StringUtils.isNotBlank(body)) { + request.setEntity(new StringEntity(body, "utf-8")); + } + + return httpClient.execute(request); + } + + /** + * Put stream + * @param host + * @param path + * @param method + * @param headers + * @param querys + * @param body + * @return + * @throws Exception + */ + public static HttpResponse doPut(String host, String path, String method, + Map headers, + Map querys, + byte[] body) + throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPut request = new HttpPut(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (body != null) { + request.setEntity(new ByteArrayEntity(body)); + } + + return httpClient.execute(request); + } + + /** + * Delete + * + * @param host + * @param path + * @param method + * @param headers + * @param querys + * @return + * @throws Exception + */ + public static HttpResponse doDelete(String host, String path, String method, + Map headers, + Map querys) + throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpDelete request = new HttpDelete(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + return httpClient.execute(request); + } + + private static String buildUrl(String host, String path, Map querys) throws UnsupportedEncodingException { + StringBuilder sbUrl = new StringBuilder(); + sbUrl.append(host); + if (!StringUtils.isBlank(path)) { + sbUrl.append(path); + } + if (null != querys) { + StringBuilder sbQuery = new StringBuilder(); + for (Map.Entry query : querys.entrySet()) { + if (0 < sbQuery.length()) { + sbQuery.append("&"); + } + if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) { + sbQuery.append(query.getValue()); + } + if (!StringUtils.isBlank(query.getKey())) { + sbQuery.append(query.getKey()); + if (!StringUtils.isBlank(query.getValue())) { + sbQuery.append("="); + sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8")); + } + } + } + if (0 < sbQuery.length()) { + sbUrl.append("?").append(sbQuery); + } + } + + return sbUrl.toString(); + } + + private static HttpClient wrapClient(String host) { + HttpClient httpClient = new DefaultHttpClient(); + if (host.startsWith("https://")) { + sslClient(httpClient); + } + + return httpClient; + } + + private static void sslClient(HttpClient httpClient) { + try { + SSLContext ctx = SSLContext.getInstance("TLS"); + X509TrustManager tm = new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return null; + } + public void checkClientTrusted(X509Certificate[] xcs, String str) { + + } + public void checkServerTrusted(X509Certificate[] xcs, String str) { + + } + }; + ctx.init(null, new TrustManager[] { tm }, null); + SSLSocketFactory ssf = new SSLSocketFactory(ctx); + ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + ClientConnectionManager ccm = httpClient.getConnectionManager(); + SchemeRegistry registry = ccm.getSchemeRegistry(); + registry.register(new Scheme("https", 443, ssf)); + } catch (KeyManagementException ex) { + throw new RuntimeException(ex); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/config/AliyunSmsConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/config/AliyunSmsConfig.java new file mode 100644 index 0000000..220071b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/config/AliyunSmsConfig.java @@ -0,0 +1,45 @@ +package com.ruoyi.thirdparty.aliyunsms.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 阿里云短信服务配置 + * 文档:https://help.aliyun.com/document_detail/101414.html + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "aliyun.sms") +public class AliyunSmsConfig { + + /** + * 阿里云 AccessKey ID + */ + private String accessKeyId; + + /** + * 阿里云 AccessKey Secret + */ + private String accessKeySecret; + + /** + * 短信签名(已审核通过的签名名称) + */ + private String signName; + + /** + * 短信模板 CODE(已审核通过的模板,且模板变量需为 ${code}) + */ + private String templateCode; + + /** + * 区域 ID,默认 cn-hangzhou + */ + private String regionId = "cn-hangzhou"; + + /** + * 服务端点 endpoint,默认 dysmsapi.aliyuncs.com + */ + private String endpoint = "dysmsapi.aliyuncs.com"; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/controller/AliyunSmsController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/controller/AliyunSmsController.java new file mode 100644 index 0000000..c6528d2 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/controller/AliyunSmsController.java @@ -0,0 +1,30 @@ +package com.ruoyi.thirdparty.aliyunsms.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.thirdparty.aliyunsms.service.AliyunSmsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 阿里云短信测试接口(管理端使用,避免对外暴露) + */ +@RestController +@RequestMapping("/admin/aliyunsms") +public class AliyunSmsController extends BaseController { + + @Autowired + private AliyunSmsService aliyunSmsService; + + /** + * 测试发送验证码:使用配置中的默认签名+模板,模板变量为 ${code} + */ + @GetMapping("/sendVerifyCode") + public AjaxResult sendVerifyCode(@RequestParam String mobile, @RequestParam String code) { + String result = aliyunSmsService.sendVerifyCode(mobile, code); + return "0".equals(result) ? success("发送成功") : AjaxResult.error("发送失败:" + result); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/service/AliyunSmsService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/service/AliyunSmsService.java new file mode 100644 index 0000000..20b0c95 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/service/AliyunSmsService.java @@ -0,0 +1,24 @@ +package com.ruoyi.thirdparty.aliyunsms.service; + +public interface AliyunSmsService { + + /** + * 发送短信验证码(使用配置中的默认模板) + * + * @param mobile 手机号 + * @param code 验证码 + * @return "0" 表示成功;其它返回阿里云返回的 Code(如 isv.MOBILE_NUMBER_ILLEGAL) + */ + String sendVerifyCode(String mobile, String code); + + /** + * 通用发送,可指定模板与模板参数 JSON + * + * @param mobile 手机号 + * @param signName 签名(为空则使用配置默认) + * @param templateCode 模板 Code(为空则使用配置默认) + * @param templateParam 模板参数 JSON 字符串,例如 {"code":"1234"} + * @return "0" 表示成功;其它返回阿里云返回的 Code + */ + String sendSms(String mobile, String signName, String templateCode, String templateParam); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/service/impl/AliyunSmsServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/service/impl/AliyunSmsServiceImpl.java new file mode 100644 index 0000000..e2f87f4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/aliyunsms/service/impl/AliyunSmsServiceImpl.java @@ -0,0 +1,86 @@ +package com.ruoyi.thirdparty.aliyunsms.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.aliyuncs.CommonRequest; +import com.aliyuncs.CommonResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.profile.DefaultProfile; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.thirdparty.aliyunsms.config.AliyunSmsConfig; +import com.ruoyi.thirdparty.aliyunsms.service.AliyunSmsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 阿里云短信服务实现,基于 aliyun-java-sdk-core + dysmsapi 通用调用方式 + */ +@Slf4j +@Service +public class AliyunSmsServiceImpl implements AliyunSmsService { + + @Autowired + private AliyunSmsConfig aliyunSmsConfig; + + @Override + public String sendVerifyCode(String mobile, String code) { + String templateParam = "{\"code\":\"" + code + "\"}"; + return sendSms(mobile, aliyunSmsConfig.getSignName(), aliyunSmsConfig.getTemplateCode(), templateParam); + } + + @Override + public String sendSms(String mobile, String signName, String templateCode, String templateParam) { + if (StringUtils.isEmpty(aliyunSmsConfig.getAccessKeyId()) || StringUtils.isEmpty(aliyunSmsConfig.getAccessKeySecret())) { + log.error("阿里云短信未配置 AccessKey,请检查 aliyun.sms 配置"); + return "CONFIG_ERROR"; + } + String useSign = StringUtils.isNotEmpty(signName) ? signName : aliyunSmsConfig.getSignName(); + String useTemplate = StringUtils.isNotEmpty(templateCode) ? templateCode : aliyunSmsConfig.getTemplateCode(); + + try { + DefaultProfile profile = DefaultProfile.getProfile( + aliyunSmsConfig.getRegionId(), + aliyunSmsConfig.getAccessKeyId(), + aliyunSmsConfig.getAccessKeySecret() + ); + IAcsClient client = new DefaultAcsClient(profile); + + CommonRequest request = new CommonRequest(); + request.setSysMethod(MethodType.POST); + request.setSysDomain(aliyunSmsConfig.getEndpoint()); + request.setSysVersion("2017-05-25"); + request.setSysAction("SendSms"); + request.putQueryParameter("RegionId", aliyunSmsConfig.getRegionId()); + request.putQueryParameter("PhoneNumbers", mobile); + request.putQueryParameter("SignName", useSign); + request.putQueryParameter("TemplateCode", useTemplate); + if (StringUtils.isNotEmpty(templateParam)) { + request.putQueryParameter("TemplateParam", templateParam); + } + + CommonResponse response = client.getCommonResponse(request); + String data = response.getData(); + log.info("阿里云短信发送 mobile={}, sign={}, template={}, param={}, result={}", + mobile, useSign, useTemplate, templateParam, data); + + if (StringUtils.isEmpty(data)) { + return "EMPTY_RESPONSE"; + } + JSONObject json = JSONObject.parseObject(data); + String respCode = json.getString("Code"); + if ("OK".equalsIgnoreCase(respCode)) { + return "0"; + } + return respCode; + } catch (ClientException e) { + log.error("阿里云短信发送异常 mobile={}, errCode={}, errMsg={}", mobile, e.getErrCode(), e.getErrMsg(), e); + return e.getErrCode() == null ? "CLIENT_ERROR" : e.getErrCode(); + } catch (Exception e) { + log.error("阿里云短信发送未知异常 mobile={}", mobile, e); + return "UNKNOWN_ERROR"; + } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/apispace/config/ApiSpaceProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/apispace/config/ApiSpaceProperties.java new file mode 100644 index 0000000..d2c64ee --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/apispace/config/ApiSpaceProperties.java @@ -0,0 +1,20 @@ +package com.ruoyi.thirdparty.apispace.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "apispace") +public class ApiSpaceProperties { + + /** + * 运营商三要素实名认证 Token + */ + private String carriersAuthToken; + + /** + * 运营商三要素实名认证 URL + */ + private String carriersAuthUrl; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/config/BaiduConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/config/BaiduConfig.java new file mode 100644 index 0000000..6d91f3d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/config/BaiduConfig.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.baidu.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "baidu") +public class BaiduConfig { + private String baseUrl; + private String token; + private String logidUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/domain/body/BaiduConversionType.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/domain/body/BaiduConversionType.java new file mode 100644 index 0000000..c9206e6 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/domain/body/BaiduConversionType.java @@ -0,0 +1,9 @@ +package com.ruoyi.thirdparty.baidu.domain.body; + +import lombok.Data; + +@Data +public class BaiduConversionType { + private String logidUrl; + private Integer newType; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/domain/body/BaiduUploadConvertData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/domain/body/BaiduUploadConvertData.java new file mode 100644 index 0000000..0474439 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/domain/body/BaiduUploadConvertData.java @@ -0,0 +1,11 @@ +package com.ruoyi.thirdparty.baidu.domain.body; + +import lombok.Data; + +import java.util.List; + +@Data +public class BaiduUploadConvertData { + private String token; + private List conversionTypes; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/service/BaiduService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/service/BaiduService.java new file mode 100644 index 0000000..7fd8e25 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/service/BaiduService.java @@ -0,0 +1,6 @@ +package com.ruoyi.thirdparty.baidu.service; + +public interface BaiduService { + + void upload(Integer userId, Integer newType, String bdVid); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/service/impl/BaiduServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/service/impl/BaiduServiceImpl.java new file mode 100644 index 0000000..d518f96 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baidu/service/impl/BaiduServiceImpl.java @@ -0,0 +1,67 @@ +package com.ruoyi.thirdparty.baidu.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.thirdparty.baidu.config.BaiduConfig; +import com.ruoyi.thirdparty.baidu.domain.body.BaiduConversionType; +import com.ruoyi.thirdparty.baidu.domain.body.BaiduUploadConvertData; +import com.ruoyi.thirdparty.baidu.service.BaiduService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Slf4j +@Service +public class BaiduServiceImpl implements BaiduService { + + @Autowired + private BaiduConfig baiduConfig = new BaiduConfig(); + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + //注册49 + //购买10 + @Override + public void upload(Integer userId, Integer newType, String bdVid) { + log.info("userId is {}, newType is {}, bdVid is {}", userId, newType, bdVid); + if (StringUtils.isBlank(bdVid) || newType == null || userId == null) { + return; + } + if (newType != 10 && newType != 49) { + return; + } + if (Objects.equals(10, newType)) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(TtUserBlendErcash::getUserId, userId) + .eq(TtUserBlendErcash::getType, TtAccountRecordType.INPUT.getCode()) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.RECHARGE.getCode()) + ; + // 大于1非首次充值 + List ercashes = ttUserBlendErcashMapper.selectList(queryWrapper); + if (ercashes.size() != 1) { + return; + } + } + // 构建请求头 + Map headers = new HashMap<>(); + BaiduUploadConvertData data = new BaiduUploadConvertData(); + data.setToken(baiduConfig.getToken()); + BaiduConversionType type = new BaiduConversionType(); + type.setNewType(newType); + type.setLogidUrl(baiduConfig.getLogidUrl() + bdVid); + data.setConversionTypes(Collections.singletonList(type)); + String body = JSON.toJSONString(data); + String result = HttpUtils.sendPostJSONString(baiduConfig.getBaseUrl(), body, headers); + log.info("body is {}, result is {}", body, result); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/config/BaiweiPayConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/config/BaiweiPayConfig.java new file mode 100644 index 0000000..f475569 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/config/BaiweiPayConfig.java @@ -0,0 +1,36 @@ +package com.ruoyi.thirdparty.baiweipay.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "baiweipay") +public class BaiweiPayConfig { + + /** + * 服务器URL + */ + private String serverUrl; + + /** + * 商户ID + */ + private String pid; + + /** + * 签名 + */ + private String sign; + + /** + * 通知URL + */ + private String notifyUrl; + + /** + * 返回URL + */ + private String returnUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/controller/BaiweiPayController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/controller/BaiweiPayController.java new file mode 100644 index 0000000..82e53f1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/controller/BaiweiPayController.java @@ -0,0 +1,50 @@ +package com.ruoyi.thirdparty.baiweipay.controller; + +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.thirdparty.baiweipay.service.BaiweiPayService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Api(tags = "百威支付") +@Slf4j +@RestController +@RequestMapping("api/baiweipay") +public class BaiweiPayController { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private BaiweiPayService baiweiPayService; + + @ApiOperation("支付") + @UserPermission + @PostMapping(value = "/pay") + public R pay(@RequestBody @Validated CreateOrderParam param) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + // 是否实名认证(0未认证 1已认证) + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired(); + } + String ip = IpUtils.getIpAddr(); + return baiweiPayService.pay(param, ttUser, ip); + } + + // @GetMapping("/notify") + public String notify(@RequestParam Map params) { + return baiweiPayService.notify(params); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/domain/BaiweiPayResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/domain/BaiweiPayResponse.java new file mode 100644 index 0000000..c712d4d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/domain/BaiweiPayResponse.java @@ -0,0 +1,19 @@ +package com.ruoyi.thirdparty.baiweipay.domain; + +import lombok.Data; + +@Data +public class BaiweiPayResponse { + + private String code; + + private String msg; + + private String tradeNo; + + private String payurl; + + private String qrcode; + + private String urlscheme; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/service/BaiweiPayService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/service/BaiweiPayService.java new file mode 100644 index 0000000..e99038f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/service/BaiweiPayService.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.baiweipay.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; + +import java.util.Map; + +public interface BaiweiPayService { + + R pay(CreateOrderParam param, TtUser ttUser, String ip); + + String notify(Map params); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/service/impl/BaiweiPayServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/service/impl/BaiweiPayServiceImpl.java new file mode 100644 index 0000000..80d5bfa --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/service/impl/BaiweiPayServiceImpl.java @@ -0,0 +1,403 @@ +package com.ruoyi.thirdparty.baiweipay.service.impl; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtPromotionLevel; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.baiweipay.config.BaiweiPayConfig; +import com.ruoyi.thirdparty.baiweipay.service.BaiweiPayService; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import com.ruoyi.thirdparty.baiweipay.domain.BaiweiPayResponse; +import com.ruoyi.thirdparty.baiweipay.util.BaiweiPayUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; + +@Slf4j +@Service +public class BaiweiPayServiceImpl implements BaiweiPayService { + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private BaiweiPayConfig baiweiPayConfig; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtRechargeProdMapper rechargeListMapper; + + @Autowired + private TtBonusService bonusService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private TtPromotionLevelMapper ttPromotionLevelMapper; + + @Autowired + private TtFirstRechargeMapper ttFirstRechargeMapper; + + @Autowired + private RestTemplate restTemplate; + + @Override + public R pay(CreateOrderParam param, TtUser ttUser, String ip) { + // 1.查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, param.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品"); + } + if (goods.getPrice().compareTo(param.getGoodsPrice()) != 0) { + return R.fail("商品价格不一致"); + } + + // 计算总价值 + BigDecimal totalAmount = param.getGoodsPrice().multiply(new BigDecimal(param.getGoodsNum())); + + // 2.构建支付参数 + Map map = new HashMap<>(); + map.put("pid", baiweiPayConfig.getPid()); + map.put("type", "alipay"); + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + map.put("out_trade_no", orderId); + map.put("notify_url", baiweiPayConfig.getNotifyUrl()); + map.put("return_url", baiweiPayConfig.getReturnUrl()); + map.put("name", goods.getName()); + map.put("money", String.valueOf(totalAmount)); + map.put("clientip", ip); + String key = baiweiPayConfig.getSign(); + BaiweiPayUtil signUtil = new BaiweiPayUtil(key); + String sign = signUtil.getSign(map); + map.put("sign", sign); + map.put("sign_type", "MD5"); + + // 3.发送请求 + // HttpRequest post = HttpUtil.createPost(baiweiPayConfig.getServerUrl()); + // post.header("Content-Type","application/x-www-form-urlencoded"); + // post.formStr(map); + // HttpResponse res = post.execute(); + + // 切换请求 + // 获取服务器 URL + String url = baiweiPayConfig.getServerUrl(); + + // 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + // 构建表单数据 + MultiValueMap formParams = new LinkedMultiValueMap<>(); + for (Map.Entry entry : map.entrySet()) { + formParams.add(entry.getKey(), entry.getValue()); + } + + // 构建请求实体 + HttpEntity> requestEntity = new HttpEntity<>(formParams, headers); + + // 发送 POST 请求 + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + + // 获取响应 + String result = response.getBody(); + + // 4.解析响应 + BaiweiPayResponse resBody = JSONUtil.toBean(result, BaiweiPayResponse.class); + if (!resBody.getCode().equals("1")) { + return R.fail(resBody.getMsg()); + } + + // 5.创建订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(resBody.getTradeNo()); + order.setUserId(ttUser.getUserId()); + order.setType(PayType.GFHT_PAY.getCode()); + order.setGoodsId(param.getGoodsId()); + order.setGoodsPrice(param.getGoodsPrice()); + order.setGoodsNum(param.getGoodsNum()); + order.setTotalAmount(totalAmount); + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + + return R.ok(resBody); + } + + @Override + public String notify(Map params) { + log.info("支付成功通知"); + String result = "failure"; + + // 校验 + String tradeStatus = params.get("trade_status"); + if (!"TRADE_SUCCESS".equals(tradeStatus)) { + log.error("交易失败"); + return result; + } + + // 查询订单信息 + String outTradeNo = params.get("trade_no"); + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOutTradeNo, outTradeNo) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .one(); + // 防止重复通知 + if (!PayOrderStatus.NO_PAY.getCode().equals(order.getStatus())) { + return result; + } + + // 查询用户信息 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + + // 3.查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeListMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + + // 4.账户结算 + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + + try { + // 首充赠送 + firstChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广等级充值赠送 + promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广充值返佣 + promotionChargeCommission(user, goods, order.getGoodsNum()); + } catch (Exception e) { + e.printStackTrace(); + } + + // 5.更新订单 + boolean update = new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .update(); + if (update) { + result = "success"; + } + + // 6.发送充值通知 + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(order.getUserId().toString(), order.getGoodsPrice()); + + return result; + } + + // 账户结算 + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + + // 加钱(充值不加积分:productA + productC 全部计入 account_amount) + // BigDecimal totalAmount = order.getTotalAmount(); + BigDecimal productAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal productGift = goods.getProductC().multiply(new BigDecimal(goodsNumber)); + BigDecimal totalAmount = productAmount.add(productGift); + BigDecimal totalCredits = BigDecimal.ZERO; + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount.toString()); + userService.update(userUpdate); + + user = userService.getById(user.getUserId()); + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + + .amount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? totalAmount : null) + .finalAmount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountAmount().add(totalAmount) : null) + + .credits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? totalCredits : null) + .finalCredits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountCredits().add(totalCredits) : null) + + .total(totalAmount.add(totalCredits)) // 收支合计 + + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + userBlendErcashMapper.insert(blendErcash); + + // 充值记录 + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(order.getUserId()); + ttRechargeRecord.setParentId(user.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(user.getAccountAmount()); + ttRechargeRecord.setOrderId(order.getOrderId()); + ttRechargeRecord.setOutTradeNo(order.getOutTradeNo()); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("1"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + ttRechargeRecordMapper.insert(ttRechargeRecord); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } + + /** + * 首充赠送 + */ + private void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.判断是否为首充 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtUserBlendErcash::getSource, 1) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()); + List ttUserBlendErcashes = userBlendErcashMapper.selectList(wrapper); + if (ttUserBlendErcashes.size() > 0) { + return; + } + + // 2.加钱 + // 充值金额 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + // 查询赠送比例 + BigDecimal firstChargeAmountRatio = ttFirstRechargeMapper.selectRatioByMinAmount(totalAmount); + if (Objects.isNull(firstChargeAmountRatio)) { + return; + } + // 计算赠送金额 + BigDecimal giftAmount = totalAmount.multiply(firstChargeAmountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.FIRST_CHARGE.getCode()) + .remark(TtAccountRecordSource.FIRST_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广等级充值赠送 + */ + @Async + public void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.查询用户推广等级对应的赠送比例 + TtPromotionLevel ttPromotionLevel = ttPromotionLevelMapper.selectById(ttUser.getPromotionLevel()); + if (Objects.isNull(ttPromotionLevel)) + return; + + // 2.加钱 + BigDecimal amountRatio = ttPromotionLevel.getRechargeGiftRatio(); + if (Objects.isNull(amountRatio)) + return; + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal giftAmount = totalAmount.multiply(amountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getCode()) + .remark(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广充值返佣 + */ + private void promotionChargeCommission(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + if (Objects.isNull(ttUser.getParentId())) { + return; + } + // 充值返佣统一走 TtBonusService:按各级上级 commissionRate 逐级向上实时返佣、招商周结, + // 写 source=13,与卡密充值(ApiRechargeServiceImpl→bonus)口径一致,避免使用推广等级费率造成口径不符。 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + bonusService.distributeRechargeCommission(ttUser.getUserId(), totalAmount); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/util/BaiweiPayUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/util/BaiweiPayUtil.java new file mode 100644 index 0000000..d3fe6b5 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/baiweipay/util/BaiweiPayUtil.java @@ -0,0 +1,76 @@ +package com.ruoyi.thirdparty.baiweipay.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.TreeMap; + +public class BaiweiPayUtil { + + private String key; + + public BaiweiPayUtil(String key) { + this.key = key; + } + + /** + * 生成MD5签名 + * + * @param params 参数列表 + * @return 生成的签名 + */ + public String getSign(Map params) { + // 排序参数 + Map sortedParams = new TreeMap<>(params); + sortedParams.remove("sign"); + sortedParams.remove("sign_type"); + + // 构建签名字符串 + StringBuilder signStr = new StringBuilder(); + for (Map.Entry entry : sortedParams.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + signStr.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); + } + } + + if (signStr.length() > 0) { + signStr.setLength(signStr.length() - 1); // 移除最后一个& + } + + // 添加商户密钥 + signStr.append(this.key); + + // 计算MD5签名 + return md5(signStr.toString()).toLowerCase(); + } + + /** + * 计算MD5哈希值 + * + * @param input 输入字符串 + * @return MD5哈希值 + */ + private static String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(input.getBytes()); + return bytesToHex(messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 algorithm not found", e); + } + } + + /** + * 将字节数组转换为十六进制字符串 + * + * @param bytes 字节数组 + * @return 十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/config/ChengduHipayProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/config/ChengduHipayProperties.java new file mode 100644 index 0000000..edbe471 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/config/ChengduHipayProperties.java @@ -0,0 +1,30 @@ +package com.ruoyi.thirdparty.chengduhipay.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 成都Hipay充值配置,对应application.yml中chengdu-hipay节点 + */ +@Data +@ConfigurationProperties(prefix = "chengdu-hipay") +public class ChengduHipayProperties { + + /** API基础地址,如 https://xahnob7yqke3.hipay.one */ + private String baseUrl; + + /** 商户编号,由API平台提供 */ + private String recvId; + + /** API密钥,由API平台提供 */ + private String apiKey; + + /** 充值通道编号,由API平台提供 */ + private String payChannelId; + + /** 支付回调通知地址 */ + private String notifyUrl; + + /** 前端支付完成跳转地址 */ + private String returnUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/ChengduHipayController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/ChengduHipayController.java new file mode 100644 index 0000000..fb81160 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/ChengduHipayController.java @@ -0,0 +1,139 @@ +package com.ruoyi.thirdparty.chengduhipay.controller; + +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.chengduhipay.model.PaymentData; +import com.ruoyi.thirdparty.chengduhipay.service.ChengduHipayPaymentService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.security.MessageDigest; +import java.util.Map; + +/** + * 成都Hipay充值接口,对应API文档V1.0.4 + * 接口清单: + * POST /api/chengduhipay/pay —— 创建充值订单 + * POST /api/chengduhipay/notify —— 接收支付回调通知(Hipay平台调用) + * GET /api/chengduhipay/queryPay —— 查询充值订单 + * GET /api/chengduhipay/balance —— 查询商户余额 + */ +@Api(tags = "成都Hipay充值") +@Slf4j +@RestController +@RequestMapping("/api/chengduhipay") +public class ChengduHipayController { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private ChengduHipayPaymentService chengduHipayPaymentService; + + /** + * 创建充值订单 + * 对应API文档2.1节:POST /pay/pay/createpay + */ + @ApiOperation("创建充值订单") + @UserPermission + // @PostMapping("/pay") + public R pay(@RequestBody @Validated PayRequest param) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired("未实名认证"); + } + String ip = IpUtils.getIpAddr(); + return chengduHipayPaymentService.pay(userId, param); + } + + /** + * 充值回调通知 + * 由Hipay平台在用户支付成功后调用,对应API文档2.2节 + * 通用返回格式: {"code":1, "msg":"success", "data":"{...}"} + * data字段为JSON字符串,需要反序列化 + */ + @ApiOperation("充值回调通知") + @Anonymous + // @PostMapping("/notify") + public String notify(@RequestBody Map params) { + log.info("ChengduHipay notify params: {}", JSONUtil.toJsonStr(params)); + chengduHipayPaymentService.notifyCallback(params); + return "success"; + } + + /** + * 查询本地订单支付状态(供前端调起支付后轮询使用) + * 前端轮询逻辑建议:每 2~3 秒调一次,最多 60 秒,遇到 success=true 即弹出“充值成功”并刷新余额。 + * @param param 请求体,包含创建充值接口返回的本地订单号 orderId + */ + @ApiOperation("查询本地订单支付状态") + @UserPermission + @PostMapping("/orderStatus") + public R> orderStatus(@RequestBody @Validated OrderStatusRequest param) { + Long userId = SecurityUtils.getUserId(); + return chengduHipayPaymentService.queryOrderStatus(userId, param.getOrderId()); + } + + /** + * 查询充值订单 + * 对应API文档2.3节:GET /pay/pay/getpay + * @param Id Hipay平台返回的订单唯一编号 + */ + @ApiOperation("查询充值订单") + @GetMapping("/queryPay") + public R queryPay(@RequestParam String Id) { + return chengduHipayPaymentService.getPay(Id); + } + + /** + * 查询商户余额 + * 对应API文档4节:GET /pay/pay/getbalance + */ + @ApiOperation("查询商户余额") + @GetMapping("/balance") + public R balance() { + return chengduHipayPaymentService.getBalance(); + } + + public static void main(String[] args) throws Exception { + // 常量定义 + final String RECV_ID = "e7c0f86eff4296c636b8d290300efd29"; + final String API_KEY = "922297c5e1dc44c78e8b4afac7315b16"; + final String ORDER_ID = "2060184782830178304"; + final String AMOUNT = "100.00"; + + // 1. 计算 sign = md5("pay" + recvid + orderid + amount + apikey) + String signStr = "pay" + RECV_ID + ORDER_ID + AMOUNT + API_KEY; + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] signDigest = md.digest(signStr.getBytes("UTF-8")); + StringBuilder signSb = new StringBuilder(); + for (byte b : signDigest) { + signSb.append(String.format("%02x", b & 0xff)); + } + String sign = signSb.toString(); + + // 2. 计算 resign = md5(sign + apikey) + String resignStr = sign + API_KEY; + byte[] resignDigest = md.digest(resignStr.getBytes("UTF-8")); + StringBuilder resignSb = new StringBuilder(); + for (byte b : resignDigest) { + resignSb.append(String.format("%02x", b & 0xff)); + } + String resign = resignSb.toString(); + + // 输出结果 + System.out.println("sign = " + sign); + System.out.println("resign = " + resign); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/OrderStatusRequest.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/OrderStatusRequest.java new file mode 100644 index 0000000..7a0486a --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/OrderStatusRequest.java @@ -0,0 +1,15 @@ +package com.ruoyi.thirdparty.chengduhipay.controller; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 查询本地订单支付状态请求参数 + */ +@Data +public class OrderStatusRequest { + + @NotBlank(message = "订单号不能为空") + private String orderId; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/PayRequest.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/PayRequest.java new file mode 100644 index 0000000..76a5423 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/PayRequest.java @@ -0,0 +1,19 @@ +package com.ruoyi.thirdparty.chengduhipay.controller; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 用户充值请求参数 + */ +@Data +public class PayRequest { + @NotNull(message = "商品id不能为空") + private Integer goodsId; + + /** + * 支付方式:1=支付宝,2=银联 + */ + private Integer payType; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/PayResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/PayResponse.java new file mode 100644 index 0000000..5e8b6c0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/controller/PayResponse.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.chengduhipay.controller; + +import lombok.Data; + +/** + * 充值响应返回给前端的数据 + */ +@Data +public class PayResponse { + /** 二维码地址 */ + private String qrUrl; + /** H5支付页面地址 */ + private String payUrl; + /** 平台订单号 */ + private String orderId; + /** Hipay平台订单唯一编号,用于查询接口 */ + private String platformOrderId; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/enums/OrderTypeEnum.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/enums/OrderTypeEnum.java new file mode 100644 index 0000000..5aa18f3 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/enums/OrderTypeEnum.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.chengduhipay.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 订单类型,对应API文档1.2.2节 + */ +@AllArgsConstructor +@Getter +public enum OrderTypeEnum { + RECHARGE(1, "充值订单"), + WITHDRAW(3, "代付订单"), + ; + + private final int code; + private final String desc; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/enums/StateEnum.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/enums/StateEnum.java new file mode 100644 index 0000000..1360f4b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/enums/StateEnum.java @@ -0,0 +1,20 @@ +package com.ruoyi.thirdparty.chengduhipay.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 订单状态,对应API文档1.2.3节 + */ +@AllArgsConstructor +@Getter +public enum StateEnum { + CREATE(2, "已创建"), + EXCHANGE(5, "已转币"), + CANCEL(9, "已取消"), + ERROR(99, "错误"), + ; + + private final int code; + private final String desc; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/model/CreatePayResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/model/CreatePayResult.java new file mode 100644 index 0000000..99e7bd0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/model/CreatePayResult.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.chengduhipay.model; + +import lombok.Data; + +/** + * 创建充值订单API响应,对应API文档通用返回对象+2.1.5节 + */ +@Data +public class CreatePayResult { + /** 返回代码,1=成功,非1=错误 */ + private int code; + /** 返回消息 */ + private String msg; + /** 业务数据 */ + private PaymentData data; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/model/NotifyData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/model/NotifyData.java new file mode 100644 index 0000000..1a71171 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/model/NotifyData.java @@ -0,0 +1,44 @@ +package com.ruoyi.thirdparty.chengduhipay.model; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 充值回调通知数据,对应API文档2.2.3节返回参数 + */ +@Data +public class NotifyData { + /** 平台唯一标识ID */ + private String id; + /** 商户订单号 */ + private String orderid; + /** 订单类型:1=充值 3=代付 */ + private int ordertype; + /** 订单状态:5=已转币才算支付成功 */ + private int state; + /** 收币方编号 */ + private String recvid; + /** 币数量 */ + private BigDecimal amount; + /** 收币手续费 */ + private BigDecimal recvcharge; + /** 创建日期 */ + private String createtime; + /** 收币方昵称 */ + private String recvnickname; + /** 收币方头像 */ + private String recvheader; + /** 转币日期 */ + private String transtime; + /** 前台跳转地址 */ + private String returnurl; + /** 说明 */ + private String note; + /** 系统备注 */ + private String remark; + /** 签名 */ + private String sign; + /** 验签字段 resign=md5(sign+apikey) */ + private String resign; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/model/PaymentData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/model/PaymentData.java new file mode 100644 index 0000000..56872c0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/model/PaymentData.java @@ -0,0 +1,54 @@ +package com.ruoyi.thirdparty.chengduhipay.model; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 充值订单数据详情,对应API文档2.1.4节返回参数 + */ +@Data +public class PaymentData { + /** 平台唯一标识ID */ + private String id; + /** 商户订单号 */ + private String orderid; + /** 订单类型:1=充值 3=代付 */ + private int ordertype; + /** 订单状态:2=已创建 5=已转币 9=已取消 99=错误 */ + private int state; + /** 收币方编号 */ + private String recvid; + /** 币数量 */ + private BigDecimal amount; + /** 收币手续费 */ + private BigDecimal recvcharge; + /** 创建日期 yyyy-MM-dd HH:mm:ss */ + private String createtime; + /** 收币方昵称 */ + private String recvnickname; + /** 收币方头像 */ + private String recvheader; + /** 转币日期 yyyy-MM-dd HH:mm:ss */ + private String transtime; + /** 二维码地址 */ + private String qrurl; + /** 回调地址 */ + private String notifyurl; + /** 二维码内容 */ + private String qrcode; + /** 前台跳转地址 */ + private String returnurl; + /** 说明(请求原样返回) */ + private String note; + /** 系统备注 */ + private String remark; + /** 验签字段 resign=md5(sign+apikey),创建订单时为空 */ + private String resign; + /** 签名(请求签名原样返回) */ + private String sign; + /** H5支付页面地址 */ + private String payurl; + /** 收款银行信息JSON字符串(仅银行卡通道有数据) */ + private String recvinfo; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/service/ChengduHipayPaymentService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/service/ChengduHipayPaymentService.java new file mode 100644 index 0000000..c939d8f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/chengduhipay/service/ChengduHipayPaymentService.java @@ -0,0 +1,525 @@ +package com.ruoyi.thirdparty.chengduhipay.service; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MoneyUtil; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.thirdparty.chengduhipay.config.ChengduHipayProperties; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse; +import com.ruoyi.thirdparty.chengduhipay.enums.StateEnum; +import com.ruoyi.thirdparty.chengduhipay.model.CreatePayResult; +import com.ruoyi.thirdparty.chengduhipay.model.NotifyData; +import com.ruoyi.thirdparty.chengduhipay.model.PaymentData; +import com.ruoyi.thirdparty.common.service.PayFrequencyGuard; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.DigestUtils; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.sql.Timestamp; +import java.util.*; + +/** + * 成都Hipay充值服务,严格按照API文档V1.0.4实现 + * 实现接口: + * 2.1 创建充值接口 —— POST /pay/pay/createpay + * 2.2 充值回调接口 —— 验签+账户结算 + * 2.3 查询充值单接口 —— GET /pay/pay/getpay + * 4 查询余额接口 —— GET /pay/pay/getbalance + */ +@Slf4j +@Service +@EnableConfigurationProperties(ChengduHipayProperties.class) +public class ChengduHipayPaymentService { + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private ChengduHipayProperties properties; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Autowired + private PayFrequencyGuard payFrequencyGuard; + + // ======================================================================== + // 渠道选择:根据支付方式 + 充值金额,选择手续费最低的渠道 + // ======================================================================== + + /** + * 按费率从低到高返回支付渠道优先列表,当前金额覆盖的排前面,未覆盖的排后面作为降级。 + * + * 渠道清单: + * ─── 支付宝(payType=1)─── + * 637 支付宝超大 2000~10000 4.8% + * 612 支付宝大额 800~10000 5.5% + * 621 支付宝中额 500~5000 5.8% + * 610 支付宝小额 300~2000 6.0% + * 616 超级支付宝 100~10000 12% + * ─── 银联(payType=2)─── + * 601 数字人民币 100~5000 0.7% + * 653 银联扫码 30~500 1.0% + */ + private List selectPayChannelPriority(Integer payType, BigDecimal amount) { + int amt = amount.intValue(); + + // 1=支付宝 + if (payType != null && payType == 1) { + List preferred = new ArrayList<>(); + List fallback = new ArrayList<>(); + if (amt >= 2000 && amt <= 10000) preferred.add("637"); else fallback.add("637"); + if (amt >= 800 && amt <= 10000) preferred.add("612"); else fallback.add("612"); + if (amt >= 500 && amt <= 5000) preferred.add("621"); else fallback.add("621"); + if (amt >= 300 && amt <= 2000) preferred.add("610"); else fallback.add("610"); + if (amt >= 100 && amt <= 10000) preferred.add("616"); else fallback.add("616"); + preferred.addAll(fallback); + return preferred; + } + + // 2=银联 + if (payType != null && payType == 2) { + List preferred = new ArrayList<>(); + List fallback = new ArrayList<>(); + if (amt >= 100 && amt <= 5000) preferred.add("601"); else fallback.add("601"); + if (amt >= 30 && amt <= 500) preferred.add("653"); else fallback.add("653"); + preferred.addAll(fallback); + return preferred; + } + + // 未指定 payType,回退到配置默认渠道 + return List.of(properties.getPayChannelId()); + } + + // ======================================================================== + // 2.1 创建充值接口 + // ======================================================================== + + /** + * 创建充值订单,对应API文档2.1节 + * + * @param uid 用户ID + * @param request 充值请求(商品ID、数量、支付通道) + * @return 包含二维码地址/H5支付页面的响应 + */ + public R pay(Long uid, PayRequest request) { + // 1. 查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, request.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品"); + } + + // 2. 生成订单号(雪花ID) + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + + // 3. 计算金额(一次只充一个商品,数量固定为 1) + BigDecimal totalAmount = goods.getPrice(); + String amount = MoneyUtil.toStr(totalAmount); + + // 4. MD5签名: sign=md5("pay"+recvid+orderid+amount+apikey) + String signText = "pay" + properties.getRecvId() + orderId + amount + properties.getApiKey(); + String sign = DigestUtils.md5DigestAsHex(signText.getBytes(StandardCharsets.UTF_8)); + + // 5. 按费率从低到高依次尝试渠道,失败则降级到下一渠道 + List channelPriority = selectPayChannelPriority(request.getPayType(), totalAmount); + log.info("ChengduHipay payType={}, amount={}, channelPriority={}", request.getPayType(), amount, channelPriority); + + CreatePayResult resBody = null; + String usedChannel = null; + + for (String payChannelId : channelPriority) { + log.info("ChengduHipay 尝试渠道: {}", payChannelId); + + // 6. 组装请求参数 + Map data = new HashMap<>(); + data.put("recvid", properties.getRecvId()); + data.put("orderid", orderId); + data.put("amount", amount); + data.put("sign", sign); + data.put("paychannelid", payChannelId); + data.put("memuid", DigestUtils.md5DigestAsHex((uid + "").getBytes(StandardCharsets.UTF_8))); + data.put("notifyurl", properties.getNotifyUrl()); + data.put("returnurl", properties.getReturnUrl()); + data.put("note", "充值" + amount); + + log.info("ChengduHipay createPay request channel={}: {}", payChannelId, JSONUtil.toJsonStr(data)); + + // 7. 调用Hipay创建充值接口 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> entity = new HttpEntity<>(data, headers); + + String createPayUrl = properties.getBaseUrl() + "/pay/pay/createpay"; + ResponseEntity response = restTemplate.exchange(createPayUrl, HttpMethod.POST, entity, String.class); + String result = response.getBody(); + + log.info("ChengduHipay createPay response channel={}: {}", payChannelId, result); + + // 8. 解析响应 + resBody = JSONUtil.toBean(result, CreatePayResult.class); + if (resBody.getCode() == 1) { + usedChannel = payChannelId; + break; + } + log.warn("渠道 {} 失败 code={} msg={},降级尝试下一渠道", payChannelId, resBody.getCode(), resBody.getMsg()); + } + + if (usedChannel == null || resBody == null || resBody.getCode() != 1) { + String errMsg = resBody != null ? resBody.getMsg() : "所有渠道均不可用"; + log.error("创建充值订单失败,所有渠道均已尝试,最后msg={}", errMsg); + return R.fail(errMsg); + } + + log.info("ChengduHipay 最终使用渠道: {}", usedChannel); + + // 9. 保存订单到数据库 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(resBody.getData().getOrderid()); + order.setUserId(Math.toIntExact(uid)); + order.setType(PayType.CHENGDU_HI_PAY.getCode()); + order.setGoodsId(request.getGoodsId()); + order.setGoodsPrice(goods.getPrice()); + order.setGoodsNum(1); + order.setTotalAmount(totalAmount); + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + + // 10. 返回支付链接 + PayResponse resp = new PayResponse(); + resp.setQrUrl(resBody.getData().getQrurl()); + resp.setPayUrl(resBody.getData().getPayurl()); + resp.setOrderId(orderId); + resp.setPlatformOrderId(resBody.getData().getId()); + + return R.ok(resp); + } + + // ======================================================================== + // 2.2 充值回调接口 + // ======================================================================== + + /** + * 处理Hipay平台的充值回调通知,对应API文档2.2节 + * 验签方式:resign == md5(sign + apikey) + * 成功标准:code==1 且 state==5(已转币) + */ + public R notifyCallback(Map params) { + log.info("ChengduHipay notify callback: {}", JSONUtil.toJsonStr(params)); + + // 1. 检查返回码 + String code = params.get("code"); + if (!"1".equals(code)) { + log.error("回调返回失败,code={}, msg={}", code, params.get("msg")); + return R.fail("支付失败"); + } + + // 2. 解析data字段(data为JSON字符串,需要反序列化) + String dataStr = params.get("data"); + if (ObjectUtil.isEmpty(dataStr)) { + log.error("回调data字段为空"); + return R.fail("回调数据为空"); + } + NotifyData resBody = JSONUtil.toBean(dataStr, NotifyData.class); + + // 3. 订单状态判断:必须为5(已转币)才算支付成功 + if (StateEnum.EXCHANGE.getCode() != resBody.getState()) { + log.error("交易状态非已转币,state={}", resBody.getState()); + return R.fail("交易未完成"); + } + + // 4. 验签:resign == md5(sign + apikey) + String sign = resBody.getSign(); + String resign = resBody.getResign(); + String expectedResign = DigestUtils.md5DigestAsHex( + (sign + properties.getApiKey()).getBytes(StandardCharsets.UTF_8)); + if (!expectedResign.equals(resign)) { + log.error("回调验签失败,expected={}, actual={}", expectedResign, resign); + return R.fail("签名验证失败"); + } + + // 5. 查询本地订单(按平台订单号查询,不带状态,用于取 id 做 CAS) + String outTradeNo = resBody.getOrderid(); + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOutTradeNo, outTradeNo) + .eq(TtOrder::getType, PayType.CHENGDU_HI_PAY.getCode()) + .one(); + if (order == null) { + log.error("订单不存在,outTradeNo={}", outTradeNo); + return R.fail("订单不存在"); + } + + // 6. DB条件CAS + 事务:NO_PAY->PAY_COMPLE 抢占成功者才结算,防成功回调重复推送双倍加币 + Boolean credited = new TransactionTemplate(transactionManager).execute(txStatus -> { + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(TtOrder::getId, order.getId()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()); + int rows = orderMapper.update(null, uw); + if (rows <= 0) { + return Boolean.FALSE; + } + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + if (user == null) { + throw new IllegalStateException("ChengduHipay 回调用户不存在, userId=" + order.getUserId()); + } + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (goods == null) { + throw new IllegalStateException("ChengduHipay 回调商品不存在, goodsId=" + order.getGoodsId()); + } + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + return Boolean.TRUE; + }); + + // 7. 发送充值成功通知(幂等:已处理则跳过) + if (Boolean.TRUE.equals(credited)) { + // 支付成功:清空该用户的未支付次数缓存 + payFrequencyGuard.clearOnPaid(order.getUserId().longValue()); + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice( + order.getUserId().toString(), order.getGoodsPrice()); + log.info("ChengduHipay回调处理成功,orderId={}", order.getOrderId()); + } else { + log.info("ChengduHipay回调幂等跳过(订单已处理),orderId={}", order.getOrderId()); + } + return R.ok(); + } + + // ======================================================================== + // 2.2.1 查询本地订单支付状态(供前端轮询使用) + // ======================================================================== + + /** + * 查询本地订单的支付状态,供前端在创建充值订单后轮询使用。 + * 反映回调是否已落库结算完成。 + * 使用场景:前端拿到支付二维码/H5链接后,每隔几秒轮询一次本接口, + * 状态映射: + * 0 支付中(NO_PAY) + * 4 充值成功(PAY_COMPLE) + * -1 / 2 / 3 充值失败(PAY_FAIL / CANCEL / CALL_BACK_ERRO) + * + * @param userId 当前登录用户ID + * @param orderId 本地订单号(创建充值时返回的 orderId) + */ + public R> queryOrderStatus(Long userId, String orderId) { + if (ObjectUtil.isEmpty(orderId)) { + return R.fail("订单号不能为空"); + } + + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, orderId) + .one(); + if (order == null) { + return R.fail("订单不存在"); + } + // 越权防护:仅允许查询自己的订单 + if (!Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) { + return R.fail("无权查询该订单"); + } + + String status = order.getStatus(); + + String message; + if (PayOrderStatus.PAY_COMPLE.getCode().equals(status)) { + message = "充值成功"; + } else if (PayOrderStatus.NO_PAY.getCode().equals(status)) { + message = "支付中"; + } else if (PayOrderStatus.CANCEL.getCode().equals(status)) { + message = "订单已取消"; + } else if (PayOrderStatus.CALL_BACK_ERRO.getCode().equals(status)) { + message = "回调处理异常"; + } else { + message = "支付失败"; + } + + Map data = new HashMap<>(); + data.put("status", status); + data.put("amount", order.getGoodsPrice()); + data.put("message", message); + return R.ok(data); + } + + // ======================================================================== + // 2.3 查询充值单接口 + // ======================================================================== + + /** + * 查询充值订单,对应API文档2.3节 + * GET /pay/pay/getpay?id={平台订单唯一编号} + * @param Id Hipay平台返回的订单唯一编号 + */ + public R getPay(String Id) { + String url = properties.getBaseUrl() + "/pay/pay/getpay?id=" + Id; + log.info("ChengduHipay getPay request: {}", url); + + ResponseEntity response = restTemplate.getForEntity(url, String.class); + String result = response.getBody(); + log.info("ChengduHipay getPay response: {}", result); + + CreatePayResult resBody = JSONUtil.toBean(result, CreatePayResult.class); + if (resBody.getCode() != 1) { + return R.fail(resBody.getMsg()); + } + return R.ok(resBody.getData()); + } + + // ======================================================================== + // 4 查询余额接口 + // ======================================================================== + + /** + * 查询商户余额,对应API文档4节 + * GET /pay/pay/getbalance?id={商户编号}&pass=md5(apikey) + * 返回格式: {"code":1,"data":"{\"balance\":168.8880}","msg":"success"} + */ + public R getBalance() { + String pass = DigestUtils.md5DigestAsHex( + properties.getApiKey().getBytes(StandardCharsets.UTF_8)); + String url = properties.getBaseUrl() + "/pay/pay/getbalance?id=" + + properties.getRecvId() + "&pass=" + pass; + log.info("ChengduHipay getBalance request: {}", url); + + ResponseEntity response = restTemplate.getForEntity(url, String.class); + String result = response.getBody(); + log.info("ChengduHipay getBalance response: {}", result); + + // 余额接口data为JSON字符串: {"code":1,"data":"{\"balance\":168.8880}","msg":"success"} + JSONObject json = JSONUtil.parseObj(result); + if (json.getInt("code") != 1) { + return R.fail(json.getStr("msg")); + } + // data字段是JSON字符串,需要二次解析 + String dataStr = json.getStr("data"); + if (ObjectUtil.isEmpty(dataStr)) { + return R.ok(null); + } + JSONObject dataJson = JSONUtil.parseObj(dataStr); + return R.ok(dataJson.getStr("balance")); + } + + // ======================================================================== + // 账户结算(与现有hipay逻辑保持一致) + // ======================================================================== + + /** + * 支付成功后的账户结算:加余额、记录流水、更新VIP + */ + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + + BigDecimal oldAmount = user.getAccountAmount(); + if (oldAmount == null) { + oldAmount = BigDecimal.ZERO; + } + BigDecimal newAmount = oldAmount.add(totalAmount); + + // 更新用户余额 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount); + userService.update(userUpdate); + + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + .amount(totalAmount) + .finalAmount(newAmount) + .total(totalAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + + // 充值记录 + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(order.getUserId()); + ttRechargeRecord.setParentId(user.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(newAmount); + ttRechargeRecord.setOrderId(order.getOrderId()); + ttRechargeRecord.setOutTradeNo(order.getOutTradeNo()); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("1"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + ttRechargeRecordMapper.insert(ttRechargeRecord); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/NetworkDataLoader/BaseNetworkDataLoader.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/NetworkDataLoader/BaseNetworkDataLoader.java new file mode 100644 index 0000000..5d2a305 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/NetworkDataLoader/BaseNetworkDataLoader.java @@ -0,0 +1,34 @@ +package com.ruoyi.thirdparty.common.NetworkDataLoader; + +import cn.hutool.core.util.HashUtil; +import com.ruoyi.common.core.domain.entity.SysDictData; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Arrays; +import java.util.List; +@Data +public abstract class BaseNetworkDataLoader { + + public String name; + + protected List allEnum; + + // 根据类型和标签获取code + protected String getCodeByNameOrHash(List typeList, String nameOrHash){ + + for (SysDictData type : typeList){ + if (type.getDictLabel().equals(nameOrHash)) return type.getDictValue(); + if (type.getDictType().equals(nameOrHash)) return type.getDictValue(); + } + return null; + } + + protected Long createOrnamentId(String MarketHashName){ + Long id = Long.valueOf(HashUtil.rsHash(MarketHashName)); + if (id < 0) id = Math.abs(id); + return id; + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/NetworkDataLoader/NetworkDataLoader.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/NetworkDataLoader/NetworkDataLoader.java new file mode 100644 index 0000000..d4b1489 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/NetworkDataLoader/NetworkDataLoader.java @@ -0,0 +1,12 @@ +package com.ruoyi.thirdparty.common.NetworkDataLoader; + +import com.ruoyi.domain.entity.TtOrnament; + +import java.util.List; +import java.util.Set; + +public interface NetworkDataLoader { + + Set load(); + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/NetworkDataLoader/ZBTNetworkDataLoader.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/NetworkDataLoader/ZBTNetworkDataLoader.java new file mode 100644 index 0000000..6be3fcf --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/NetworkDataLoader/ZBTNetworkDataLoader.java @@ -0,0 +1,157 @@ +package com.ruoyi.thirdparty.common.NetworkDataLoader; + +import cn.hutool.core.util.HashUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.domain.dto.zbt.OrnamentZBT; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.thirdparty.zbt.param.ProductListParams; +import com.ruoyi.thirdparty.zbt.param.SearchParams; +import com.ruoyi.thirdparty.zbt.result.ResultZbt; +import com.ruoyi.thirdparty.zbt.result.product.AvailableMarket; +import com.ruoyi.thirdparty.zbt.result.product.AvailableMarketList; +import com.ruoyi.thirdparty.zbt.result.product.SearchData; +import com.ruoyi.thirdparty.zbt.service.ZBTService; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@AllArgsConstructor +// @Data +public class ZBTNetworkDataLoader extends BaseNetworkDataLoader implements NetworkDataLoader{ + + private ZBTService zbtService; + + @Override + public Set load() { + + Set res = new HashSet<>(); + + for (SysDictData type : allEnum){ + // 根据指定的饰品类型加载数据 + if (!type.getDictType().equals("ornaments_type")) continue; + + log.info("正在加载{}类型的饰品资源~",type.getDictLabel()); + + Set data = ZBTLoadOrnamentByType(type); + + res.addAll(data); + } + + return res; + } + + private Set ZBTLoadOrnamentByType(SysDictData type){ + + HashSet result = new HashSet<>(); + + // 分批请求api + int page = 1; + boolean stopFlag = false; + while (true) { + + // 结束标记 + if (stopFlag) break; + + //1 请求平台数据 + SearchParams param = SearchParams.builder() + .page(String.valueOf(page)) + .category(type.getDictLabel()) + .limit("200") + .build(); + ResultZbt response = zbtService.search(param); + + // 解析平台数据 + SearchData data = response.getData(); + if (!response.getSuccess() || ObjectUtil.isEmpty(data)) break; + List list = JSONUtil.toList(JSONUtil.toJsonStr(data.getList()), OrnamentZBT.class); + + if (list.size() < 200) stopFlag = !stopFlag; + + // 转换为通用类型 + Set collect = list.stream().map(item -> { + + // 生成本平台id + Long id = createOrnamentId(item.getMarketHashName()); + + TtOrnament ornament = TtOrnament.builder() + .id(id) + .zbtId(item.getItemId()) + .marketHashName(item.getMarketHashName()) + .name(item.getItemName()) + .shortName(item.getShortName()) + + // 价格信息 + .price(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getPrice() : null) + .quantity(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getQuantity() : null) + + .imageUrl(item.getImageUrl()) + + .type(type.getDictValue()) + .typeName(item.getTypeName()) + .typeHashName(item.getType()) + + .quality(getCodeByNameOrHash(allEnum,item.getQuality())) + .qualityName(item.getQualityName()) + .qualityHashName(item.getQuality()) + .qualityColor(item.getQualityColor()) + + .rarity(getCodeByNameOrHash(allEnum,item.getRarity())) + .rarityName(item.getRarityName()) + .rarityHashName(item.getRarity()) + .rarityColor(item.getRarityColor()) + + .exterior(getCodeByNameOrHash(allEnum,item.getExterior())) + .exteriorHashName(item.getExterior()) + .exteriorName(item.getExteriorName()) + + .updateTime(new Date()) + .build(); + + // 补充价格信息 + AvailableMarket sellInfo = apiSellInfo(String.valueOf(ornament.getZbtId())); + if (ObjectUtil.isNotEmpty(sellInfo)) { + ornament.setUsePrice(sellInfo.getCnyPrice()); + } + + return ornament; + }).collect(Collectors.toSet()); + + result.addAll(collect); + + // log.info("加载【扎比特】{}条类型为{}的饰品数据。",page*collect.size(),typeHash); + System.out.print("\r加载【扎比特】"+page*collect.size()+"条类型为"+type.getDictLabel()+"的饰品数据。请耐心等待"); + + page++; + } + + return result; + + } + + // 查询价格信息 + private AvailableMarket apiSellInfo(String itemId){ + ProductListParams p = ProductListParams.builder() + .itemId(itemId) + .page("1") + .limit("1") + .language("zh_CN") + .build(); + ResultZbt zbtData = zbtService.productList(p); + if (!zbtData.getSuccess()) return null; + List list = zbtData.getData().getList(); + if (list.isEmpty()) return null; + return list.get(0); + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/config/DeliveryRabbitConsumer.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/config/DeliveryRabbitConsumer.java new file mode 100644 index 0000000..16717c5 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/config/DeliveryRabbitConsumer.java @@ -0,0 +1,35 @@ +package com.ruoyi.thirdparty.common.config; + +import com.rabbitmq.client.Channel; +import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig; +import com.ruoyi.thirdparty.common.service.DeliverGoodsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@Slf4j +public class DeliveryRabbitConsumer { + + private final DeliverGoodsService deliverGoodsService; + + public DeliveryRabbitConsumer(DeliverGoodsService deliverGoodsService) { + this.deliverGoodsService = deliverGoodsService; + } + + // 自动提货业务 + @RabbitListener(queues = DelayedQueueConfig.DELIVERY_QUEUE) + public void deliveryReceive(Message message, Channel channel) throws IOException { + Integer userId = Integer.valueOf(new String(message.getBody())); + channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); + try { + deliverGoodsService.autoDelivery(userId); + } catch (Exception e) { + log.error(e.getMessage()); + channel.basicRecover(false); + } + } +} \ No newline at end of file diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/config/NetworkDataLoaderConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/config/NetworkDataLoaderConfig.java new file mode 100644 index 0000000..fa8b85f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/config/NetworkDataLoaderConfig.java @@ -0,0 +1,51 @@ +package com.ruoyi.thirdparty.common.config; + +// 网络资源加载器配置 + +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.thirdparty.common.NetworkDataLoader.NetworkDataLoader; +import com.ruoyi.thirdparty.common.NetworkDataLoader.ZBTNetworkDataLoader; +import com.ruoyi.thirdparty.zbt.service.ZBTService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Configuration +public class NetworkDataLoaderConfig { + + @Autowired + private ZBTService zbtService; + + @Autowired + private SysDictDataMapper dictDataMapper; + + @Bean + public List networkDataLoaderList(){ + + // 加载必要的枚举数据 + List dictTypes = Arrays.asList( + "ornaments_type", "ornaments_type_name", + "ornaments_exterior", "ornaments_exterior_name", + "ornaments_quality", "ornaments_quality_name", + "ornaments_rarity", "ornaments_rarity_name"); + List enumList = dictDataMapper.selectDictDataByTypes(dictTypes); + + ArrayList networkDataLoaders = new ArrayList<>(); + + // zbt加载器 + ZBTNetworkDataLoader zbtNetworkDataLoader = new ZBTNetworkDataLoader(zbtService); + zbtNetworkDataLoader.setAllEnum(enumList); + zbtNetworkDataLoader.setName("扎比特加载器"); + + networkDataLoaders.add(zbtNetworkDataLoader); + + return networkDataLoaders; + + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiCommonController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiCommonController.java new file mode 100644 index 0000000..36cf6ea --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiCommonController.java @@ -0,0 +1,31 @@ +package com.ruoyi.thirdparty.common.controller; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.thirdparty.common.service.ApiCommonService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Api(tags = "第三方公共接口") +@RestController +@RequestMapping("/api/thirdparty/common") +public class ApiCommonController { + + private final ApiCommonService commonService; + + public ApiCommonController(ApiCommonService commonService) { + this.commonService = commonService; + } + + @ApiOperation("生成二维码") + @PostMapping("/skipLinkToQRCode") + public R skipLinkToQRCode(HttpServletResponse response, @RequestParam("contents") String contents) throws IOException { + return commonService.writerPayImage(response, contents); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiRechargeConfigController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiRechargeConfigController.java new file mode 100644 index 0000000..8d3f67d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiRechargeConfigController.java @@ -0,0 +1,84 @@ +package com.ruoyi.thirdparty.common.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.service.TtOrderService; +import com.ruoyi.admin.service.TtRechargeConfigService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.common.constant.PartyType; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.dto.recharge.RechargeConfigOrderVo; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeConfig; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Api(tags = "充值配置接口") +@RestController +@RequestMapping("/api/recharge/config") +public class ApiRechargeConfigController extends BaseController { + + @Autowired + private TtRechargeConfigService ttRechargeConfigService; + + @Autowired + private TtOrderService ttOrderService; + + @ApiOperation("充值结果列表") + @PostMapping("/order") + public R> order() { + startPage(); + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()); + wrapper.eq(TtOrder::getUserId, getUserId()); + wrapper.eq(TtOrder::getThirdParty, PartyType.INNER_PICTURE_PAY.getCode()); + wrapper.orderByDesc(TtOrder::getCreateTime); + List list = ttOrderService.list(wrapper); + if (CollectionUtils.isEmpty(list)) { + return R.ok(new ArrayList<>()); + } + List vos = new ArrayList<>(); + List goodsIds = new ArrayList<>(); + for (TtOrder ttOrder : list) { + RechargeConfigOrderVo vo = new RechargeConfigOrderVo(); + BeanUtils.copyProperties(ttOrder, vo); + goodsIds.add(ttOrder.getGoodsId()); + vos.add(vo); + } + LambdaQueryWrapper wrapper1 = Wrappers.lambdaQuery(); + wrapper1.in(TtRechargeConfig::getId, goodsIds); + List configs = ttRechargeConfigService.list(wrapper1); + Map configMap = configs.stream().collect(Collectors.toMap(TtRechargeConfig::getId, Function.identity())); + for (RechargeConfigOrderVo vo : vos) { + if (configMap.get(vo.getGoodsId()) != null) { + vo.setPicture(configMap.get(vo.getGoodsId()).getPicture()); + } + } + return R.ok(vos); + } + + + @ApiOperation("充值列表") + @PostMapping("/list") + public R> list() { + startPage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(TtRechargeConfig::getStatus, "0"); + List list = ttRechargeConfigService.list(wrapper); + return R.ok(list); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiRechargeController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiRechargeController.java new file mode 100644 index 0000000..480ccf4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiRechargeController.java @@ -0,0 +1,246 @@ +package com.ruoyi.thirdparty.common.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.service.TtRechargeProdService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import cn.hutool.crypto.digest.DigestUtil; +import com.ruoyi.common.annotation.RateLimiter; +import com.ruoyi.common.enums.LimitType; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.service.ChengduHipayPaymentService; +import com.ruoyi.thirdparty.common.model.RechargeCardPayRequest; +import com.ruoyi.thirdparty.richku.service.RichKuPaymentService; +import com.ruoyi.thirdparty.common.model.UnifiedPayRequest; +import com.ruoyi.thirdparty.common.service.ApiRechargeService; +import com.ruoyi.thirdparty.common.service.PayCompanyRouter; +import com.ruoyi.thirdparty.common.service.PayFrequencyGuard; +import com.ruoyi.thirdparty.abkpay.service.AbkPayPaymentService; +import com.ruoyi.thirdparty.itpay.service.ItPayPaymentService; +import com.ruoyi.thirdparty.snappay.service.SnapPayPaymentService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Api(tags = "充值") +@RestController +@RequestMapping("/api/recharge") +public class ApiRechargeController extends BaseController { + + private final ISysConfigService sysConfigService; + private final TtUserService userService; + private final ApiRechargeService apiRechargeService; + private final TtRechargeProdService rechargeListService; + private final PayCompanyRouter payCompanyRouter; + private final ChengduHipayPaymentService chengduHipayPaymentService; + private final RichKuPaymentService richKuPaymentService; + private final ItPayPaymentService itPayPaymentService; + private final AbkPayPaymentService abkPayPaymentService; + private final SnapPayPaymentService snapPayPaymentService; + private final PayFrequencyGuard payFrequencyGuard; + private final RedisLock redisLock; + + public ApiRechargeController(ISysConfigService sysConfigService, + TtUserService userService, + ApiRechargeService apiRechargeService, + TtRechargeProdService rechargeListService, + PayCompanyRouter payCompanyRouter, + ChengduHipayPaymentService chengduHipayPaymentService, + RichKuPaymentService richKuPaymentService, + ItPayPaymentService itPayPaymentService, + AbkPayPaymentService abkPayPaymentService, + SnapPayPaymentService snapPayPaymentService, + PayFrequencyGuard payFrequencyGuard, + RedisLock redisLock) { + this.redisLock = redisLock; + this.sysConfigService = sysConfigService; + this.userService = userService; + this.apiRechargeService = apiRechargeService; + this.rechargeListService = rechargeListService; + this.payCompanyRouter = payCompanyRouter; + this.chengduHipayPaymentService = chengduHipayPaymentService; + this.richKuPaymentService = richKuPaymentService; + this.itPayPaymentService = itPayPaymentService; + this.abkPayPaymentService = abkPayPaymentService; + this.snapPayPaymentService = snapPayPaymentService; + this.payFrequencyGuard = payFrequencyGuard; + } + + @ApiOperation("卡支付") + @UpdateUserCache + @RateLimiter(time = 60, count = 30, limitType = LimitType.IP) + @PostMapping("/cardPay") + public R cardPay(@RequestBody RechargeCardPayRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + // 检查卡密充值维护开关 + String cardRechargeMaintenance = sysConfigService.selectConfigByKey("cardRechargeMaintenance"); + if ("1".equals(cardRechargeMaintenance)) { + return R.fail("卡密充值维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + // 是否实名认证(0未认证 1已认证) + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired(); + } + // 控制层防重锁:按卡密内容(password)串行,锁包裹整个 @Transactional 事务,解锁晚于事务提交, + // 避免弱网重复提交导致同一卡密重复充值(锁粒度=单卡密,不影响其他卡密并发) + String lockKey = RedisConstants.CARD_PAY_LOCK + DigestUtil.sha256Hex(request.getPassword()); + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + return R.fail(false, "充值进行中,请勿重复操作"); + } + try { + String msg = apiRechargeService.cardPay(request.getPassword(), ttUser); + if (StringUtils.isEmpty(msg)) { + return R.ok(true, "充値成功!"); + } else { + return R.fail(false, msg); + } + } finally { + redisLock.unlock(lockKey); + } + } + + @ApiOperation("充值列表") + // @UpdateUserCache + @Anonymous + @PostMapping("/list") + public R>> list() { + startPage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(TtRechargeProd::getStatus, "0"); + List list = rechargeListService.list(wrapper); + List> result = list.stream().map(prod -> { + Map map = new HashMap<>(); + map.put("id", prod.getId()); + map.put("name", prod.getName()); + map.put("price", prod.getPrice()); + map.put("status", prod.getStatus()); + map.put("cardLink", prod.getCardLink()); + map.put("picture", prod.getPicture()); + map.put("transactionLink", prod.getTransactionLink()); + // productA/productC 统一转字符串,拼接展示标签:有赠送时显示 "500+50 赠送",否则显示金币数 + BigDecimal productA = prod.getProductA() != null ? prod.getProductA() : BigDecimal.ZERO; + BigDecimal productC = prod.getProductC() != null ? prod.getProductC() : BigDecimal.ZERO; + String productAStr = productA.stripTrailingZeros().toPlainString(); + String productCStr = productC.stripTrailingZeros().toPlainString(); + map.put("productA", productAStr); + map.put("productC", productCStr); + if (productC.compareTo(BigDecimal.ZERO) > 0) { + map.put("label", productAStr + "+" + productCStr + " 赠送"); + } else { + map.put("label", productAStr); + } + return map; + }).collect(Collectors.toList()); + return R.ok(result); + } + + @ApiOperation("统一充值下单(按权重分配支付公司)") + @UserPermission + @PostMapping("/unifiedPay") + public R unifiedPay(@RequestBody UnifiedPayRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + // 检查充值渠道维护开关 + String rechargeMaintenance = sysConfigService.selectConfigByKey("rechargeMaintenance"); + if ("1".equals(rechargeMaintenance)) { + return R.fail("充值渠道维护中......"); + } + Long userId = getUserId(); + TtUser ttUser = userService.getById(userId); + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired("未实名认证"); + } + + // 频控:封禁期内直接拒绝(渠道无关,按 uid 统一生效) + if (payFrequencyGuard.isBanned(userId)) { + return R.fail(705,"频繁拉单未支付,请五分钟后重试"); + } + + // 路由逻辑:金额<300 且 itpay 已启用(权重>0) 默认走 itpay;>=300 按权重选择(如 abkpay,其内部再按金额选产品编码:300~800用8012,>=800用8003) + // 说明:把某渠道权重设为 0 即视为停用,不再强制路由到它;itpay 需保持权重>0,否则 <300 规则失效回落到权重选择 + String company; + TtRechargeProd goods = rechargeListService.getById(request.getGoodsId()); + if (goods == null) { + return R.fail("充值商品不存在"); + } + if (snapPayPaymentService.isRoutable(goods.getPrice())) { + // 金额位于 SnapPay 限额区间(默认 1000~50000)且已启用:强制走 SnapPay; + // 若 SnapPay 停用(sys_config snappayEnabled=0 或未配置 merchantId),则回落到下方规则 + company = "snappay"; + } else if (goods.getPrice().compareTo(new BigDecimal("300")) < 0 + && payCompanyRouter.isEnabled("itpay")) { + company = "itpay"; + } else { + company = payCompanyRouter.select(request.getPayType()); + } + + // 转换为对应公司的请求格式 + PayRequest payReq = new PayRequest(); + payReq.setGoodsId(request.getGoodsId()); + payReq.setPayType(request.getPayType()); + + R result = dispatchPay(company, userId, payReq); + + // SnapPay 走不了(下单失败/通道异常)时,自动按权重回落到其他渠道(如 abkpay)再试一次; + // 注:snappay 不在权重池内,回落不会再选中自己,不会死循环 + if ("snappay".equals(company) && !R.isSuccess(result)) { + String fallback = payCompanyRouter.select(request.getPayType()); + log.warn("SnapPay 下单失败,按权重回落到 {}, 原因: {}", fallback, result.getMsg()); + company = fallback; + result = dispatchPay(fallback, userId, payReq); + } + + // 下单成功后累计"拉单未支付"次数(支付成功回调会清零) + if (R.isSuccess(result)) { + payFrequencyGuard.recordUnpaidPull(userId); + return result; + } + // 统一下单失败错误码 706(涵盖所有支付渠道,具体失败原因见各渠道日志) + log.error("统一充值下单失败, company={}, msg={}", company, result.getMsg()); + return R.fail(706, "创建订单失败"); + } + + /** + * 按支付公司名称分发下单请求 + */ + private R dispatchPay(String company, Long userId, PayRequest payReq) { + return switch (company) { + case "chengduhipay" -> chengduHipayPaymentService.pay(userId, payReq); + case "richku" -> richKuPaymentService.pay(userId, payReq, null); + case "itpay" -> itPayPaymentService.pay(userId, payReq); + case "abkpay" -> abkPayPaymentService.pay(userId, payReq); + case "snappay" -> snapPayPaymentService.pay(userId, payReq); + default -> { + log.error("未知支付公司: {}", company); + yield R.fail("支付通道暂不可用"); + } + }; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiSmsController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiSmsController.java new file mode 100644 index 0000000..b35a6bd --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/ApiSmsController.java @@ -0,0 +1,93 @@ +package com.ruoyi.thirdparty.common.controller; + +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.common.model.ApiSmsBody; +import com.ruoyi.thirdparty.common.model.ApiSmsNoSliderBody; +import com.ruoyi.thirdparty.common.service.ApiSmsService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Api(tags = "短信") +@RestController +@RequestMapping("/api/sms") +public class ApiSmsController extends BaseController { + + private final ISysConfigService sysConfigService; + private final ApiSmsService apiSmsService; + + @Autowired + private RedisCache redisCache; + + public ApiSmsController(ISysConfigService sysConfigService, + ApiSmsService apiSmsService) { + this.sysConfigService = sysConfigService; + this.apiSmsService = apiSmsService; + } + + @ApiOperation("获取短信验证码") + @Anonymous + @PostMapping("/getVerifyCode") + public R getVerifyCode(@RequestBody ApiSmsBody smsBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + + String phoneNumber = smsBody.getPhoneNumber(); + String captchaId = smsBody.getCaptchaId(); + + // 校验滑块验证码 + if (StringUtils.isEmpty(captchaId)) { + return R.fail("请先完成滑块验证"); + } + String verifiedKey = "captcha:verified:" + phoneNumber; + String storedCaptchaId = redisCache.getCacheObject(verifiedKey); + if (StringUtils.isEmpty(storedCaptchaId) || !storedCaptchaId.equals(captchaId)) { + return R.fail("滑块验证已过期或无效,请重新验证"); + } + + String msg = apiSmsService.getVerifyCode(smsBody); + if (StringUtils.isNotEmpty(msg)) { + // 短信发送失败(次数/余额不足),返回特定错误码 + if (msg.contains("次数") || msg.contains("余额") || msg.contains("不足")) { + return R.fail(999, "短信发送次数不足"); + } + return R.fail(msg); + } + return R.ok("获取验证码成功,请注意短信通知!"); + } + + @ApiOperation("获取短信验证码(免滑块)") + @Anonymous + @PostMapping("/getVerifyCodeNoSlider") + public R getVerifyCodeNoSlider(@RequestBody ApiSmsNoSliderBody body) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + + // 不校验滑块,直接发送验证码;适配 service 层入参不含 captchaId + ApiSmsBody smsBody = new ApiSmsBody(); + smsBody.setPhoneNumber(body.getPhoneNumber()); + smsBody.setType(body.getType()); + String msg = apiSmsService.getVerifyCode(smsBody); + if (StringUtils.isNotEmpty(msg)) { + if (msg.contains("次数") || msg.contains("余额") || msg.contains("不足")) { + return R.fail(999, "短信发送次数不足"); + } + return R.fail(msg); + } + return R.ok("获取验证码成功,请注意短信通知!"); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/DeliverGoodsController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/DeliverGoodsController.java new file mode 100644 index 0000000..0b6d90e --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/controller/DeliverGoodsController.java @@ -0,0 +1,82 @@ +package com.ruoyi.thirdparty.common.controller; + +import com.ruoyi.domain.dto.deliver.TradeBuyParam; +import com.ruoyi.domain.dto.deliver.TradeManualConfirmParam; +import com.ruoyi.domain.vo.AvailableMarketOrnamentVO; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.thirdparty.common.service.DeliverGoodsService; +import com.ruoyi.thirdparty.zbt.param.ProductListParams; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Api(tags = "管理端发货管理") +@RestController +@RequestMapping("/admin/deliverGoods") +public class DeliverGoodsController extends BaseController { + + private final DeliverGoodsService deliverGoodsService; + + public DeliverGoodsController(DeliverGoodsService deliverGoodsService) { + this.deliverGoodsService = deliverGoodsService; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class GetAvailableMarketListParam{ + // private List hashNameList; + private Integer partyType; + private List ornamentsId; + } + + // 获取饰品在各个平台的在售信息 + @ApiOperation("获取饰品在各个平台的在售信息") + @PostMapping("/getAvailableMarketList") + public R getAvailableMarketList(@RequestBody GetAvailableMarketListParam param) { + return deliverGoodsService.getAvailableMarketList(param); + } + + // 根据饰品hashName获取所有平台的在售列表(弃用接口) + @ApiOperation("根据饰品hashName获取所有平台的在售列表(弃用接口)") + @GetMapping("/getAvailableMarketListByHashName/{marketHashName}") + public R> getAvailableMarketListByHashName(@PathVariable("marketHashName") String marketHashName, + ProductListParams productListParams) { + List list = deliverGoodsService.getAvailableMarketListByHashName(marketHashName, productListParams); + if (StringUtils.isNull(list)) return R.fail("获取饰品所有在售列表异常!"); + return R.ok(list); + } + + @ApiOperation("购买发货") + @PostMapping("/tradeBuy") + public R tradeBuy(@RequestBody TradeBuyParam param) { + return deliverGoodsService.tradeBuy(param); + } + + @ApiOperation("手动确认发货") + @PostMapping("/tradeConfirm") + public R tradeConfirm(@RequestBody TradeManualConfirmParam param) { + return deliverGoodsService.tradeConfirm(param); + } + + @ApiOperation("同步状态") + @GetMapping("/synchronousStatus") + public R synchronousStatus(String outTradeNo) { + String msg = deliverGoodsService.synchronousStatus(outTradeNo); + return StringUtils.isEmpty(msg) ? R.ok(true, "同步成功!") : R.fail(false, msg); + } + + @ApiOperation("一键同步状态") + @GetMapping("/syncAllStatus") + public R syncAllStatus() { + int row = deliverGoodsService.syncAllStatus(); + return row > 0 ? R.ok() : R.fail("一键同步失败"); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/ApiSmsBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/ApiSmsBody.java new file mode 100644 index 0000000..ea1b1c5 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/ApiSmsBody.java @@ -0,0 +1,15 @@ +package com.ruoyi.thirdparty.common.model; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class ApiSmsBody { + private String phoneNumber; + @ApiModelProperty("1注册 2登录 3更换手机号 4找回密码 5实名认证") + private String type; + @ApiModelProperty("滑块验证码ID") + private String captchaId; + @ApiModelProperty("上级邀请码(推广后台登录:手机号未注册时用它校验是否可自动注册为主播)") + private String parentInvitationCode; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/ApiSmsNoSliderBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/ApiSmsNoSliderBody.java new file mode 100644 index 0000000..383d779 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/ApiSmsNoSliderBody.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.common.model; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 免滑块短信验证码请求体(不含 captchaId) + */ +@Data +public class ApiSmsNoSliderBody { + @ApiModelProperty("手机号") + private String phoneNumber; + + @ApiModelProperty("1注册 2登录 3更换手机号 4找回密码 5实名认证") + private String type; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/RechargeCardPayRequest.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/RechargeCardPayRequest.java new file mode 100644 index 0000000..e2781d6 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/RechargeCardPayRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.thirdparty.common.model; + +import lombok.Data; + +@Data +public class RechargeCardPayRequest { + + private String password; // 卡密 +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/UnifiedPayRequest.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/UnifiedPayRequest.java new file mode 100644 index 0000000..e1bf036 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/model/UnifiedPayRequest.java @@ -0,0 +1,20 @@ +package com.ruoyi.thirdparty.common.model; + +import io.swagger.annotations.ApiModelProperty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 统一充值请求,由后端按权重分配支付公司 + */ +@Data +public class UnifiedPayRequest { + @NotNull(message = "商品id不能为空") + private Integer goodsId; + + @ApiModelProperty("支付方式:1=支付宝,2=银联") + private Integer payType; + + @ApiModelProperty("1:原有支付,2:图片支付") + private Integer payWay = 1; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/scheduled/ThirdPartyTask.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/scheduled/ThirdPartyTask.java new file mode 100644 index 0000000..d7615f7 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/scheduled/ThirdPartyTask.java @@ -0,0 +1,73 @@ +package com.ruoyi.thirdparty.common.scheduled; + +import cn.hutool.core.util.ObjectUtil; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.thirdparty.common.NetworkDataLoader.BaseNetworkDataLoader; +import com.ruoyi.thirdparty.common.NetworkDataLoader.NetworkDataLoader; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Slf4j +@Configuration // 1.主要用于标记配置类,兼备Component的效果。 +@EnableScheduling // 2.开启定时任务 +public class ThirdPartyTask { + + @Autowired + private TtOrnamentService ttOrnamentService; + + @Value("${mkcsgo.startLoadOrnaments}") + private Boolean startLoadOrnaments; + + // 资源加载器集合 + @Autowired + private List networkDataLoaderList; + + // 定时加载发货平台的物品数据 + //@Scheduled(cron = "0 50 23 * * ?") + // @Scheduled(cron = "0 6 17 * * ?") + public void networkDataLoad(){ + + log.info("是否加载网络ornaments资源{}", startLoadOrnaments); + if (!startLoadOrnaments) return; + + // 1 加载各个平台数据 + Set ttOrnaments = new HashSet<>(); + for (NetworkDataLoader loader : networkDataLoaderList) { + log.info("【{}】开始加载网络资源~",((BaseNetworkDataLoader)loader).name); + Set load = loader.load(); + ttOrnaments.addAll(load); + } + if (ObjectUtil.isEmpty(ttOrnaments) || ttOrnaments.isEmpty()) { + log.warn("加载饰品网络资源失败。"); + return; + } + + // 2 写db + log.info("db allData ing~~~"); + for (TtOrnament item : ttOrnaments) { + try { + ttOrnamentService.saveOrUpdate(item); + } catch (DuplicateKeyException e) { + // log.info("过滤重复数据 【id】{}【hash】{}save。",item.getId(),item.getMarketHashName()); + } catch (Exception e) { + e.printStackTrace(); + log.info("保存数据异常。"); + break; + } + } + + log.info("加载饰品网络资源成功。"); + + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/ApiCommonService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/ApiCommonService.java new file mode 100644 index 0000000..816005f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/ApiCommonService.java @@ -0,0 +1,11 @@ +package com.ruoyi.thirdparty.common.service; + +import com.ruoyi.common.core.domain.R; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public interface ApiCommonService { + + R writerPayImage(HttpServletResponse response, String contents) throws IOException; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/ApiRechargeService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/ApiRechargeService.java new file mode 100644 index 0000000..e1dae4c --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/ApiRechargeService.java @@ -0,0 +1,8 @@ +package com.ruoyi.thirdparty.common.service; + +import com.ruoyi.domain.entity.sys.TtUser; + +public interface ApiRechargeService { + + String cardPay(String password, TtUser ttUser); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/ApiSmsService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/ApiSmsService.java new file mode 100644 index 0000000..13e9591 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/ApiSmsService.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.common.service; + +import com.ruoyi.thirdparty.common.model.ApiSmsBody; + +public interface ApiSmsService { + + String getVerifyCode(ApiSmsBody smsBody); + + String validateCaptcha(String code, String verifyKey); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/DeliverGoodsService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/DeliverGoodsService.java new file mode 100644 index 0000000..4bca1ef --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/DeliverGoodsService.java @@ -0,0 +1,30 @@ +package com.ruoyi.thirdparty.common.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.deliver.TradeBuyParam; +import com.ruoyi.domain.dto.deliver.TradeManualConfirmParam; +import com.ruoyi.domain.vo.AvailableMarketOrnamentVO; +import com.ruoyi.thirdparty.common.controller.DeliverGoodsController.GetAvailableMarketListParam; +import com.ruoyi.thirdparty.zbt.param.ProductListParams; + +import java.util.List; + +public interface DeliverGoodsService { + + R getAvailableMarketList(Integer ornamentsId, Integer partyType); + + R tradeBuy(TradeBuyParam param); + + String synchronousStatus(String outTradeNo); + + int syncAllStatus(); + + void autoDelivery(Integer userId); + + List getAvailableMarketListByHashName(String marketHashName, ProductListParams productListParams); + + R getAvailableMarketList(GetAvailableMarketListParam param); + + R tradeConfirm(TradeManualConfirmParam param); + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/PayCompanyRouter.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/PayCompanyRouter.java new file mode 100644 index 0000000..5317de4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/PayCompanyRouter.java @@ -0,0 +1,107 @@ +package com.ruoyi.thirdparty.common.service; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONUtil; +import com.ruoyi.domain.other.PayCompanyWeight; +import com.ruoyi.system.service.ISysConfigService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 支付公司权重路由器,根据配置的权重比例随机选择支付公司。 + * 配置格式: [{"name":"chengduhipay","weight":7},{"name":"hipay","weight":3}] + * 表示10次支付中,约7次到chengduhipay,3次到hipay。 + */ +@Slf4j +@Component +public class PayCompanyRouter { + + private final ISysConfigService sysConfigService; + + public PayCompanyRouter(ISysConfigService sysConfigService) { + this.sysConfigService = sysConfigService; + } + + /** + * 按权重随机选择一个支付公司(不区分渠道) + */ + public String select() { + return select(null); + } + + /** + * 按权重随机选择一个支持指定渠道的支付公司 + * @param payType 支付渠道:1=支付宝,2=银联。为 null 则不限制 + */ + public String select(Integer payType) { + String configJson = sysConfigService.selectConfigByKey("payCompanyWeights"); + if (StrUtil.isBlank(configJson)) { + log.warn("payCompanyWeights 配置为空,回退到 chengduhipay"); + return "chengduhipay"; + } + + try { + JSONArray jsonArray = JSONUtil.parseArray(configJson); + List list = jsonArray.toList(PayCompanyWeight.class); + + // 过滤有效权重 + 渠道匹配(payType为空则不过滤渠道) + List validList = list.stream() + .filter(w -> w.getWeight() > 0 && StrUtil.isNotBlank(w.getName())) + .filter(w -> w.getPayType() == null || payType == null || w.getPayType().equals(payType)) + .toList(); + + if (validList.isEmpty()) { + log.warn("payCompanyWeights 无匹配 payType={} 的有效权重,回退到 chengduhipay", payType); + return "chengduhipay"; + } + + int totalWeight = validList.stream().mapToInt(PayCompanyWeight::getWeight).sum(); + if (totalWeight <= 0) { + log.warn("payCompanyWeights 总权重为0,回退到 chengduhipay"); + return "chengduhipay"; + } + + int random = ThreadLocalRandom.current().nextInt(totalWeight); + int cumulative = 0; + for (PayCompanyWeight w : validList) { + cumulative += w.getWeight(); + if (random < cumulative) { + log.info("加权随机选中支付公司: {}, payType={}, 权重={}/{}, random={}", + w.getName(), w.getPayType(), w.getWeight(), totalWeight, random); + return w.getName(); + } + } + + return validList.get(0).getName(); + + } catch (Exception e) { + log.error("解析 payCompanyWeights 失败,回退到 chengduhipay: {}", e.getMessage()); + return "chengduhipay"; + } + } + + /** + * 判断指定支付公司当前是否启用(在 payCompanyWeights 中且 weight>0)。 + * 用于强制路由前的开关校验:权重设为 0 即视为停用。 + */ + public boolean isEnabled(String name) { + if (StrUtil.isBlank(name)) { + return false; + } + String configJson = sysConfigService.selectConfigByKey("payCompanyWeights"); + if (StrUtil.isBlank(configJson)) { + return false; + } + try { + List list = JSONUtil.parseArray(configJson).toList(PayCompanyWeight.class); + return list.stream().anyMatch(w -> name.equals(w.getName()) && w.getWeight() > 0); + } catch (Exception e) { + log.error("解析 payCompanyWeights 失败: {}", e.getMessage()); + return false; + } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/PayFrequencyGuard.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/PayFrequencyGuard.java new file mode 100644 index 0000000..eb74efb --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/PayFrequencyGuard.java @@ -0,0 +1,82 @@ +package com.ruoyi.thirdparty.common.service; + +import com.ruoyi.admin.config.RedisConstants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 支付拉单频控守卫(渠道无关,按 uid 统一生效)。 + * 规则:x 分钟内 y 次拉单未支付则封禁 z 分钟。当前 x=5, y=3, z=10。 + * 两个缓存: + * 次数缓存 PAY_PULL_UNPAID_COUNT+uid:TTL=x 分钟,值=累计未支付拉单次数; + * 封禁缓存 PAY_PULL_BAN+uid:值=z、TTL=z 分钟。 + */ +@Slf4j +@Component +public class PayFrequencyGuard { + + /** 次数缓存有效期 x(分钟) */ + private static final long PULL_WINDOW_MINUTES = 5; + /** 触发封禁的未支付拉单次数阈值 y */ + private static final long PULL_MAX_UNPAID = 3; + /** 封禁时长 z(分钟),同时作为封禁缓存的值 */ + private static final long BAN_MINUTES = 10; + + private static final DefaultRedisScript RECORD_UNPAID_SCRIPT; + static { + RECORD_UNPAID_SCRIPT = new DefaultRedisScript<>(); + RECORD_UNPAID_SCRIPT.setScriptText( + "local count = redis.call('INCR', KEYS[1]) " + + "if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end " + + "if count >= tonumber(ARGV[2]) then " + + " redis.call('SET', KEYS[2], ARGV[3], 'EX', ARGV[4]) " + + " redis.call('DEL', KEYS[1]) " + + " return 1 " + + "end " + + "return 0" + ); + RECORD_UNPAID_SCRIPT.setResultType(Long.class); + } + + private final StringRedisTemplate stringRedisTemplate; + + public PayFrequencyGuard(StringRedisTemplate stringRedisTemplate) { + this.stringRedisTemplate = stringRedisTemplate; + } + + /** 是否处于封禁期 */ + public boolean isBanned(Long uid) { + return Boolean.TRUE.equals(stringRedisTemplate.hasKey(RedisConstants.PAY_PULL_BAN + uid)); + } + + /** + * 记录一次"拉单未支付":次数缓存自增,达到阈值则写入封禁缓存并清空次数缓存。 + * 次数缓存 TTL = x 分钟;封禁缓存 值=TTL=z 分钟。 + */ + public void recordUnpaidPull(Long uid) { + String countKey = RedisConstants.PAY_PULL_UNPAID_COUNT + uid; + String banKey = RedisConstants.PAY_PULL_BAN + uid; + Long result = stringRedisTemplate.execute( + RECORD_UNPAID_SCRIPT, + List.of(countKey, banKey), + String.valueOf(PULL_WINDOW_MINUTES * 60), + String.valueOf(PULL_MAX_UNPAID), + String.valueOf(BAN_MINUTES), + String.valueOf(BAN_MINUTES * 60) + ); + if (result != null && result == 1L) { + log.warn("支付频控触发封禁:uid={}, {}分钟内未支付拉单达{}次, 封禁{}分钟", + uid, PULL_WINDOW_MINUTES, PULL_MAX_UNPAID, BAN_MINUTES); + } + } + + /** 支付成功后清空该用户的未支付次数缓存(支付了就不计入未支付) */ + public void clearOnPaid(Long uid) { + stringRedisTemplate.delete(RedisConstants.PAY_PULL_UNPAID_COUNT + uid); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/RechargeSuccessfulNoticeService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/RechargeSuccessfulNoticeService.java new file mode 100644 index 0000000..576883b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/RechargeSuccessfulNoticeService.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.common.service; + +import java.math.BigDecimal; + +/** + * 充值成功通知 + */ +public interface RechargeSuccessfulNoticeService { + + /** + * 发送充值成功通知 + */ + void sendRechargeSuccessNotice(String userId, BigDecimal amount); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/ApiCommonServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/ApiCommonServiceImpl.java new file mode 100644 index 0000000..04f7f4f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/ApiCommonServiceImpl.java @@ -0,0 +1,42 @@ +package com.ruoyi.thirdparty.common.service.impl; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.thirdparty.common.service.ApiCommonService; +import org.springframework.stereotype.Service; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Service +public class ApiCommonServiceImpl implements ApiCommonService { + + @Override + public R writerPayImage(HttpServletResponse response, String contents) throws IOException { + ServletOutputStream out = response.getOutputStream(); + try { + Map hints = new HashMap<>(); + hints.put(EncodeHintType.CHARACTER_SET,"UTF-8"); + hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); + hints.put(EncodeHintType.MARGIN, 0); + BitMatrix bitMatrix = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE,300,300,hints); + MatrixToImageWriter.writeToStream(bitMatrix,"jpg",out); + return R.ok("success"); + }catch (Exception e){ + return R.fail("数据异常"); + }finally { + if(out != null){ + out.flush(); + out.close(); + } + } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/ApiRechargeServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/ApiRechargeServiceImpl.java new file mode 100644 index 0000000..a61e912 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/ApiRechargeServiceImpl.java @@ -0,0 +1,373 @@ +package com.ruoyi.thirdparty.common.service.impl; + +import cn.hutool.core.util.IdUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtBlankCardKeyMapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtPromoCardKeyMapper; +import com.ruoyi.admin.mapper.TtRechargeCardMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtBlankCardKey; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtPromoCardKey; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeCard; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.thirdparty.common.service.ApiRechargeService; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; + +@Service +@Slf4j +public class ApiRechargeServiceImpl implements ApiRechargeService { + + private final TtBonusService bonusService; + private final TtUserService userService; + private final TtRechargeCardMapper rechargeCardMapper; + private final TtRechargeRecordMapper rechargeRecordMapper; + + public ApiRechargeServiceImpl(TtBonusService bonusService, + TtUserService userService, + TtRechargeCardMapper rechargeCardMapper, + TtRechargeRecordMapper rechargeRecordMapper) { + this.bonusService = bonusService; + this.userService = userService; + this.rechargeCardMapper = rechargeCardMapper; + this.rechargeRecordMapper = rechargeRecordMapper; + } + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtPromoCardKeyMapper promoCardKeyMapper; + + @Autowired + private TtBlankCardKeyMapper blankCardKeyMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public String cardPay(String password, TtUser ttUser) { + // 并发防护由控制层 CARD_PAY_LOCK 负责(按卡密内容加锁、包裹整个事务、解锁晚于提交); + // 此处仅保留数据库乐观锁(WHERE status=0 + 影响行数校验)作为并发双用兜底 + long startTime = System.currentTimeMillis(); + try { + TtRechargeCard ttRechargeCard = new LambdaQueryChainWrapper<>(rechargeCardMapper).eq(TtRechargeCard::getPassword, password) + .eq(TtRechargeCard::getStatus, "0").eq(TtRechargeCard::getDelFlag, "0").one(); + + // 如果后台卡密表找不到,依次查找空白卡密表、卡商推广平台卡密表 + if (StringUtils.isNull(ttRechargeCard)) { + String blankResult = cardPayByBlankCardKey(password, ttUser); + if (blankResult != null) return blankResult; + return cardPayByPromoCardKey(password, ttUser); + } + + // 原子占用:仅当 status=0 时置为 status=1,防止并发双用 + LambdaUpdateWrapper cardUpdateWrapper = new LambdaUpdateWrapper<>(); + cardUpdateWrapper.eq(TtRechargeCard::getId, ttRechargeCard.getId()) + .eq(TtRechargeCard::getStatus, "0") + .set(TtRechargeCard::getStatus, "1") + .set(TtRechargeCard::getUseUserId, ttUser.getUserId()) + .set(TtRechargeCard::getUseTime, DateUtils.getNowDate()) + .set(TtRechargeCard::getUpdateBy, ttUser.getUserName()) + .set(TtRechargeCard::getUpdateTime, DateUtils.getNowDate()); + if (rechargeCardMapper.update(null, cardUpdateWrapper) > 0) { + + BigDecimal totalAmount = ttRechargeCard.getPrice(); + boolean isBlankCard = Integer.valueOf(1).equals(ttRechargeCard.getIsBlank()); + boolean isBlankAccount = Integer.valueOf(1).equals(ttUser.getIsBlankAccount()); + + // 空白卡密仅允许白号使用 + if (isBlankCard && !isBlankAccount) { + // 回滚卡密状态 + ttRechargeCard.setStatus("0"); + ttRechargeCard.setUseUserId(null); + ttRechargeCard.setUseTime(null); + rechargeCardMapper.updateById(ttRechargeCard); + return "该卡密仅限白号使用!"; + } + + // 白号只能使用空白卡密:命中普通卡密一律拒绝并回滚卡密状态 + if (isBlankAccount && !isBlankCard) { + ttRechargeCard.setStatus("0"); + ttRechargeCard.setUseUserId(null); + ttRechargeCard.setUseTime(null); + rechargeCardMapper.updateById(ttRechargeCard); + return "白号仅限使用空白卡密!"; + } + + // 1. 同步增加用户余额(不动积分),与支付宝/微信通道保持一致 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate.eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount.toString()); + userService.update(userUpdate); + + // 2. 同步写入充值流水(updateUserVIPLevel 依赖 source=RECHARGE 流水累计金额,必须同步落库) + TtUser refreshUser = userService.getById(ttUser.getUserId()); + Timestamp now = new Timestamp(System.currentTimeMillis()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(refreshUser.getUserId()) + .amount(totalAmount) + .finalAmount(refreshUser.getAccountAmount()) + .total(totalAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + .createTime(now) + .updateTime(now) + .build(); + userBlendErcashMapper.insert(blendErcash); + + // 3. 更新VIP等级,必须在充值流水同步落库后调用 + userService.updateUserVIPLevel(ttUser.getUserId()); + + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(ttUser.getUserId()); + ttRechargeRecord.setParentId(ttUser.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(refreshUser.getAccountAmount()); + ttRechargeRecord.setOrderId(password); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("0"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + // 空白卡密标记:充值记录列表中会被过滤不显示 + if (isBlankCard) { + ttRechargeRecord.setIsBlankCard(1); + } + // 实时计算返佣(白号跳过返佣,不触发上级佣金) + if (rechargeRecordMapper.insert(ttRechargeRecord) > 0 && !isBlankAccount) { + bonusService.bonus(ttUser.getUserId().intValue(), ttRechargeRecord.getId()); + } + + // 设置商户订单号 + String orderId = String.valueOf(IdUtil.getSnowflake(1, 1).nextId()); + // 创建订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(password); + order.setUserId(ttUser.getUserId()); + order.setType(PayType.MK_CARD.getCode()); + order.setGoodsId(ttRechargeCard.getId()); + order.setGoodsPrice(ttRechargeCard.getPrice()); + order.setGoodsNum(1); + order.setTotalAmount(totalAmount); + order.setStatus(PayOrderStatus.PAY_COMPLE.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + + // 发送充值成功通知 + // rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(ttUser.getUserId().toString(), totalAmount); + + return ""; + } else { + log.warn("卡密充值竞态拦截: password={}, userId={}", password, ttUser.getUserId()); + return "该卡密已被使用!"; + } + } catch (Exception e) { + // 显式标记回滚:卡密占用与加余额/写流水/建订单必须原子,避免出现"卡密已消耗但钱未到账" + log.error("卡密充值异常回滚: password={}, userId={}", password, ttUser.getUserId(), e); + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return "数据异常!"; + } finally { + // P1#9: 统一响应时间,缓解三表查询时序侧信道 + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed < 50) { + try { Thread.sleep(50 - elapsed); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } + } + } + } + + /** + * 空白卡密兑换(tt_blank_card_key 表) + * 直接到账:status=1,仅加余额,不写流水/不更新VIP/不建订单 + * @return null 表示未找到该卡密,继续查找其他表;非null 表示已处理 + */ + private String cardPayByBlankCardKey(String password, TtUser ttUser) { + TtBlankCardKey blankCard = new LambdaQueryChainWrapper<>(blankCardKeyMapper) + .eq(TtBlankCardKey::getCardCode, password) + .eq(TtBlankCardKey::getStatus, 0) + .one(); + if (blankCard == null) return null; // 未找到,继续查找其他表 + + // 空白卡密仅限白号使用:普通用户/主播/招商命中此卡密一律拒绝,且不消耗卡密(保留status=0,方便白号后续仍能正常使用) + if (!Integer.valueOf(1).equals(ttUser.getIsBlankAccount())) { + return "该卡密仅限白号使用!"; + } + + // 原子占用:仅当 status=0 时置为 status=1,防止并发双用 + LambdaUpdateWrapper blankUpdateWrapper = new LambdaUpdateWrapper<>(); + blankUpdateWrapper.eq(TtBlankCardKey::getId, blankCard.getId()) + .eq(TtBlankCardKey::getStatus, 0) + .set(TtBlankCardKey::getStatus, 1) + .set(TtBlankCardKey::getUsedById, ttUser.getUserId().longValue()) + .set(TtBlankCardKey::getUsedByName, ttUser.getUserName()) + .set(TtBlankCardKey::getUsedTime, DateUtils.getNowDate()); + if (blankCardKeyMapper.update(null, blankUpdateWrapper) <= 0) { + log.warn("空白卡密竞态拦截: cardCode={}, userId={}", password, ttUser.getUserId()); + return "该卡密已被使用!"; + } + + BigDecimal totalAmount = blankCard.getAmount(); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate.eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount.toString()); + userService.update(userUpdate); + + // 写入余额流水与充值记录,供前端 cocos「余额记录/充值记录」展示; + // 后台通过 is_blank_account(余额流水)与 is_blank_card(充值记录)过滤,不体现白号数据。 + // 白号不产生上级佣金,不触发返佣、不写订单、不更新VIP。 + TtUser refreshUser = userService.getById(ttUser.getUserId()); + Timestamp now = new Timestamp(System.currentTimeMillis()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(refreshUser.getUserId()) + .amount(totalAmount) + .finalAmount(refreshUser.getAccountAmount()) + .total(totalAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + .createTime(now) + .updateTime(now) + .build(); + userBlendErcashMapper.insert(blendErcash); + + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(ttUser.getUserId()); + ttRechargeRecord.setParentId(ttUser.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(refreshUser.getAccountAmount()); + ttRechargeRecord.setOrderId(password); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("0"); + // 空白卡密标记:后台充值记录列表按 is_blank_card 过滤不显示 + ttRechargeRecord.setIsBlankCard(1); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + rechargeRecordMapper.insert(ttRechargeRecord); + + return ""; + } + + /** + * 卡商推广平台卡密兑换(promo_card_key 表) + * 玩家使用卡密直接到账:status=1(已使用),加余额+写流水+更新VIP + */ + private String cardPayByPromoCardKey(String password, TtUser ttUser) { + TtPromoCardKey promoCard = new LambdaQueryChainWrapper<>(promoCardKeyMapper) + .eq(TtPromoCardKey::getCardCode, password) + .eq(TtPromoCardKey::getStatus, 0) + .one(); + if (promoCard == null) return "卡密不存在或已被使用!"; + + // 白号只能使用空白卡密:卡商卡密一律拒绝,不消费该卡密 + if (Integer.valueOf(1).equals(ttUser.getIsBlankAccount())) { + return "白号仅限使用空白卡密!"; + } + + BigDecimal totalAmount = promoCard.getAmount(); + + // 1. 原子占用:仅当仍未使用(status=0)时置为已使用(status=1),与卡商销毁(status=0→2)在 status=0 上互斥竞争, + // 有且只有一方成功,杜绝"玩家刚好使用、卡商刚好销毁"的并发双花(旧 updateById 无 status 守卫会盲覆盖销毁态) + LambdaUpdateWrapper useWrapper = new LambdaUpdateWrapper<>(); + useWrapper.eq(TtPromoCardKey::getId, promoCard.getId()) + .eq(TtPromoCardKey::getStatus, 0) + .set(TtPromoCardKey::getStatus, 1) + .set(TtPromoCardKey::getUsedById, ttUser.getUserId().longValue()) + .set(TtPromoCardKey::getUsedByName, ttUser.getUserName()) + .set(TtPromoCardKey::getUsedTime, DateUtils.getNowDate()); + if (promoCardKeyMapper.update(null, useWrapper) <= 0) { + // 期间被销毁或被其他请求先用掉,未加余额,安全退出 + return "卡密不存在或已被使用!"; + } + + // 2. 增加用户余额 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate.eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount.toPlainString()); + userService.update(userUpdate); + + // 3. 写入充值流水 + TtUser refreshUser = userService.getById(ttUser.getUserId()); + Timestamp now = new Timestamp(System.currentTimeMillis()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(refreshUser.getUserId()) + .amount(totalAmount) + .finalAmount(refreshUser.getAccountAmount()) + .total(totalAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark("卡密充值: " + promoCard.getCardCode()) + .createTime(now) + .updateTime(now) + .build(); + userBlendErcashMapper.insert(blendErcash); + + // 4. 更新VIP等级 + userService.updateUserVIPLevel(ttUser.getUserId()); + + // 5. 写入充值记录(与普通卡密/第三方支付通道保持一致,确保"充值记录"列表可见) + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(ttUser.getUserId()); + ttRechargeRecord.setParentId(ttUser.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(refreshUser.getAccountAmount()); + ttRechargeRecord.setOrderId(password); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("0"); + // 记录来源卡商ID(promo_card_key.creatorId 为卡商 userId) + if (promoCard.getCreatorId() != null) { + ttRechargeRecord.setCardMerchantId(promoCard.getCreatorId().intValue()); + } + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + // 实时计算返佣,触发上级佣金 + if (rechargeRecordMapper.insert(ttRechargeRecord) > 0) { + bonusService.bonus(ttUser.getUserId().intValue(), ttRechargeRecord.getId()); + } + + // 6. 写入支付订单,口径与后台卡密 cardPay 主分支完全一致(type=卡密、status=支付完成); + // 商品ID 固定 0:卡商卡密不在 tt_recharge_config 里,避免误命中同ID充值商品导致取错商品图 + TtOrder order = TtOrder.builder() + .orderId(String.valueOf(IdUtil.getSnowflake(1, 1).nextId())) + .outTradeNo(password) + .userId(ttUser.getUserId()) + .type(PayType.MK_CARD.getCode()) + .goodsId(0) + .goodsPrice(totalAmount) + .goodsNum(1) + .totalAmount(totalAmount) + .status(PayOrderStatus.PAY_COMPLE.getCode()) + .createTime(new Date()) + .build(); + orderMapper.insert(order); + + return ""; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/ApiSmsServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/ApiSmsServiceImpl.java new file mode 100644 index 0000000..b42d970 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/ApiSmsServiceImpl.java @@ -0,0 +1,151 @@ +package com.ruoyi.thirdparty.common.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.RandomUtil; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.thirdparty.aliyunsms.service.AliyunSmsService; +import com.ruoyi.thirdparty.common.model.ApiSmsBody; +import com.ruoyi.thirdparty.common.service.ApiSmsService; +import com.ruoyi.thirdparty.foursms.service.FourSmsService; +import com.ruoyi.thirdparty.note.service.YunXinNoteService; +import com.ruoyi.thirdparty.smsbao.service.SmsbaoService; +import com.ruoyi.thirdparty.winic.service.WinicService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +public class ApiSmsServiceImpl implements ApiSmsService { + + private final RedisCache redisCache; + private final TtUserService userService; + private final YunXinNoteService yunXinNoteService; + + public ApiSmsServiceImpl(RedisCache redisCache, + TtUserService userService, + YunXinNoteService yunXinNoteService) { + this.redisCache = redisCache; + this.userService = userService; + this.yunXinNoteService = yunXinNoteService; + } + + @Autowired + private SmsbaoService smsbaoService; + + @Autowired + private WinicService winicService; + + @Autowired + private AliyunSmsService aliyunSmsService; + + @Autowired + private FourSmsService fourSmsService; + + @Value("${ruoyi.name}") + private String systemName; + + /** 短信发送频率限制:10 分钟内最多 3 次 */ + private static final int SMS_RATE_LIMIT_MAX_COUNT = 3; + private static final int SMS_RATE_LIMIT_MINUTES = 10; + + @Override + public String getVerifyCode(ApiSmsBody smsBody) { + String phoneNumber = smsBody.getPhoneNumber(); + String type = smsBody.getType(); + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY; + if (StringUtils.isEmpty(type)) { + return "类型不能为空"; + } + if (StringUtils.isEmpty(phoneNumber)) { + return "手机号不能为空"; + } + if (!Validator.isMobile(phoneNumber)) { + return "手机号格式错误,请检查手机号是否输入正确!"; + } + // 频率限制:同一手机号 10 分钟内最多发送 3 次(不限类型,防止跨类型刷短信) + String rateLimitKey = "sms:rate_limit:" + phoneNumber; + Integer sendCount = redisCache.getCacheObject(rateLimitKey); + if (sendCount != null && sendCount >= SMS_RATE_LIMIT_MAX_COUNT) { + return "操作过于频繁,请" + SMS_RATE_LIMIT_MINUTES + "分钟后再试"; + } + if ("1".equals(type) && !userService.checkPhoneUnique(TtUser.builder().phoneNumber(phoneNumber).build())) { + return "手机号'" + phoneNumber + "'已被注册!"; + } + // type=2(登录)不再校验手机号是否已注册,因为 verificationCodeLoginOrRegister 接口支持未注册自动注册 + if ("3".equals(type) || "4".equals(type)) { + TtUser ttUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()).eq(TtUser::getPhoneNumber, phoneNumber).one(); + if (StringUtils.isNull(ttUser)) return "该手机号未在本站注册!"; + } + if ("1".equals(type)) { + verifyKey = verifyKey + "ApiRegister_" + phoneNumber; + } else if ("2".equals(type)) { + verifyKey = verifyKey + "ApiLogin_" + phoneNumber; + } else if ("3".equals(type)) { + verifyKey = verifyKey + "UpdatePhoneNumber_" + phoneNumber; + } else if ("4".equals(type)) { + verifyKey = verifyKey + "ApiForgetPassword_" + phoneNumber; + } else if ("5".equals(type)) { + verifyKey = verifyKey + "ApiRealNameAuth_" + phoneNumber; + } + String cacheCode = redisCache.getCacheObject(verifyKey); + if (StringUtils.isNotEmpty(cacheCode)) { + return "验证码已发送,请注意手机短信信息!"; + } + String randomCode = RandomUtil.randomNumbers(4); + // 网易云信 + // String code = yunXinNoteService.sendNote(phoneNumber, randomCode); + // if (StringUtils.isEmpty(code)) { + // return "发送短信验证码时出现异常!"; + // } + //短信宝(备用渠道) +// String content = "【长沙赤焰网络】您的验证码为" + randomCode + ",该验证码1分钟内有效,请勿泄露于他人。"; +// String code = smsbaoService.sendSms(phoneNumber, content); +// if (!"0".equals(code)) { +// return "发送短信验证码时出现异常!"; +// } + // 切换为 4SMS(模板化发送,params.code 为验证码;返回 "0" 表示成功) +// String code = fourSmsService.sendVerifyCode(phoneNumber, randomCode); +// if (!"0".equals(code)) { +// return "发送短信验证码时出现异常!"; +// } + // 阿里云短信(已停用,改为短信宝) + String code = aliyunSmsService.sendVerifyCode(phoneNumber, randomCode); + if (!"0".equals(code)) { + return "发送短信验证码时出现异常!"; + } + // 切换为中正云通信 + // String content = "【OG】您的验证码为" + randomCode + ",该验证码5分钟内有效,请勿泄露于他人。"; + // String code = winicService.sendSms(phoneNumber, content); + // if (!"000".equals(code)) { + // return "发送短信验证码时出现异常!"; + // } + // 更新发送频率计数(保留剩余 TTL,避免重置窗口) + if (sendCount == null) { + redisCache.setCacheObject(rateLimitKey, 1, SMS_RATE_LIMIT_MINUTES, TimeUnit.MINUTES); + } else { + long remainTtl = redisCache.getExpire(rateLimitKey); + redisCache.setCacheObject(rateLimitKey, sendCount + 1, + remainTtl > 0 ? (int) remainTtl : SMS_RATE_LIMIT_MINUTES * 60, TimeUnit.SECONDS); + } + redisCache.setCacheObject(verifyKey, randomCode, 5, TimeUnit.MINUTES); + return ""; + } + + @Override + public String validateCaptcha(String code, String verifyKey) { + verifyKey = CacheConstants.CAPTCHA_CODE_KEY + verifyKey; + String captcha = redisCache.getCacheObject(verifyKey); + if (!code.equalsIgnoreCase(captcha)) return "验证码错误"; + redisCache.deleteObject(verifyKey); + return "success"; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/DeliverGoodsServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/DeliverGoodsServiceImpl.java new file mode 100644 index 0000000..f55d7a3 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/DeliverGoodsServiceImpl.java @@ -0,0 +1,713 @@ +package com.ruoyi.thirdparty.common.service.impl; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.admin.service.TtOrnamentYYService; +import com.ruoyi.admin.service.TtYyOrnamentsService; +import com.ruoyi.common.constant.ThirdParty.Platform; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.DeliveryOrderStatus; +import com.ruoyi.domain.common.constant.PartyType; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.dto.deliver.TradeBuyParam; +import com.ruoyi.domain.dto.deliver.TradeManualConfirmParam; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtYYOrnaments; +import com.ruoyi.domain.vo.AvailableMarketOrnamentVO; +import com.ruoyi.domain.vo.OnSaleOrnamentVO; +import com.ruoyi.domain.vo.queryTemplateSaleByCategoryDataVO; +import com.ruoyi.domain.vo.queryTemplateSaleByCategoryVO; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.common.controller.DeliverGoodsController.GetAvailableMarketListParam; +import com.ruoyi.thirdparty.common.service.DeliverGoodsService; +import com.ruoyi.thirdparty.cs340.domain.body.ByTemplateCreateOrder; +import com.ruoyi.thirdparty.cs340.domain.result.ByTemplateCreateOrderResult; +import com.ruoyi.thirdparty.cs340.domain.result.Cs340OrderStatusResult; +import com.ruoyi.thirdparty.cs340.domain.result.Cs340Result; +import com.ruoyi.thirdparty.cs340.service.Cs340Service; +import com.ruoyi.thirdparty.v5item.domain.body.CreateOrderBody; +import com.ruoyi.thirdparty.v5item.domain.result.CreateOrderResult; +import com.ruoyi.thirdparty.v5item.domain.result.QueryOrderStatusResult; +import com.ruoyi.thirdparty.v5item.domain.result.V5ItemResult; +import com.ruoyi.thirdparty.v5item.service.V5ItemService; +import com.ruoyi.thirdparty.yyyouping.service.yyyoupingService; +import com.ruoyi.thirdparty.yyyouping.utils.YYClient; +import com.ruoyi.thirdparty.yyyouping.utils.common.YYResult; +import com.ruoyi.thirdparty.zbt.param.*; +import com.ruoyi.thirdparty.zbt.result.ResultZbt; +import com.ruoyi.thirdparty.zbt.result.buy.OpenBuyResultDTO; +import com.ruoyi.thirdparty.zbt.result.order.OrderBuyDetailDTO; +import com.ruoyi.thirdparty.zbt.result.product.AvailableMarket; +import com.ruoyi.thirdparty.zbt.result.product.AvailableMarketList; +import com.ruoyi.thirdparty.zbt.service.ZBTService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.DeliveryOrderStatus.DELIVERY_BEFORE; +import static com.ruoyi.domain.common.constant.DeliveryOrderStatus.ORDER_CANCEL; + +@Service +@Slf4j +public class DeliverGoodsServiceImpl implements DeliverGoodsService { + + private final ISysConfigService configService; + private final ZBTService zbtService; + private final TtUserMapper userMapper; + private final TtOrnamentMapper ornamentsMapper; + private final TtBoxRecordsMapper boxRecordsMapper; + private final TtDeliveryRecordMapper deliveryRecordMapper; + + //@Autowired + private YYClient yyClient; + + @Autowired + private TtYyOrnamentsService ttYyOrnamentsService; + + @Autowired + private TtOrnamentService ttOrnamentService; + + @Autowired + private TtOrnamentYYService ttOrnamentYYService; + + @Autowired + private TtOrnamentsYYMapper ttOrnamentsYYMapper; + + @Autowired + private yyyoupingService yyyoupingService; + + @Autowired + private V5ItemService v5ItemService; + + @Autowired + private Cs340Service cs340Service; + + public DeliverGoodsServiceImpl(ISysConfigService configService, + ZBTService zbtService, + TtUserMapper userMapper, + TtOrnamentMapper ornamentsMapper, + TtBoxRecordsMapper boxRecordsMapper, + TtDeliveryRecordMapper deliveryRecordMapper) { + this.configService = configService; + this.zbtService = zbtService; + this.userMapper = userMapper; + this.ornamentsMapper = ornamentsMapper; + this.boxRecordsMapper = boxRecordsMapper; + this.deliveryRecordMapper = deliveryRecordMapper; + } + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Override + public R getAvailableMarketList(Integer ornamentsId,Integer partyType) { + + // 从平台获取的数据集 + List resList = null; + if (partyType.equals(PartyType.ZBT.getCode())){ + + // 获取饰品信息 + TtOrnament ornament = new LambdaQueryChainWrapper<>(ornamentsMapper) + .eq(TtOrnament::getId, ornamentsId) + .one(); + // 构建查询参数 + ProductListParams apiParm = ProductListParams.builder() + .itemId(String.valueOf(ornament.getId())) + .build(); + + // 查询接口 + ResultZbt productList = zbtService.productList(apiParm); + if (!productList.getSuccess()) { + log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!"); + return null; + } + + // 过滤可用数据 + AvailableMarketList availableMarketList = productList.getData(); + + resList = availableMarketList.getList(); + + }else if (partyType.equals(PartyType.YY_YOU_PING.getCode())){ + + // 获取饰品信息 + TtOrnament ornament = new LambdaQueryChainWrapper<>(ornamentsMapper) + .eq(TtOrnament::getId, ornamentsId) + .one(); + // 构建查询参数 + ProductListParams apiParm = ProductListParams.builder() + .itemId(String.valueOf(ornament.getId())) + .build(); + + // 查询接口 + ResultZbt productList = zbtService.productList(apiParm); + if (!productList.getSuccess()) { + log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!"); + return null; + } + + // 过滤可用数据 + AvailableMarketList availableMarketList = productList.getData(); + + resList = availableMarketList.getList(); + } + + if (ObjectUtil.isEmpty(resList)) return R.fail("平台没有该物品在售信息。"); + + // 构建返回结果 + List resultList = new ArrayList<>(); + + // 提取可用数据 + for (AvailableMarket availableMarket : resList) { + OnSaleOrnamentVO data = OnSaleOrnamentVO.builder().build(); + data.setId(availableMarket.getId()); + data.setDelivery(availableMarket.getDelivery()); + data.setItemName(availableMarket.getItemName()); + data.setImageUrl(availableMarket.getImageUrl()); + data.setCnyPrice(availableMarket.getCnyPrice()); + data.setPrice(availableMarket.getPrice()); + resultList.add(data); + } + return R.ok(resultList); + } + + @Override + public R tradeBuy(TradeBuyParam param) { + // 获取提货订单数据 + TtDeliveryRecord ttDeliveryRecord = deliveryRecordMapper.selectById(param.getDeliveryRecordId()); + + // 获取用户数据 + TtUser ttUser = userMapper.selectById(ttDeliveryRecord.getUserId()); + // 初始化数据 + String transactionLink = ttUser.getTransactionLink(); + if (StringUtils.isEmpty(transactionLink)) { + return R.fail("该用户未绑定Steam交易链接!"); + } + String outTradeNo = ttDeliveryRecord.getOutTradeNo(); + + if (param.getPartyType().equals(PartyType.ZBT.getCode())){ + + // 构建购买参数 + NormalBuyParamV2DTO normalBuyParamV2DTO = new NormalBuyParamV2DTO(); + normalBuyParamV2DTO.setOutTradeNo(outTradeNo); + normalBuyParamV2DTO.setProductId(param.getProductId().toString()); + normalBuyParamV2DTO.setTradeUrl(transactionLink); + + // 购买 + ResultZbt tradeBuy = zbtService.tradeBuy(normalBuyParamV2DTO); + if (!tradeBuy.getSuccess()) { + return R.ok(tradeBuy.getErrorMsg()); + } + + // 获取结果数据 + OpenBuyResultDTO data = tradeBuy.getData(); + System.err.println("结果数据:" + data); + // 更新提货订单数据 + ttDeliveryRecord.setBuyPrice(data.getBuyPrice()); + ttDeliveryRecord.setThirdpartyDelivery(data.getDelivery()); + ttDeliveryRecord.setOrderId(data.getOrderId()); + ttDeliveryRecord.setStatus(DELIVERY_BEFORE.getCode()); // 发货订单状态 1待发货 + ttDeliveryRecord.setMessage(DELIVERY_BEFORE.getMsg()); + ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) { + return R.ok(); + } + return R.fail("更新提货订单数据异常!"); + + }else if (param.getPartyType().equals(PartyType.V5ITEM.getCode())){ + LambdaQueryWrapper ttOrnamentsQuery = new LambdaQueryWrapper<>(); + ttOrnamentsQuery.eq(TtOrnament::getId, param.getProductId()); + TtOrnament ttOrnament = ttOrnamentService.getOne(ttOrnamentsQuery); + + CreateOrderBody createOrderBody = new CreateOrderBody(); + createOrderBody.setMarketHashName(ttOrnament.getMarketHashName()); + BigDecimal purchasePrice = param.getPrice(); //.multiply(new BigDecimal("2")); + createOrderBody.setPurchasePrice(purchasePrice); + createOrderBody.setTradeUrl(transactionLink); + createOrderBody.setMerchantOrderNo(outTradeNo); + + V5ItemResult createOrderResult = v5ItemService.createOrder(createOrderBody); + + if (createOrderResult.getCode() != 0) { + return R.fail(createOrderResult.getMsg()); + } + // 更新提货订单数据 + ttDeliveryRecord.setBuyPrice(createOrderResult.getData().getPayAmount()); + ttDeliveryRecord.setThirdpartyDelivery(1); + ttDeliveryRecord.setOrderId(createOrderResult.getData().getOrderNo()); + ttDeliveryRecord.setStatus(DELIVERY_BEFORE.getCode()); // 发货订单状态 1待发货 + ttDeliveryRecord.setMessage(DELIVERY_BEFORE.getMsg()); + ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) { + return R.ok(); + } + return R.ok("正在开发"); + } else if (Objects.equals(param.getPartyType(), PartyType.CS340.getCode())) { + LambdaQueryWrapper ttOrnamentsQuery = new LambdaQueryWrapper<>(); + ttOrnamentsQuery.eq(TtOrnament::getId, param.getProductId()); + TtOrnament ttOrnament = ttOrnamentService.getOne(ttOrnamentsQuery); + BigDecimal purchasePrice = param.getPrice();// .multiply(new BigDecimal("2")); + ByTemplateCreateOrder createOrderBody = new ByTemplateCreateOrder(); + createOrderBody.setPurchasePrice(purchasePrice.toString()); + createOrderBody.setTradeLinks(transactionLink); + createOrderBody.setMerchantOrderNo(outTradeNo); + createOrderBody.setCommodityHashName(ttOrnament.getMarketHashName()); + + Cs340Result createOrderResult = cs340Service.createOrder(createOrderBody); + + if (createOrderResult.getCode() != 0) { + return R.fail(createOrderResult.getMsg()); + } + // 更新提货订单数据 + ttDeliveryRecord.setBuyPrice(createOrderResult.getData().getPayAmount()); + ttDeliveryRecord.setThirdpartyDelivery(1); + ttDeliveryRecord.setOrderId(createOrderResult.getData().getOrderNo()); + ttDeliveryRecord.setStatus(DELIVERY_BEFORE.getCode()); // 发货订单状态 1待发货 + ttDeliveryRecord.setMessage(DELIVERY_BEFORE.getMsg()); + ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) { + return R.ok(); + } + return R.ok("正在开发"); + } else if (param.getPartyType().equals(PartyType.YY_YOU_PING.getCode())){ + return R.ok("暂未开放。"); + } else { + return R.fail("非法的第三方平台代码"); + } + + } + + @Override + @Transactional + public String synchronousStatus(String outTradeNo) { + // 获取提货数据 + TtDeliveryRecord ttDeliveryRecord = new LambdaQueryChainWrapper<>(deliveryRecordMapper) + .eq(TtDeliveryRecord::getOutTradeNo, outTradeNo).one(); + if (ttDeliveryRecord == null) { + return "未找到对应的提货记录"; + } + if (DeliveryOrderStatus.ORDER_COMPLETE.getCode().equals(ttDeliveryRecord.getStatus()) + || DeliveryOrderStatus.ORDER_CANCEL.getCode().equals(ttDeliveryRecord.getStatus())) { + return ""; + } + // 查询 CS340 订单状态 + Cs340Result orderStatusResult = cs340Service.queryOrderStatus(ttDeliveryRecord.getOrderId()); + if (orderStatusResult == null || orderStatusResult.getData() == null || orderStatusResult.getCode() != 0) { + return "查询购买订单详情失败"; + } + // CS340 大状态:2失败 / 100创建中 / 120支付中 / 130异常挂起 / 140交货中 / 280已取消 / 340已完成 / 360结算中 + Integer status = orderStatusResult.getData().getBigStatus(); + + ttDeliveryRecord.setMessage(orderStatusResult.getData().getStatusMsg()); + ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + + if (Objects.equals(status, 2) || Objects.equals(status, 280)) { + // 失败 / 已取消 → 本地映射为「订单取消」。 + // 业务语义:取消后饰品需同时出现在「提货背包」和「我的库存」两个 tab,供用户再次发起提货。 + // 实现方式:保持 box_records.status=2 不动,仅将 delivery_record.status 改为 11。 + // - 「我的库存」SQL: status IN (0,2) + NOT EXISTS (delivery.status!=11) → 命中(box=2, delivery=11 通过 NOT EXISTS) + // - 「提货背包」SQL: status = 2 + NOT EXISTS (delivery.status!=11) → 命中(box=2, delivery=11 通过 NOT EXISTS) + // 与 [TtDeliveryRecordServiceImpl.deliveryFail](后台「退回申请」)口径保持一致。 + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_CANCEL.getCode()); + } else if (Objects.equals(status, 340)) { + // 已完成 → 本地置为「订单完成」,并将 box 标为已提货 + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_COMPLETE.getCode()); + // 联动:订单完成后将对应 box_records 状态标为「已经提货」,不再展示在背包 / 提货背包,不可分解 / 转赠 + markBoxAsDelivered(ttDeliveryRecord.getBoxRecordsId()); + } else { + // 其他大状态(创建中 / 支付中 / 交货中 / 结算中 / 异常挂起)暂记为「已发货」,等待下次同步 + ttDeliveryRecord.setStatus(DeliveryOrderStatus.DELIVERY_AFTER.getCode()); + } + + if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) return ""; + return "同步提货订单数据异常!"; + } + + /** + * 将提货记录对应的 box_records 状态从 APPLY_DELIVERY(2) 走到 DELIVERY_YET(3)。 + * 仅在当前状态确为「申请提货」时才写入,避免覆盖其他状态(如已被 admin 删除 / 已分解)。 + */ + private void markBoxAsDelivered(Long boxRecordsId) { + if (boxRecordsId == null) { + return; + } + TtBoxRecords box = boxRecordsMapper.selectById(boxRecordsId); + if (box != null && TtboxRecordStatus.APPLY_DELIVERY.getCode().equals(box.getStatus())) { + box.setStatus(TtboxRecordStatus.DELIVERY_YET.getCode()); + box.setUpdateTime(DateUtils.getNowDate()); + boxRecordsMapper.updateById(box); + } + } + + /** + * 保留V5历史同步订单状态的代码逻辑,后面再对接V5的话及时打开 + */ + private String synchronousStatusForV5(String outTradeNo) { + // 获取提货数据 + TtDeliveryRecord ttDeliveryRecord = new LambdaQueryChainWrapper<>(deliveryRecordMapper) + .eq(TtDeliveryRecord::getOutTradeNo, outTradeNo).one(); + if (10 == ttDeliveryRecord.getStatus() || 11 == ttDeliveryRecord.getStatus()) { + return ""; + } + // 构建查询参数 + OrderBuyDetailParams orderBuyDetailParams = new OrderBuyDetailParams(); + orderBuyDetailParams.setOutTradeNo(ttDeliveryRecord.getOutTradeNo()); + QueryOrderStatusResult orderStatusResult = v5ItemService.queryOrderStatus(ttDeliveryRecord.getOutTradeNo(), ttDeliveryRecord.getOrderId()); + if (orderStatusResult == null) { + return "查询购买订单详情V2失败"; + } + // 获取结果数据 + Integer status = orderStatusResult.getStatus(); + + if (Objects.equals(status, 4)){ + ttDeliveryRecord.setStatus(status); + ttDeliveryRecord.setMessage(orderStatusResult.getStatusMsg()); + ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + // 将用户提货饰品返回用户背包 + TtBoxRecords ttBoxRecords = new LambdaQueryChainWrapper<>(boxRecordsMapper) + .eq(TtBoxRecords::getId, ttDeliveryRecord.getBoxRecordsId()) + .eq(TtBoxRecords::getHolderUserId, ttDeliveryRecord.getUserId()) + .eq(TtBoxRecords::getOrnamentId, ttDeliveryRecord.getOrnamentId()) + .eq(TtBoxRecords::getStatus, "2") // 状态 2已提取 + .one(); + if (StringUtils.isNull(ttBoxRecords)) { + return "未查询到该用户背包的饰品信息!"; + } + ttBoxRecords.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); // 状态 0背包 + ttBoxRecords.setUpdateTime(DateUtils.getNowDate()); + boxRecordsMapper.updateById(ttBoxRecords); + if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) { + return ""; + } + } else { + ttDeliveryRecord.setMessage(orderStatusResult.getStatusMsg()); + // 发送发货成功通知 + Map noticeMap = new HashMap<>(); + noticeMap.put("userId", ttDeliveryRecord.getUserId().toString()); + noticeMap.put("title", "发货成功通知"); + noticeMap.put("content", "发货成功,请注意查收。"); + noticeMap.put("createTime", DateUtils.getTime()); + rabbitTemplate.convertAndSend("notice_queue", noticeMap); + } + + ttDeliveryRecord.setStatus(DeliveryOrderStatus.DELIVERY_AFTER.getCode()); + ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) return ""; + return "同步提货订单数据异常!"; + } + + @Override + public int syncAllStatus() { + List records = new LambdaQueryChainWrapper<>(deliveryRecordMapper) + .eq(TtDeliveryRecord::getStatus, 1) + .list(); + for (TtDeliveryRecord record : records) { + String msg = synchronousStatus(record.getOutTradeNo()); + } + return records.size(); + } + + @Override + public void autoDelivery(Integer userId) { + // 获取需要自动发货的数据 + List deliveryRecordList = new LambdaQueryChainWrapper<>(deliveryRecordMapper) + .eq(TtDeliveryRecord::getUserId, userId) + .eq(TtDeliveryRecord::getDelivery, "2") // 网站发货模式(1人工发货 2自动发货 3主播号提取) + .eq(TtDeliveryRecord::getStatus, "0") // 发货订单状态(0发起提货 1待发货 3待收货 10订单完成 11订单取消) + .list(); + if (StringUtils.isNull(deliveryRecordList) || deliveryRecordList.isEmpty()) { + log.info("{}号用户发起提货,无自动发货饰品,请在后台手动发货!", userId); + return; + } + + // 饰品购买价格溢价率 + String buyPricePremiumRateStr = configService.selectConfigByKey("buyPricePremiumRate"); + BigDecimal buyPricePremiumRate = new BigDecimal(buyPricePremiumRateStr); + // ZBT平台币种汇率 + String ZBTParitiesStr = configService.selectConfigByKey("ZBTParities"); + BigDecimal ZBTParities = new BigDecimal(ZBTParitiesStr); + + // 快速购买 + for (TtDeliveryRecord ttDeliveryRecord : deliveryRecordList) { + // 获取用户信息 + TtUser ttUser = userMapper.selectById(ttDeliveryRecord.getUserId()); + // 获取饰品信息 + TtOrnament ttOrnament = ornamentsMapper.selectById(ttDeliveryRecord.getOrnamentId()); + // 计算可接受最高价 + BigDecimal ornamentsPrice = ttDeliveryRecord.getOrnamentsPrice(); // 饰品价格 + BigDecimal price = ornamentsPrice.divide(ZBTParities, 2, RoundingMode.HALF_UP).multiply(buyPricePremiumRate); + // 构建购买参数 + QuickBuyParamV2DTO quickBuyParamV2DTO = new QuickBuyParamV2DTO(); + // TODO: 2024/3/30 ttOrnament.getId() + quickBuyParamV2DTO.setItemId(String.valueOf(ttOrnament.getId())); + quickBuyParamV2DTO.setMaxPrice(price.toString()); // 购买a可以接受的最高价格(T币) + quickBuyParamV2DTO.setOutTradeNo(ttDeliveryRecord.getOutTradeNo()); + quickBuyParamV2DTO.setTradeUrl(ttUser.getTransactionLink()); + ResultZbt tradeQuickBuy = zbtService.tradeQuickBuy(quickBuyParamV2DTO); + if (!tradeQuickBuy.getSuccess()) { + log.error("快速购买接口v2异常,订单号:{}", ttDeliveryRecord.getOutTradeNo()); + continue; + } + // 获取结果数据 + OpenBuyResultDTO data = tradeQuickBuy.getData(); + // 更新提货订单数据 + ttDeliveryRecord.setBuyPrice(data.getBuyPrice()); + ttDeliveryRecord.setThirdpartyDelivery(data.getDelivery()); + ttDeliveryRecord.setOrderId(data.getOrderId()); + ttDeliveryRecord.setStatus(DELIVERY_BEFORE.getCode()); // 发货订单状态 1待发货 + ttDeliveryRecord.setMessage("等待发货"); + ttDeliveryRecord.setUpdateBy("自动发货"); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + // 更新状态 + if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) { + log.info("订单'{}'自动发货成功,等待卖家发货中......", ttDeliveryRecord.getOutTradeNo()); + } + } + } + + public List ZBTGetAvailable(String marketHashName, ProductListParams productListParams){ + + LambdaQueryWrapper ttOrnamentsQuery = new LambdaQueryWrapper<>(); + ttOrnamentsQuery.eq(TtOrnament::getMarketHashName, marketHashName); + TtOrnament ornament = ttOrnamentService.getOne(ttOrnamentsQuery); + + // zbt所有数据 + productListParams.setItemId(String.valueOf(ornament.getId())); + ResultZbt availableZbt = zbtService.productList(productListParams); + + if (!availableZbt.getSuccess()){ + log.warn(availableZbt.getErrorMsg()); + return null; + } + + AvailableMarketList data = availableZbt.getData(); + return data.getList().stream().map(item->{ + return AvailableMarketOrnamentVO.builder() + .platform(Platform.ZBT.getCode()) + .itemId(item.getItemId()) + .delivery(item.getDelivery()) + .itemName(item.getItemName()) + .imageUrl(item.getImageUrl()) + .cnyPrice(item.getCnyPrice()) + .price(item.getPrice()) + .build(); + }).collect(Collectors.toList()); + + } + + public List YYGetAvailable(String marketHashName, ProductListParams productListParams){ + + LambdaQueryWrapper ttYYOrnamentsQuery = new LambdaQueryWrapper<>(); + ttYYOrnamentsQuery.eq(TtYYOrnaments::getHashName,marketHashName); + TtYYOrnaments yyOrnament = ttYyOrnamentsService.getOne(ttYYOrnamentsQuery); + + HashMap map = new HashMap<>(); + JSONObject o = new JSONObject(); + o.put("templateId",yyOrnament.getId()); + log.info("obj="+o.toString()); + map.put("requestList", ListUtil.of(o)); + YYResult yyResult = yyyoupingService.yyApiBatchGetOnSaleCommodityInfo(map); + + Object dataJson = yyResult.getData(); + ObjectMapper objectMapper = new ObjectMapper(); + queryTemplateSaleByCategoryDataVO dataVO; + try { + dataVO = objectMapper.readValue(JSON.toJSONString(dataJson), queryTemplateSaleByCategoryDataVO.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + List voList = dataVO.getSaleTemplateByCategoryResponseList(); + + List collect = voList.stream().map(item -> { + return AvailableMarketOrnamentVO.builder() + .platform(Platform.YY.getCode()) + .templateId(item.getTemplateId()) + .imageUrl(item.getIconUrl()) + .build(); + }).collect(Collectors.toList()); + + return null; + } + + @Override + public List getAvailableMarketListByHashName(String marketHashName, ProductListParams productListParams) { + + // zbt所有数据 + List ZBTGetAvailable = ZBTGetAvailable(marketHashName, productListParams); + + // yy所有数据 + List YYGetAvailable = YYGetAvailable(marketHashName, productListParams); + + // 合并所有数据 + + return null; + + // // 获取饰品itemId + // TtOrnaments ornaments = new LambdaQueryChainWrapper<>(ornamentsMapper).eq(TtOrnaments::getId, ornamentsId).one(); + // // 构建查询参数 + // productListParams.setItemId(ornaments.getItemId()); + // ResultZbt productList = zbtService.productList(productListParams); // 查询接口 + // if (!productList.getSuccess()) { + // log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!"); + // return null; + // } + // AvailableMarketList availableMarketList = productList.getData(); + // // 查询到的数据 + // List list = availableMarketList.getList(); + // // 构建返回结果 + // List resultList = new ArrayList<>(); + // // 提取可用数据 + // for (AvailableMarket availableMarket : list) { + // AvailableMarketOrnamentsListDataVO data = AvailableMarketOrnamentsListDataVO.builder().build(); + // data.setId(availableMarket.getId()); + // data.setDelivery(availableMarket.getDelivery()); + // data.setItemName(availableMarket.getItemName()); + // data.setImageUrl(availableMarket.getImageUrl()); + // data.setCnyPrice(availableMarket.getCnyPrice()); + // data.setPrice(availableMarket.getPrice()); + // resultList.add(data); + // } + // return resultList; + } + + @Override + public R getAvailableMarketList(GetAvailableMarketListParam param) { + + // 返回结果 + List resultList = new ArrayList<>(); + + // 获取饰品信息(这里暂时不支持批量查询) + List list = new LambdaQueryChainWrapper<>(ornamentsMapper) + .in(TtOrnament::getId, param.getOrnamentsId().get(0)) + .list(); + + if (param.getPartyType().equals(PartyType.ZBT.getCode())){ + + for (TtOrnament item: list){ + + if (ObjectUtil.isEmpty(item.getZbtId())) continue; + + // 构建查询参数 + ProductListParams apiParm = ProductListParams.builder() + .itemId(String.valueOf(item.getZbtId())) + .build(); + + // 查询接口 + ResultZbt productList = zbtService.productList(apiParm); + if (!productList.getSuccess()) { + log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!"); + return R.fail("调用ZBT获取某一个饰品的所有在售接口异常"); + } + + // 过滤可用数据 + AvailableMarketList availableMarketList = productList.getData(); + + List list1 = availableMarketList.getList(); + + if (ObjectUtil.isEmpty(list1)) { + return R.fail("平台没有该物品在售信息。"); + } + + // 提取可用数据 + for (AvailableMarket availableMarket : list1) { + OnSaleOrnamentVO data = OnSaleOrnamentVO.builder().build(); + data.setPartyType(PartyType.ZBT.getCode()); + data.setId(availableMarket.getId()); + data.setDelivery(availableMarket.getDelivery()); + data.setItemName(availableMarket.getItemName()); + data.setImageUrl(availableMarket.getImageUrl()); + data.setCnyPrice(availableMarket.getCnyPrice()); + data.setPrice(availableMarket.getPrice()); + resultList.add(data); + } + } + + } else if (param.getPartyType().equals(PartyType.YY_YOU_PING.getCode())){ + + // 构建查询参数 + HashMap apiParam = new HashMap<>(); + + ArrayList> ids = new ArrayList<>(); + for (TtOrnament item : list){ + Map i = MapUtil.builder("templateId", String.valueOf(item.getYyyoupingId())).build(); + ids.add(i); + } + + apiParam.put("requestList",ids); + + // 查询接口 + YYResult yyResult = yyyoupingService.yyApiBatchGetOnSaleCommodityInfo(apiParam); + if (!yyResult.isSuccess()) { + log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!"); + return R.fail("调用YY获取某一个饰品的所有在售接口异常"); + } + + // 过滤可用数据 + List data = JSONUtil.toList(JSON.toJSONString(yyResult.getData()), String.class); + resultList = data.stream().map(item->{ + cn.hutool.json.JSONObject obj = JSONUtil.parseObj(item); + cn.hutool.json.JSONObject saleTemplateResponse = obj.get("saleTemplateResponse", cn.hutool.json.JSONObject.class); + cn.hutool.json.JSONObject saleCommodityResponse = obj.get("saleCommodityResponse", cn.hutool.json.JSONObject.class); + + return OnSaleOrnamentVO.builder() + .partyType(PartyType.YY_YOU_PING.getCode()) + .id(saleTemplateResponse.get("templateId",String.class)) + .itemName(saleCommodityResponse.get("name",String.class)) + .imageUrl(list.get(0).getImageUrl()) + .price(saleCommodityResponse.get("minSellPrice",BigDecimal.class)) + .build(); + }).collect(Collectors.toList()); + + } + + return R.ok(resultList); + } + + @Override + public R tradeConfirm(TradeManualConfirmParam param) { + // 获取提货订单数据 + TtDeliveryRecord ttDeliveryRecord = deliveryRecordMapper.selectById(param.getDeliveryRecordId()); + + // 更新提货订单数据 + ttDeliveryRecord.setBuyPrice(param.getPrice()); + ttDeliveryRecord.setThirdpartyDelivery(1); + ttDeliveryRecord.setOrderId(param.getOrderNo()); + ttDeliveryRecord.setStatus(param.getStatus()); // 发货订单状态 1待发货 + ttDeliveryRecord.setMessage(param.getMessage()); + ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) { + return R.ok(); + } + return R.fail("更新提货订单数据异常!"); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/RechargeSuccessfulNoticeServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/RechargeSuccessfulNoticeServiceImpl.java new file mode 100644 index 0000000..f9341d3 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/service/impl/RechargeSuccessfulNoticeServiceImpl.java @@ -0,0 +1,51 @@ +package com.ruoyi.thirdparty.common.service.impl; + +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.message.service.MessageService; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +public class RechargeSuccessfulNoticeServiceImpl implements RechargeSuccessfulNoticeService { + + /*@Autowired + private RabbitTemplate rabbitTemplate; + + @Override + public void sendRechargeSuccessNotice(String userId, BigDecimal amount) { + try { + Map noticeMap = new HashMap<>(); + noticeMap.put("userId", userId); + noticeMap.put("title", "充值到账通知"); + noticeMap.put("content", "您充值" + amount + "已到账,请注意查收。"); + noticeMap.put("createTime", DateUtils.getTime()); + rabbitTemplate.convertAndSend("notice_queue", noticeMap); + } catch (Exception e) { + log.error("happen exception is {}", e.getMessage(), e); + } + + }*/ + @Autowired + private MessageService messageService; + + @Override + public void sendRechargeSuccessNotice(String userId, BigDecimal amount) { + AsyncManager.me().run(() -> { + try { + String title = MessageCategoryEnum.SYS_RECHARGE.getTitle(); + String content = "您充值" + amount + "金币已到账,请注意查收。"; + messageService.sendNotice(Long.valueOf(userId), title, content, MessageCategoryEnum.SYS_RECHARGE.getCode()); + } catch (Exception e) { + log.error("发送充值通知失败,userId={}, error={}", userId, e.getMessage(), e); + } + }); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/task/LoadAllOrnaments.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/task/LoadAllOrnaments.java new file mode 100644 index 0000000..8f7f2d3 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/common/task/LoadAllOrnaments.java @@ -0,0 +1,606 @@ +package com.ruoyi.thirdparty.common.task; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.math.MathUtil; +import cn.hutool.core.util.HashUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.ConfigService; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.admin.service.TtOrnamentYYService; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.domain.common.constant.PartyType; +import com.ruoyi.domain.dto.yyyouping.OrnamentYY; +import com.ruoyi.domain.dto.zbt.OrnamentZBT; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.common.NetworkDataLoader.NetworkDataLoader; +import com.ruoyi.thirdparty.yyyouping.service.yyyoupingService; +import com.ruoyi.thirdparty.yyyouping.utils.common.YYResult; +import com.ruoyi.thirdparty.zbt.param.SearchParams; +import com.ruoyi.thirdparty.zbt.result.ResultZbt; +import com.ruoyi.thirdparty.zbt.result.product.SearchData; +import com.ruoyi.thirdparty.zbt.service.ZBTService; +import io.swagger.models.auth.In; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.sql.SQLIntegrityConstraintViolationException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.PartyType.YY_YOU_PING; +import static com.ruoyi.domain.common.constant.PartyType.ZBT; + +@Slf4j +// 项目启动加载所有饰品信息 +//@Component +public class LoadAllOrnaments implements ApplicationRunner { + + // @Value("${yy-youping.ornamentsFilePath}") + // private String YYOrnamentsFilePath; + // + // @Autowired + // private yyyoupingService yyyoupingService; + + @Autowired + private TtOrnamentService ttOrnamentService; + + @Value("${mkcsgo.startLoadOrnaments}") + private Boolean startLoadOrnaments; + + // 资源加载器集合 + @Autowired + private List networkDataLoaderList; + + @Override + public void run(ApplicationArguments args) throws Exception { + + log.info("是否加载网络ornaments资源{}", startLoadOrnaments); + if (!startLoadOrnaments) return; + + // 1 加载各个平台数据 + Set ttOrnaments = new HashSet<>(); + for (NetworkDataLoader loader : networkDataLoaderList) { + Set load = loader.load(); + ttOrnaments.addAll(load); + } + if (ObjectUtil.isEmpty(ttOrnaments) || ttOrnaments.isEmpty()) { + log.warn("加载饰品网络资源失败。"); + return; + } + + // 2 写db + log.info("db allData ing~~~"); + for (TtOrnament item : ttOrnaments) { + try { + ttOrnamentService.saveOrUpdate(item); + } catch (DuplicateKeyException e) { + // log.info("过滤重复数据 【id】{}【hash】{}save。",item.getId(),item.getMarketHashName()); + } catch (Exception e) { + e.printStackTrace(); + log.info("保存数据异常。"); + break; + } + } + + log.info("加载饰品网络资源成功。"); + + } + + // 从平台获取数据 + // private Set loadOrnamentFromNet(PartyType[] partyTypes) { + // + // // 加载必要的枚举数据 + // List dictTypes = Arrays.asList( + // "ornaments_type", "ornaments_type_name", + // "ornaments_exterior", "ornaments_exterior_name", + // "ornaments_quality", "ornaments_quality_name", + // "ornaments_rarity", "ornaments_rarity_name"); + // List enumList = dictDataMapper.selectDictDataByTypes(dictTypes); + // + // Set res = null; + // for (NetworkDataLoader loader:networkDataLoaderList){ + // Set load = loader.load(); + // res.addAll(load); + // } + // + // return res; + // + // //1 加载ZBT平台数据 + // + // //... 加载...平台数据 + // + // //2 合并所有数据 + // + // // //1 从一个平台获取第一组基础数据 + // // Set baseData = loadBaseData(partyTypes[1],enumList); + // // if (ObjectUtil.isEmpty(baseData) || baseData.isEmpty()) return baseData; + // // + // // //2 整合其他平台 + // // return mergeOtherParty(baseData, partyTypes, enumList); + // + // } + // + // public Set mergeOtherParty(Set baseData,PartyType[] partyTypes ,List enumList){ + // + // // Integer f = 0; + // for (int i = 1; i < partyTypes.length; i++) { + // if (partyTypes[i].equals(YY_YOU_PING)) { + // + // YYResult yyResult = yyyoupingService.yyApiTemplateQuery(new HashMap()); + // String fileUrl = (String) yyResult.getData(); + // log.info(fileUrl); + // + // // 下载文件 + // SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss"); + // String format = dateFormat.format(System.currentTimeMillis()); + // String fileName = YYOrnamentsFilePath + "yy_orn_" + format + ".txt"; + // + // long size = HttpUtil.downloadFile(fileUrl, FileUtil.file(fileName)); + // log.info("yy饰品信息下载成功,文件大小: " + size); + // + // // 解析json + // File file = FileUtil.file(fileName); + // JSONArray jsonArray = JSONUtil.readJSONArray(file, StandardCharsets.UTF_8); + // List yyList = jsonArray.toList(OrnamentYY.class); + // + // // 转换为TtOrnament + // Set collect = yyList.stream().map(item -> { + // + // return TtOrnament.builder() + // .id(Long.valueOf(item.getHashName().hashCode())) + // .yyyoupingId(item.getId()) + // .marketHashName(item.getHashName()) + // .name(item.getName()) + // .price(BigDecimal.TEN) //// TODO: 2024/4/1 补充价格信息 + // .usePrice(BigDecimal.TEN) + // .updateTime(item.getUpdateTime()) + // .build(); + // + // }).collect(Collectors.toSet()); + // + // baseData.addAll(collect); + // + // } else if (partyTypes[i].equals(ZBT)) { + // + // // 饰品类型枚举 + // //List typeList = dictDataMapper.selectDictDataByType("ornaments_type"); + // + // for (SysDictData type : enumList) { + // + // if (!type.getDictType().equals("ornaments_type")) continue; + // + // Set ttOrnaments = ZBTLoadOrnamentByType(type.getDictLabel(),type,enumList); + // + // baseData.addAll(ttOrnaments); + // + // log.info("合并其他平台{}条数据。",ttOrnaments.size()); + // + // } + // + // } else { + // log.warn("非法的平台类型代码,获取饰品数据失败。"); + // } + // } + // + // // 过滤zbt没有的数据 + // log.info("过滤数据"); + // Set resData = baseData.stream().filter(item -> { + // return ObjectUtil.isNotEmpty(item.getZbtId()); + // }).collect(Collectors.toSet()); + // + // return resData; + // + // } + // + // // zbt 根据饰品类型获取数据 + // private Set ZBTLoadOrnamentByType(String typeHash,SysDictData type,List allEnum){ + // + // HashSet result = new HashSet<>(); + // + // // 分批请求api + // int page = 1; + // boolean stopFlag = false; + // while (true) { + // + // // 结束标记 + // if (stopFlag) break; + // + // // 请求平台数据 + // SearchParams param = SearchParams.builder() + // .page(String.valueOf(page)) + // .category(typeHash) + // .limit("200") + // .build(); + // ResultZbt response = zbtService.search(param); + // SearchData data = response.getData(); + // if (!response.getSuccess() || ObjectUtil.isEmpty(data)) break; + // + // List list = JSONUtil.toList(JSONUtil.toJsonStr(data.getList()), OrnamentZBT.class); + // + // if (list.size() < 200) stopFlag = !stopFlag; + // + // // 转换为通用类型 + // Set collect = list.stream().map(item -> { + // + // Long id = Long.valueOf(HashUtil.rsHash(item.getMarketHashName())); + // if (id < 0) id = Math.abs(id); + // + // TtOrnament ornament = TtOrnament.builder() + // .id(id) + // .zbtId(item.getItemId()) + // .marketHashName(item.getMarketHashName()) + // .name(item.getItemName()) + // .shortName(item.getShortName()) + // + // .price(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getPrice() : null) + // .usePrice(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getPrice() : null) + // .quantity(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getQuantity() : null) + // + // .imageUrl(item.getImageUrl()) + // + // .type(type.getDictValue()) + // .typeName(item.getTypeName()) + // + // .quality(getCodeByNameOrHash(allEnum,item.getQuality())) + // .qualityName(item.getQualityName()) + // .qualityColor(item.getQualityColor()) + // + // .rarity(getCodeByNameOrHash(allEnum,item.getRarity())) + // .rarityName(item.getRarityName()) + // .rarityColor(item.getRarityColor()) + // + // .exterior(getCodeByNameOrHash(allEnum,item.getExterior())) + // .exteriorHashName(item.getExterior()) + // .exteriorName(item.getExteriorName()) + // + // .updateTime(item.getUpdateTime()) + // .build(); + // + // ornament.setUsePrice(ornament.getPrice()); + // + // return ornament; + // }).collect(Collectors.toSet()); + // + // result.addAll(collect); + // + // // log.info("加载【扎比特】{}条类型为{}的饰品数据。",page*collect.size(),typeHash); + // System.out.print("\r加载【扎比特】"+page*collect.size()+"条类型为"+typeHash+"的饰品数据。"); + // + // page++; + // } + // + // return result; + // + // } + // + // // 从yy平台获取第一组基础数据 + // private Set loadBaseData(PartyType partyType,List allEnum) { + // + // Set baseData = null; + // + // if (partyType.equals(YY_YOU_PING)) { + // baseData = getBaseDataFromYY(allEnum); + // } else if (partyType.equals(ZBT)) { + // + // baseData = getBaseDataFromZBT(allEnum); + // + // } else { + // log.warn("非法的平台类型代码,获取饰品数据失败。"); + // return baseData; + // } + // + // return baseData; + // } + // + // // 从ZBT获取基础数据 + // public Set getBaseDataFromZBT(List allEnum){ + // + // Set baseData = null; + // + // YYResult yyResult = yyyoupingService.yyApiTemplateQuery(new HashMap()); + // if (!yyResult.isSuccess()) { + // log.warn(yyResult.getMsg()); + // return baseData; + // } + // String fileUrl = (String) yyResult.getData(); + // log.info("yyFile=" + fileUrl); + // + // // 下载文件 + // String fileName = YYOrnamentsFilePath + "yyOrn" + System.currentTimeMillis() + ".txt"; + // System.out.println(fileName); + // + // long size = HttpUtil.downloadFile(fileUrl, new File(fileName)); + // log.info("yy饰品信息下载成功,文件大小: " + size); + // + // // 解析json + // File file = FileUtil.file(fileName); + // JSONArray jsonArray = JSONUtil.readJSONArray(file, StandardCharsets.UTF_8); + // List yyList = jsonArray.toList(OrnamentYY.class); + // + // log.info("补充详细信息~~~"); + // // 转换为TtOrnament + // baseData = yyList.stream().map(item -> { + // + // Long id = Long.valueOf(HashUtil.rsHash(item.getHashName())); + // if (id < 0) { + // id = Math.abs(id); + // } + // + // TtOrnament ornament = TtOrnament.builder() + // .id(id) + // .yyyoupingId(item.getId()) + // .marketHashName(item.getHashName()) + // + // .type(getCodeByNameOrHash(allEnum,item.getTypeHashName())) + // .typeName(item.getTypeName()) + // .typeHashName(item.getTypeHashName()) + // + // .name(item.getName()) + // .shortName(item.getName()) + // + // .isPutaway("1") + // .isProprietaryProperty("1") + // .updateTime(item.getUpdateTime()) + // .build(); + // + // return ornament; + // }).collect(Collectors.toSet()); + // + // // 分批补充 补充其他信息 + // List> requestList = baseData.stream().map(item -> { + // return MapUtil.builder("templateId", String.valueOf(item.getYyyoupingId())).build(); + // }).collect(Collectors.toList()); + // + // int page = 0; + // int pageSize = 200; + // Boolean stopFlag = false; + // ArrayList ornamentsList = new ArrayList<>(baseData); + // while (true) { + // + // if (stopFlag) { + // System.out.println("-100% 补充完成✔"); + // break; + // } + // + // BigDecimal p = new BigDecimal(page * pageSize) + // .divide(new BigDecimal(baseData.size()),2,BigDecimal.ROUND_DOWN); + // System.out.print("\r补充"+page * pageSize+"条基础记录"+p.multiply(new BigDecimal(100)).setScale(0)+"%"); + // + // + // List> reqList = null; + // try { + // reqList = requestList.subList(page * pageSize, page * pageSize + pageSize); // 尽量大 + // } catch (Exception e) { + // int lenth = requestList.size(); + // reqList = requestList.subList(page * pageSize, lenth - 1); // 尽量大 + // stopFlag = !stopFlag; + // } + // + // HashMap map = new HashMap<>(); + // map.put("requestList", reqList); + // + // YYResult yyResult1 = yyyoupingService.yyApiBatchGetOnSaleCommodityInfo(map); + // List list = JSONUtil.toList(JSONUtil.toJsonStr(yyResult1.getData()), JSONObject.class); + // + // for (JSONObject obj : list) { + // + // JSONObject saleTemplateResponse = JSONUtil.parseObj(obj.get("saleTemplateResponse")); + // JSONObject saleCommodityResponse = JSONUtil.parseObj(obj.get("saleCommodityResponse")); + // + // String templateId = String.valueOf((Integer) saleTemplateResponse.get("templateId")); + // + // for (int i = 0; i < ornamentsList.size(); i++) { + // + // TtOrnament ornament = ornamentsList.get(i); + // if (templateId.equals(String.valueOf(ornament.getYyyoupingId()))) { + // + // ornament.setPrice(new BigDecimal(saleCommodityResponse.get("minSellPrice").toString())); + // ornament.setUsePrice(new BigDecimal(saleCommodityResponse.get("minSellPrice").toString())); + // + // ornament.setImageUrl(saleTemplateResponse.get("iconUrl").toString()); + // + // Object exteriorName = saleTemplateResponse.get("exteriorName"); + // if (ObjectUtil.isNotNull(exteriorName)){ + // ornament.setExterior(getCodeByNameOrHash(allEnum,exteriorName.toString())); + // ornament.setExteriorName(exteriorName.toString()); + // } + // + // ornament.setQuality(getCodeByNameOrHash(allEnum,saleTemplateResponse.get("qualityName").toString())); + // ornament.setQualityName(saleTemplateResponse.get("qualityName").toString()); + // // if (ObjectUtil.isEmpty(ornament.getQuality()) && ObjectUtil.isNotEmpty(ornament.getQualityName())){ + // // System.out.println(ornament.getQuality()); + // // System.out.println(ornament.getQualityName()); + // // } + // + // ornament.setRarity(getCodeByNameOrHash(allEnum,saleTemplateResponse.get("rarityName").toString())); + // ornament.setRarityName(saleTemplateResponse.get("rarityName").toString()); + // + // ornamentsList.set(i,ornament); + // + // break; + // } + // } + // } + // + // page++; + // + // } + // + // baseData = new HashSet<>(ornamentsList); + // + // return baseData; + // } + // + // // 从yy获取基础数据 + // public Set getBaseDataFromYY(List allEnum){ + // + // Set baseData = null; + // + // YYResult yyResult = yyyoupingService.yyApiTemplateQuery(new HashMap()); + // if (!yyResult.isSuccess()) { + // log.warn(yyResult.getMsg()); + // return baseData; + // } + // String fileUrl = (String) yyResult.getData(); + // log.info("yyFile=" + fileUrl); + // + // // 下载文件 + // String fileName = YYOrnamentsFilePath + "yyOrn" + System.currentTimeMillis() + ".txt"; + // System.out.println(fileName); + // + // long size = HttpUtil.downloadFile(fileUrl, new File(fileName)); + // log.info("yy饰品信息下载成功,文件大小: " + size); + // + // // 解析json + // File file = FileUtil.file(fileName); + // JSONArray jsonArray = JSONUtil.readJSONArray(file, StandardCharsets.UTF_8); + // List yyList = jsonArray.toList(OrnamentYY.class); + // + // log.info("补充详细信息~~~"); + // // 转换为TtOrnament + // baseData = yyList.stream().map(item -> { + // + // Long id = Long.valueOf(HashUtil.rsHash(item.getHashName())); + // if (id < 0) { + // id = Math.abs(id); + // } + // + // TtOrnament ornament = TtOrnament.builder() + // .id(id) + // .yyyoupingId(item.getId()) + // .marketHashName(item.getHashName()) + // + // .type(getCodeByNameOrHash(allEnum,item.getTypeHashName())) + // .typeName(item.getTypeName()) + // .typeHashName(item.getTypeHashName()) + // + // .name(item.getName()) + // .shortName(item.getName()) + // + // .isPutaway("1") + // .isProprietaryProperty("1") + // .updateTime(item.getUpdateTime()) + // .build(); + // + // return ornament; + // }).collect(Collectors.toSet()); + // + // // 分批补充 补充其他信息 + // List> requestList = baseData.stream().map(item -> { + // return MapUtil.builder("templateId", String.valueOf(item.getYyyoupingId())).build(); + // }).collect(Collectors.toList()); + // + // int page = 0; + // int pageSize = 200; + // Boolean stopFlag = false; + // ArrayList ornamentsList = new ArrayList<>(baseData); + // while (true) { + // + // if (stopFlag) { + // System.out.println("-100% 补充完成✔"); + // break; + // } + // + // BigDecimal p = new BigDecimal(page * pageSize) + // .divide(new BigDecimal(baseData.size()),2,BigDecimal.ROUND_DOWN); + // System.out.print("\r补充"+page * pageSize+"条基础记录"+p.multiply(new BigDecimal(100)).setScale(0)+"%"); + // + // + // List> reqList = null; + // try { + // reqList = requestList.subList(page * pageSize, page * pageSize + pageSize); // 尽量大 + // } catch (Exception e) { + // int lenth = requestList.size(); + // reqList = requestList.subList(page * pageSize, lenth - 1); // 尽量大 + // stopFlag = !stopFlag; + // } + // + // HashMap map = new HashMap<>(); + // map.put("requestList", reqList); + // + // YYResult yyResult1 = yyyoupingService.yyApiBatchGetOnSaleCommodityInfo(map); + // List list = JSONUtil.toList(JSONUtil.toJsonStr(yyResult1.getData()), JSONObject.class); + // + // for (JSONObject obj : list) { + // + // JSONObject saleTemplateResponse = JSONUtil.parseObj(obj.get("saleTemplateResponse")); + // JSONObject saleCommodityResponse = JSONUtil.parseObj(obj.get("saleCommodityResponse")); + // + // String templateId = String.valueOf((Integer) saleTemplateResponse.get("templateId")); + // + // for (int i = 0; i < ornamentsList.size(); i++) { + // + // TtOrnament ornament = ornamentsList.get(i); + // if (templateId.equals(String.valueOf(ornament.getYyyoupingId()))) { + // + // ornament.setPrice(new BigDecimal(saleCommodityResponse.get("minSellPrice").toString())); + // ornament.setUsePrice(new BigDecimal(saleCommodityResponse.get("minSellPrice").toString())); + // + // ornament.setImageUrl(saleTemplateResponse.get("iconUrl").toString()); + // + // Object exteriorName = saleTemplateResponse.get("exteriorName"); + // if (ObjectUtil.isNotNull(exteriorName)){ + // ornament.setExterior(getCodeByNameOrHash(allEnum,exteriorName.toString())); + // ornament.setExteriorName(exteriorName.toString()); + // } + // + // ornament.setQuality(getCodeByNameOrHash(allEnum,saleTemplateResponse.get("qualityName").toString())); + // ornament.setQualityName(saleTemplateResponse.get("qualityName").toString()); + // // if (ObjectUtil.isEmpty(ornament.getQuality()) && ObjectUtil.isNotEmpty(ornament.getQualityName())){ + // // System.out.println(ornament.getQuality()); + // // System.out.println(ornament.getQualityName()); + // // } + // + // ornament.setRarity(getCodeByNameOrHash(allEnum,saleTemplateResponse.get("rarityName").toString())); + // ornament.setRarityName(saleTemplateResponse.get("rarityName").toString()); + // + // ornamentsList.set(i,ornament); + // + // break; + // } + // } + // } + // + // page++; + // + // } + // + // baseData = new HashSet<>(ornamentsList); + // + // return baseData; + // } + // + // public static void main(String[] args) { + // System.out.println("StatTrak™".equals("")); + // } + // + // // 根据类型和标签获取code + // private String getCodeByNameOrHash(List typeList,String nameOrHash){ + // + // for (SysDictData type : typeList){ + // if (type.getDictLabel().equals(nameOrHash)) return type.getDictValue(); + // if (type.getDictType().equals(nameOrHash)) return type.getDictValue(); + // } + // return null; + // } + + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/config/Cs340Config.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/config/Cs340Config.java new file mode 100644 index 0000000..a237a5f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/config/Cs340Config.java @@ -0,0 +1,22 @@ +package com.ruoyi.thirdparty.cs340.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "cs340") +public class Cs340Config { + + private String baseUrl; + + private String appKey; + + private String privateKey; + + private String publicKey; + + private String callbackPublicKey; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/controller/Cs2pifaCallbackController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/controller/Cs2pifaCallbackController.java new file mode 100644 index 0000000..b26b6e7 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/controller/Cs2pifaCallbackController.java @@ -0,0 +1,28 @@ +package com.ruoyi.thirdparty.cs340.controller; + +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.thirdparty.cs340.domain.result.Cs340CallBackNotifyInfo; +import com.ruoyi.thirdparty.cs340.service.Cs340Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * cs2pifa 发货回调接口(对外暴露,供第三方平台回调) + */ +@RestController +@RequestMapping("/api/cs2pifa") +public class Cs2pifaCallbackController { + + @Autowired + private Cs340Service cs340Service; + + /** + * 接收 cs2pifa 平台发货回调通知 + */ + @Anonymous + @PostMapping("/notify") + public AjaxResult notify(@RequestBody Cs340CallBackNotifyInfo notify) { + return cs340Service.notify(notify); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/controller/Cs340ItemController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/controller/Cs340ItemController.java new file mode 100644 index 0000000..e29309c --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/controller/Cs340ItemController.java @@ -0,0 +1,77 @@ +package com.ruoyi.thirdparty.cs340.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.thirdparty.cs340.domain.body.ByTemplateCreateOrder; +import com.ruoyi.thirdparty.cs340.domain.body.Cs340BatchQueryPricesBody; +import com.ruoyi.thirdparty.cs340.domain.body.Cs340CheckTradeUrlBody; +import com.ruoyi.thirdparty.cs340.domain.body.Cs340OnSaleInfoBody; +import com.ruoyi.thirdparty.cs340.domain.result.BatchGetOnSaleCommodityInfoResult; +import com.ruoyi.thirdparty.cs340.domain.result.Cs340CallBackNotifyInfo; +import com.ruoyi.thirdparty.cs340.service.Cs340Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/admin/cs340item") +public class Cs340ItemController extends BaseController { + + @Autowired + private Cs340Service cs340Service; + + @PostMapping("/checkTradeUrl") + public AjaxResult checkTradeUrl(@RequestBody Cs340CheckTradeUrlBody body) { + return success(cs340Service.checkTradeUrl(body.getTradeUrl())); + } + + @GetMapping("/checkMerchantBalance") + public AjaxResult checkMerchantBalance() { + return success(cs340Service.checkMerchantBalance()); + } + + /** + * 查询库存 + */ + @PostMapping("/queryOnSaleInfo") + public AjaxResult queryOnSaleInfo(@Validated @RequestBody Cs340OnSaleInfoBody onSaleInfoBody) { + BatchGetOnSaleCommodityInfoResult result = cs340Service.queryOnSaleInfo(onSaleInfoBody); + if (result == null || result.getSaleCommodityResponse() == null) { + return AjaxResult.error("cs2pifa平台未查询到该饰品的在售信息,请确认饰品名称(hashName)是否正确,或稍后再试"); + } + return success(result); + } + + @PostMapping("/createOrder") + public AjaxResult createOrder(@RequestBody ByTemplateCreateOrder createOrderBody) { + return success(cs340Service.createOrder(createOrderBody)); + } + + @PostMapping("/notify") + public AjaxResult notify(@RequestBody Cs340CallBackNotifyInfo notify) { + return cs340Service.notify(notify); + } + + /** + * 批量实时查询三方最低在售价格 + */ + @PostMapping("/batchQueryPrices") + public AjaxResult batchQueryPrices(@Validated @RequestBody Cs340BatchQueryPricesBody body) { + List results = cs340Service.batchGetOnSaleCommodityInfoResult(body.getHashNames()); + Map priceMap = new HashMap<>(); + for (BatchGetOnSaleCommodityInfoResult result : results) { + if (result.getSaleTemplateResponse() != null + && result.getSaleCommodityResponse() != null + && result.getSaleCommodityResponse().getMinSellPrice() != null) { + priceMap.put( + result.getSaleTemplateResponse().getTemplateHashName(), + result.getSaleCommodityResponse().getMinSellPrice()); + } + } + return success(priceMap); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/ByTemplateCreateOrder.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/ByTemplateCreateOrder.java new file mode 100644 index 0000000..90e278e --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/ByTemplateCreateOrder.java @@ -0,0 +1,27 @@ +package com.ruoyi.thirdparty.cs340.domain.body; + +import lombok.Data; + +/** + * merchantOrderNo String 是 商户订单号 小于等于59位的字符串 + * tradeLinks String 是 收货方的Steam交易链接 + * commodityTemplateId String 是 商品模版ID + * 数据来源:查询模板ID templateId和hashName二选一必传递,同时传入以templateId为准,如传入不可为0 + * commodityHashName String 是 模板hashname + * 数据来源:查询模板ID templateId和hashName二选一必传递,同时传入以templateId为准,如传入不可为空 + * purchasePrice String 是 购买最高价 单位:元 + * 不可为0 + * fastShipping Integer 否 极速发货购买模式 0:优先购买极速发货; + * 1:只购买极速发货,该模式purchasePrice参数请使用fastShippingMinSellPrice作为价格,极速发货价格可通过批量查询在售商品价格或者批量查询在售商品详情接口获取,极速发货价格存在并且大于0可使用极速发货。 + */ +@Data +public class ByTemplateCreateOrder { + + private String merchantOrderNo; + + private String tradeLinks; + + private String commodityHashName; + + private String purchasePrice; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/Cs340BatchQueryPricesBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/Cs340BatchQueryPricesBody.java new file mode 100644 index 0000000..7892659 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/Cs340BatchQueryPricesBody.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.cs340.domain.body; + +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; + +import java.util.List; + +@Data +public class Cs340BatchQueryPricesBody { + + @NotEmpty(message = "hashNames不能为空") + private List hashNames; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/Cs340CheckTradeUrlBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/Cs340CheckTradeUrlBody.java new file mode 100644 index 0000000..09628f1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/Cs340CheckTradeUrlBody.java @@ -0,0 +1,9 @@ +package com.ruoyi.thirdparty.cs340.domain.body; + +import lombok.Data; + +@Data +public class Cs340CheckTradeUrlBody { + + private String tradeUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/Cs340OnSaleInfoBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/Cs340OnSaleInfoBody.java new file mode 100644 index 0000000..3e43dca --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/body/Cs340OnSaleInfoBody.java @@ -0,0 +1,12 @@ +package com.ruoyi.thirdparty.cs340.domain.body; + +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Data +public class Cs340OnSaleInfoBody { + + @NotNull(message = "marketHashName不能为空") + private String marketHashName; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/BatchGetOnSaleCommodityInfoDto.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/BatchGetOnSaleCommodityInfoDto.java new file mode 100644 index 0000000..f2de117 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/BatchGetOnSaleCommodityInfoDto.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.cs340.domain.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class BatchGetOnSaleCommodityInfoDto { + private String sign; + + private String timestamp; + + private String appKey; + + List requestList; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/ByTemplateCreateOrderDto.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/ByTemplateCreateOrderDto.java new file mode 100644 index 0000000..8bf589d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/ByTemplateCreateOrderDto.java @@ -0,0 +1,38 @@ +package com.ruoyi.thirdparty.cs340.domain.dto; + +import lombok.Data; + +/** + * merchantOrderNo String 是 商户订单号 小于等于59位的字符串 + * tradeLinks String 是 收货方的Steam交易链接 + * commodityHashName String 是 模板hashname&数据来源:查询模板ID templateId和hashName二选一必传递,同时传入以templateId为准,如传入不可为空 + * purchasePrice String 是 购买最高价 单位:元 不可为0 + * 请求示例 + * { + * "sign":"4LL1O++Btn5JgZ6kHuJAN7dEG4cgLd0goo+Cb4wld0ygpU8lU+73mLxXYtN81ApAhWuw9zn+ENrtc2KcrL6XgHMSG8JC/5NjHCGO/3jDUjAac3YMh5vdQjsqhK1IoxdrOXM9QUyUj2kU9CA4eRey5wBx+BjbzHkSXXt6tOk9pDkeg56wuKR9L3g1BldcwBEacW2fU4pfEW9vpJC9QTCU/FAT0JXMTpYOaeIn+uf9eQRlRNguI1eUoWJAiGQgVQ/TlFlkdVqeE0pHp3Ntod2r+pkb4vCKrMDI6HW28XsM0uMupOfBUL8zRNFEOvLtswJduVo5r5Fd1ysHFcFywHOT0A==", + * "timestamp":"2023-01-17 17:59:00", + * "appKey":"123456", + * "merchantOrderNo": "20230112000", + * "tradeLinks": "https://steamcommunity.com/tradeoffer/new/?partner=123456&token=vP12QeQA", + * "commodityTemplateId": "100696", + * "commodityHashName": "", + * "purchasePrice": "0.08" + * } + */ +@Data +public class ByTemplateCreateOrderDto { + + private String sign; + + private String timestamp; + + private String appKey; + + private String merchantOrderNo; + + private String tradeLinks; + + private String commodityHashName; + + private String purchasePrice; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/CheckMerchantBalanceDto.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/CheckMerchantBalanceDto.java new file mode 100644 index 0000000..37bd1d1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/CheckMerchantBalanceDto.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.cs340.domain.dto; + +import lombok.Data; + +@Data +public class CheckMerchantBalanceDto { + + private String sign; + + private String timestamp; + + private String appKey; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/CheckTradeUrlDto.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/CheckTradeUrlDto.java new file mode 100644 index 0000000..7904874 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/CheckTradeUrlDto.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.cs340.domain.dto; + +import lombok.Data; + +@Data +public class CheckTradeUrlDto { + + private String sign; + + private String timestamp; + + private String appKey; + + private String tradeLinks; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/CommodityInfoRequestListDto.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/CommodityInfoRequestListDto.java new file mode 100644 index 0000000..8bb8bd6 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/CommodityInfoRequestListDto.java @@ -0,0 +1,17 @@ +package com.ruoyi.thirdparty.cs340.domain.dto; + +import lombok.Data; + +/** + * requestList Object[] 是 批量请求参数列表 + * 数组数量:1-200 + * templateId Integer 是 模板id + * 来源:查询模板ID templateId和hashName二选一必传递,如传入不可为0 + * templateHashName String 是 模板hashName + * 来源:查询模板ID templateId和hashName二选一必传递,如传入不可为空 + */ +@Data +public class CommodityInfoRequestListDto { + private Long templateId; + private String templateHashName; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/Cs340OrderStatusDto.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/Cs340OrderStatusDto.java new file mode 100644 index 0000000..5c9379e --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/Cs340OrderStatusDto.java @@ -0,0 +1,19 @@ +package com.ruoyi.thirdparty.cs340.domain.dto; + +import lombok.Data; + +/** + * orderNo String 是 订单号 小于等于40位的数字字符串 + * merchantNo String 是 商户订单号 数据来源: + * 指定模板购买 + * 指定商品购买 + * 注:orderNo和merchantNo二选一传递,同时传入以orderNo为准 + * 小于等于59位的字符串 + */ +@Data +public class Cs340OrderStatusDto { + private String sign; + private String timestamp; + private String appKey; + private String orderNo; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/QueryTemplateSaleByCategoryDto.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/QueryTemplateSaleByCategoryDto.java new file mode 100644 index 0000000..41e284e --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/dto/QueryTemplateSaleByCategoryDto.java @@ -0,0 +1,29 @@ +package com.ruoyi.thirdparty.cs340.domain.dto; + +import lombok.Data; + +/** + * typeId Integer 是 武器大类ID + * typeHashName String 是 武器大类hashName 如传入不可为空 + * typeId和typeHashName同时传递时以typeId为准 + * weaponId String 是 武器类型ID + * weaponHashName String 是 武器类型HashName 如传入不可为空 + * weaponId和weaponHashName同时传递时以weaponId为准 + * 武器大类和武器类型同时传递,以武器类型为准。 + * priceRangeMinmun String 否 商品价格区间最小值 单位:元 + * priceRangeMaxmun String 否 商品价格区间最大值 单位:元 + * page String 否 页码 最大50 + * pageSize String 否 每页显示数量 最大值200,若传入参数>200,则按200执行 + */ +@Data +public class QueryTemplateSaleByCategoryDto { + private String sign; + private String timestamp; + private String appKey; + private Integer typeId; + private String typeHashName; + private String weaponId; + private String weaponHashName; + private String page; + private String pageSize; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/BatchGetOnSaleCommodityInfoResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/BatchGetOnSaleCommodityInfoResult.java new file mode 100644 index 0000000..0a07702 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/BatchGetOnSaleCommodityInfoResult.java @@ -0,0 +1,27 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +/** + * data Object[] 返回消息体。 + * saleTemplateResponse Object + * templateId String 模板id。 + * templateHashName String 模板hashName。 + * iconUrl String 模板图片链接。 + * exteriorName String 外观名称。 + * rarityName String 品质。 + * qualityName String 类别。 + * saleCommodityResponse Object + * minSellPrice String 在售最低价(单位:元)。 + * fastShippingMinSellPrice String 极速发货在售最低价(单位:元)。 + * referencePrice String 模板参考价(单位:元)。 + * sellNum String 在售数量。 + */ +@Data +public class BatchGetOnSaleCommodityInfoResult { + + private SaleTemplateResponse saleTemplateResponse; + + private SaleCommodityResponse saleCommodityResponse; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/ByTemplateCreateOrderResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/ByTemplateCreateOrderResult.java new file mode 100644 index 0000000..8c5feec --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/ByTemplateCreateOrderResult.java @@ -0,0 +1,37 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 参数 类型 说明 + * data Object 返回消息体。 + * merchantOrderNo String 商户订单号。 + * orderNo String 订单单号。 + * payAmount number 实际支付金额。 + * orderStatus Integer 交易状态:1,成功;0,失败。 + * 示例: + * { + * "code": 0, + * "msg": "成功", + * "timestamp": 1673943192928, + * "data": { + * "merchantOrderNo": "20230112102", + * "orderNo": "2023011710002994746", + * "payAmount": 0.1, + * "orderStatus": 0 + * } + * } + */ +@Data +public class ByTemplateCreateOrderResult { + + private String merchantOrderNo; + + private String orderNo; + + private BigDecimal payAmount; + + private Integer orderStatus; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CallBackInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CallBackInfo.java new file mode 100644 index 0000000..1084dbd --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CallBackInfo.java @@ -0,0 +1,45 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +/** + * orderNo String 订单号。 + * orderType Integer 预留字段 + * orderSubType Integer 预留字段 + * shippingMode Integer 发货模式:0,卖家直发;1,极速发货 + * tradeOfferId long 报价ID + * tradeOfferLinks String 报价链接 + * buyerUserId Integer 购买用户编号。 + * orderStatus Integer 订单大状态。 + * orderSubStatus Integer 订单小状态。 + * failCode Integer 订单失败原因编号。 + * failReason String 订单失败原因提示信息。 + * merchantOrderNo String 第三方商户单号。 + * notifyType Integer 通知类型(0:创建订单,1:等待发货,2:等待收货,3:购买成功,4:订单取消)。 + * notifyDesc String 通知类型描述(创建订单,等待发货,等待收货,购买成功,订单取消)。 + * + * 通知类型 orderStatus orderSubStatus + * 创建订单 100-创建订单 + * 等待发货 140-交货中 1101 待发送报价 + * 等待收货 1103 待确认报价 + * 购买成功 340-已完成 + * 订单取消 280-已取消 + * 订单购买失败 2-购买失败 + */ +@Data +public class Cs340CallBackInfo { + private String orderNo; + private Integer orderType; + private Integer orderSubType; + private Integer shippingMode; + private Long tradeOfferId; + private String tradeOfferLinks; + private Integer buyerUserId; + private Integer orderStatus; + private Integer orderSubStatus; + private Integer failCode; + private String failReason; + private String merchantOrderNo; + private Integer notifyType; + private String notifyDesc; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CallBackNotifyInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CallBackNotifyInfo.java new file mode 100644 index 0000000..40cca12 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CallBackNotifyInfo.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +@Data +public class Cs340CallBackNotifyInfo { + private String messageNo; + private String callBackInfo; + private String sign; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CallBackResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CallBackResult.java new file mode 100644 index 0000000..504a5fb --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CallBackResult.java @@ -0,0 +1,9 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +@Data +public class Cs340CallBackResult { + private String messageNo; + private boolean flag; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CheckTradeUrlResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CheckTradeUrlResult.java new file mode 100644 index 0000000..d1c311a --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340CheckTradeUrlResult.java @@ -0,0 +1,13 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +@Data +public class Cs340CheckTradeUrlResult { + + private String steamId; + + private Integer status; + + private String msg; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340MerchantBalanceResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340MerchantBalanceResult.java new file mode 100644 index 0000000..ec20857 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340MerchantBalanceResult.java @@ -0,0 +1,13 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class Cs340MerchantBalanceResult { + + private BigDecimal amount; + + private Integer userId; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340OrderStatusResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340OrderStatusResult.java new file mode 100644 index 0000000..7c2887d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340OrderStatusResult.java @@ -0,0 +1,43 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +/** + * data object 返回消息体。 + * orderNumber String 订单编号。 + * shippingMode Integer 发货模式:0,卖家直发;1,极速发货 + * tradeOfferId Long 报价ID + * tradeOfferLinks String 报价链接(不是每笔订单都存在) + * statusMsg String 订单状态。 + * bigStatus Integer 订单大状态。 + * bigStatusMsg String 订单大状态提示信息。 + * smallStatus Integer 订单小状态。 + * smallStatusMsg String 订单小状态提示信息。 + * failCode Integer 订单失败原因编号,仅记录失败原因具体以bigStatus状态为准 + * failReason String 订单失败原因提示信息。 + * + * 大状态bigStatus说明 + * 数值 说明 + * 2 失败 + * 100 创建中。 + * 120 支付中。 + * 130 异常挂起。 + * 140 交货中。 + * 360 结算中。 + * 340 已完成。 + * 280 已取消。 + */ +@Data +public class Cs340OrderStatusResult { + private String orderNumber; + private Integer shippingMode; + private Long tradeOfferId; + private String tradeOfferLinks; + private String statusMsg; + private Integer bigStatus; + private String bigStatusMsg; + private Integer smallStatus; + private String smallStatusMsg; + private Integer failCode; + private String failReason; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340Result.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340Result.java new file mode 100644 index 0000000..bcbf603 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340Result.java @@ -0,0 +1,15 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +@Data +public class Cs340Result { + + private Integer code; + + private String msg; + + private Long timestamp; + + private T data; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340TemplateItem.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340TemplateItem.java new file mode 100644 index 0000000..eb4c284 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/Cs340TemplateItem.java @@ -0,0 +1,44 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +/** + * cs2pifa「查询模板ID」接口(/v1/api/templateQuery)下载文件中的单个模板信息。 + *

+ * 该接口返回一个下载地址,下载后的 JSON 为平台全部模板(饰品)列表,单项字段如下: + *

+ * id             模板id
+ * name           模板中文名称
+ * hashName       模板 hashName(饰品唯一名称,对应 tt_ornament.market_hash_name)
+ * typeId         武器大类id
+ * typeName       武器大类中文名
+ * typeHashName   武器大类 hashName
+ * weaponId       武器类型id
+ * weaponName     武器类型中文名
+ * weaponHashName 武器类型 hashName
+ * updateTime     更新时间
+ * 
+ */ +@Data +public class Cs340TemplateItem { + + private Long id; + + private String name; + + private String hashName; + + private Integer typeId; + + private String typeName; + + private String typeHashName; + + private Integer weaponId; + + private String weaponName; + + private String weaponHashName; + + private String updateTime; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleCommodityResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleCommodityResponse.java new file mode 100644 index 0000000..2b43ee8 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleCommodityResponse.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +/** + *saleCommodityResponse Object + * minSellPrice String 在售最低价(单位:元)。 + * fastShippingMinSellPrice String 极速发货在售最低价(单位:元)。 + * referencePrice String 模板参考价(单位:元)。 + * sellNum String 在售数量。 + */ +@Data +public class SaleCommodityResponse { + private String minSellPrice; + private String fastShippingMinSellPrice; + private String referencePrice; + private String sellNum; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleTemplateByCategoryResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleTemplateByCategoryResponse.java new file mode 100644 index 0000000..17f5fbc --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleTemplateByCategoryResponse.java @@ -0,0 +1,37 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +/** + * templateId integer 模板id + * templateHashName string 模板hash name + * templateName string 模版名称 + * iconUrl string 模板图片 + * exteriorName string 外观名称 + * rarityName string 品质 + * typeId integer 武器大类 + * typeHashName string 武器大类hashName + * weaponId integer 武器类型ID + * weaponHashName string 武器类型标签hashName + * minSellPrice string 在售最低价(单位元) + * fastShippingMinSellPrice String 极速发货在售最低价(单位:元) + * referencePrice String 模板参考价(单位:元)。 + * sellNum integer 在售数量 + */ +@Data +public class SaleTemplateByCategoryResponse { + private Integer templateId; + private String templateHashName; + private String templateName; + private String iconUrl; + private String exteriorName; + private String rarityName; + private Integer typeId; + private String typeHashName; + private Integer weaponId; + private String weaponHashName; + private String minSellPrice; + private String fastShippingMinSellPrice; + private String referencePrice; + private Integer sellNum; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleTemplateByCategoryResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleTemplateByCategoryResult.java new file mode 100644 index 0000000..f6338b5 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleTemplateByCategoryResult.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +import java.util.List; + +/** + * currentPage + * newPageIsHaveContent + * saleTemplateByCategoryResponseList + * templateId + */ +@Data +public class SaleTemplateByCategoryResult { + private Integer currentPage; + private Boolean newPageIsHaveContent; + private List saleTemplateByCategoryResponseList; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleTemplateResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleTemplateResponse.java new file mode 100644 index 0000000..0278505 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/domain/result/SaleTemplateResponse.java @@ -0,0 +1,22 @@ +package com.ruoyi.thirdparty.cs340.domain.result; + +import lombok.Data; + +/** + * * saleTemplateResponse Object + * * templateId String 模板id。 + * * templateHashName String 模板hashName。 + * * iconUrl String 模板图片链接。 + * * exteriorName String 外观名称。 + * * rarityName String 品质。 + * * qualityName String 类别。 + */ +@Data +public class SaleTemplateResponse { + private String templateId; + private String templateHashName; + private String iconUrl; + private String exteriorName; + private String rarityName; + private String qualityName; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/service/Cs340Service.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/service/Cs340Service.java new file mode 100644 index 0000000..63ee2af --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/service/Cs340Service.java @@ -0,0 +1,56 @@ +package com.ruoyi.thirdparty.cs340.service; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.thirdparty.cs340.domain.body.ByTemplateCreateOrder; +import com.ruoyi.thirdparty.cs340.domain.body.Cs340OnSaleInfoBody; +import com.ruoyi.thirdparty.cs340.domain.result.*; + +import java.util.List; + +public interface Cs340Service { + /** + * 验证交易链接 + */ + Cs340CheckTradeUrlResult checkTradeUrl(String tradeUrl); + /** + * 余额查询 + */ + Cs340MerchantBalanceResult checkMerchantBalance(); + /** + * 创建订单 + */ + Cs340Result createOrder(ByTemplateCreateOrder order); + + /** + * 查询订单状态 + * @param orderNo 商家订单号 + */ + Cs340Result queryOrderStatus(String orderNo); + + /** + * 查询在线订单 + */ + BatchGetOnSaleCommodityInfoResult queryOnSaleInfo(Cs340OnSaleInfoBody onSaleInfoBody); + + /** + * 回调通知信息 + */ + AjaxResult notify(Cs340CallBackNotifyInfo notify); + + /** + * 批量获取价格 + */ + List batchGetOnSaleCommodityInfoResult(List templateHashNames); + + /** + * 查询模板ID:返回「全部模板列表」文件的下载地址。 + * 对应 cs2pifa 接口 /v1/api/templateQuery(频率限制:1次/120s) + */ + String queryTemplateDownloadUrl(); + + /** + * 拉取并解析 cs2pifa 全部模板(饰品)列表。 + * 内部先调用 {@link #queryTemplateDownloadUrl()} 获取下载地址,再下载并解析为对象列表。 + */ + List queryAllTemplates(); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/service/impl/Cs340ServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/service/impl/Cs340ServiceImpl.java new file mode 100644 index 0000000..5c35299 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/service/impl/Cs340ServiceImpl.java @@ -0,0 +1,386 @@ +package com.ruoyi.thirdparty.cs340.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.admin.mapper.TtDeliveryRecordMapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.http.HttpUtils; +import com.ruoyi.domain.common.constant.DeliveryOrderStatus; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.thirdparty.cs340.config.Cs340Config; +import com.ruoyi.thirdparty.cs340.domain.body.ByTemplateCreateOrder; +import com.ruoyi.thirdparty.cs340.domain.body.Cs340OnSaleInfoBody; +import com.ruoyi.thirdparty.cs340.domain.dto.*; +import com.ruoyi.thirdparty.cs340.domain.result.*; +import com.ruoyi.thirdparty.cs340.service.Cs340Service; +import com.ruoyi.thirdparty.cs340.utils.RSAUtils; +import com.ruoyi.thirdparty.yyyouping.utils.common.JacksonUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * https://www.yuque.com/laobi-hrwiq/gzgf3p/qlcuu7b8goar5dg7 + */ +@Slf4j +@Service +public class Cs340ServiceImpl implements Cs340Service { + + // 线程安全的日期格式化器(替代 SimpleDateFormat,避免并发导致签名异常) + private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Autowired + private Cs340Config cs340Config; + + @Autowired + private TtDeliveryRecordMapper deliveryRecordMapper; + + @Autowired + private TtBoxRecordsMapper boxRecordsMapper; + + @Override + public Cs340CheckTradeUrlResult checkTradeUrl(String tradeUrl) { + String url = cs340Config.getBaseUrl() + "/v1/api/checkTradeUrl"; + // 构建请求头 + Map headers = new HashMap<>(); + + CheckTradeUrlDto dto = new CheckTradeUrlDto(); + dto.setTimestamp(LocalDateTime.now().format(DTF)); + dto.setAppKey(cs340Config.getAppKey()); + dto.setTradeLinks(tradeUrl); + String body = JSON.toJSONString(dto); + dto.setSign(generateSign(body, cs340Config.getPrivateKey())); + body = JSON.toJSONString(dto); + String result = HttpUtils.sendPostJSONString(url, body, headers); + log.info("body is {}, result is {}", body, result); + // 将JSON转换为对象 + // 定义V5ItemResult的泛型类型 + TypeReference> typeRef = + new TypeReference>() {}; + // 使用FastJson将JSON字符串转换为对象 + Cs340Result result1 = JSONObject.parseObject(result, typeRef); + if (result1.getCode() != 0) { + return null; + } + return result1.getData(); + } + + @Override + public Cs340MerchantBalanceResult checkMerchantBalance() { + String url = cs340Config.getBaseUrl() + "/v1/api/getAssetsInfo"; + // 构建请求头 + Map headers = new HashMap<>(); + + CheckMerchantBalanceDto dto = new CheckMerchantBalanceDto(); + dto.setTimestamp(LocalDateTime.now().format(DTF)); + dto.setAppKey(cs340Config.getAppKey()); + String body = JSON.toJSONString(dto); + dto.setSign(generateSign(body, cs340Config.getPrivateKey())); + body = JSON.toJSONString(dto); + String result = HttpUtils.sendPostJSONString(url, body, headers); + log.info("body is {}, result is {}", body, result); + // 将JSON转换为对象 + // 定义V5ItemResult的泛型类型 + TypeReference> typeRef = + new TypeReference>() {}; + // 使用FastJson将JSON字符串转换为对象 + Cs340Result result1 = JSONObject.parseObject(result, typeRef); + if (result1.getCode() != 0) { + return null; + } + return result1.getData(); + } + + @Override + public Cs340Result createOrder(ByTemplateCreateOrder order) { + String url = cs340Config.getBaseUrl() + "/v1/api/byTemplateCreateOrder"; + // 构建请求头 + Map headers = new HashMap<>(); + + ByTemplateCreateOrderDto dto = new ByTemplateCreateOrderDto(); + BeanUtils.copyProperties(order, dto); + dto.setTimestamp(LocalDateTime.now().format(DTF)); + dto.setAppKey(cs340Config.getAppKey()); + String body = JSON.toJSONString(dto); + dto.setSign(generateSign(body, cs340Config.getPrivateKey())); + body = JSON.toJSONString(dto); + String result = HttpUtils.sendPostJSONString(url, body, headers); + log.info("body is {}, result is {}", body, result); + // 将JSON转换为对象 + TypeReference> typeRef = + new TypeReference>() {}; + return JSONObject.parseObject(result, typeRef); + } + + @Override + public Cs340Result queryOrderStatus(String orderNo) { + String url = cs340Config.getBaseUrl() + "/v1/api/orderStatus"; + // 构建请求头 + Map headers = new HashMap<>(); + + Cs340OrderStatusDto dto = new Cs340OrderStatusDto(); + dto.setOrderNo(orderNo); + dto.setTimestamp(LocalDateTime.now().format(DTF)); + dto.setAppKey(cs340Config.getAppKey()); + String body = JSON.toJSONString(dto); + dto.setSign(generateSign(body, cs340Config.getPrivateKey())); + body = JSON.toJSONString(dto); + String result = HttpUtils.sendPostJSONString(url, body, headers); + log.info("body is {}, result is {}", body, result); + // 将JSON转换为对象 + TypeReference> typeRef = + new TypeReference>() {}; + return JSONObject.parseObject(result, typeRef); + } + + @Override + public BatchGetOnSaleCommodityInfoResult queryOnSaleInfo(Cs340OnSaleInfoBody onSaleInfoBody) { + List results = batchGetOnSaleCommodityInfoResult(Collections.singletonList(onSaleInfoBody.getMarketHashName())); + if (CollectionUtils.isEmpty(results)) { + return null; + } + return results.get(0); + } + + @Override + public AjaxResult notify(Cs340CallBackNotifyInfo notify) { + log.info("收到订单更改回调通知。入参:{}", notify.toString()); + String callBackInfo = notify.getCallBackInfo(); + String messageNo = notify.getMessageNo(); + String sign = notify.getSign(); + Map params = new HashMap<>(); + params.put("messageNo", messageNo); + params.put("callBackInfo", callBackInfo); + // 第一步:检查参数是否已经排序 + String[] keys = params.keySet().toArray(new String[0]); + Arrays.sort(keys); + // 第二步:把所有参数名和参数值串在一起 + StringBuilder stringBuilder = new StringBuilder(); + for (String key : keys) { + Object value = params.get(key); + if (value != null) { + stringBuilder.append(key).append(value); + } + } + log.info("stringBuilder:{}", stringBuilder); + boolean b = false; + try { + b = RSAUtils.verifyByPublicKey(stringBuilder.toString().getBytes(StandardCharsets.UTF_8), cs340Config.getCallbackPublicKey(), sign); + } catch (Exception e) { + log.error("notify happen excepiton is {}", e.getMessage(), e); + } + Cs340CallBackResult result = new Cs340CallBackResult(); + result.setMessageNo(messageNo); + if (!b) { + log.info("b is :{}, cs340Config.getPublicKey() is {}", b, cs340Config.getCallbackPublicKey()); + return AjaxResult.success("verifyByPublicKey fail", result); + } + ObjectMapper objectMapper = new ObjectMapper(); + Cs340CallBackInfo info; + try { + // callBackInfo 是 JSON 字符串,需用 readValue 解析;convertValue 只能处理已解析的 Map/对象 + info = objectMapper.readValue(callBackInfo, Cs340CallBackInfo.class); + } catch (Exception e) { + log.error("[cs340-notify] 解析 callBackInfo 失败, callBackInfo={}", callBackInfo, e); + return AjaxResult.success("callBackInfo parse fail", result); + } + // 获取提货订单数据 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TtDeliveryRecord::getOrderId, info.getOrderNo()); + TtDeliveryRecord ttDeliveryRecord = deliveryRecordMapper.selectOne(queryWrapper); + if (Objects.equals(info.getOrderStatus(), 340)) { + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_COMPLETE.getCode()); + ttDeliveryRecord.setMessage(DeliveryOrderStatus.ORDER_COMPLETE.getMsg()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + deliveryRecordMapper.updateById(ttDeliveryRecord); + // 联动:订单完成后将对应 box_records 标为「已经提货」,不再在背包 / 提货背包展示,不可分解 / 转赠 + markBoxAsDelivered(ttDeliveryRecord.getBoxRecordsId()); + } else if (Objects.equals(info.getOrderStatus(), 280)) { + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_CANCEL.getCode()); + ttDeliveryRecord.setMessage(DeliveryOrderStatus.ORDER_CANCEL.getMsg()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + deliveryRecordMapper.updateById(ttDeliveryRecord); + } + log.info("验签结果:{}", b); + result.setFlag(true); + return AjaxResult.success(result); + } + + /** + * 将提货记录对应的 box_records 状态从 APPLY_DELIVERY(2) 走到 DELIVERY_YET(3)。 + * 仅在当前状态确为「申请提货」时才写入,避免覆盖其他状态。 + */ + private void markBoxAsDelivered(Long boxRecordsId) { + if (boxRecordsId == null) { + return; + } + TtBoxRecords box = boxRecordsMapper.selectById(boxRecordsId); + if (box != null && TtboxRecordStatus.APPLY_DELIVERY.getCode().equals(box.getStatus())) { + box.setStatus(TtboxRecordStatus.DELIVERY_YET.getCode()); + box.setUpdateTime(DateUtils.getNowDate()); + boxRecordsMapper.updateById(box); + } + } + + @Override + public List batchGetOnSaleCommodityInfoResult(List templateHashNames) { + if (CollectionUtils.isEmpty(templateHashNames)) { + return Collections.emptyList(); + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + log.error("sleep happen exception"); + } + String url = cs340Config.getBaseUrl() + "/v1/api/batchGetOnSaleCommodityInfo"; + // 构建请求头 + Map headers = new HashMap<>(); + List requestList = new ArrayList<>(); + for (String templateHashName : templateHashNames) { + CommodityInfoRequestListDto dto = new CommodityInfoRequestListDto(); + dto.setTemplateHashName(templateHashName); + requestList.add(dto); + } + + BatchGetOnSaleCommodityInfoDto dto = new BatchGetOnSaleCommodityInfoDto(); + dto.setTimestamp(LocalDateTime.now().format(DTF)); + dto.setAppKey(cs340Config.getAppKey()); + dto.setRequestList(requestList); + String body = JSON.toJSONString(dto); + dto.setSign(generateSign(body, cs340Config.getPrivateKey())); + body = JSON.toJSONString(dto); + log.info("[cs340-batchGetOnSale] 请求饰品数={}", templateHashNames.size()); + String result = HttpUtils.sendPostJSONString(url, body, headers); + // 将JSON转换为对象 + // 定义V5ItemResult的泛型类型 + TypeReference>> typeRef = + new TypeReference>>() {}; + // 使用FastJson将JSON字符串转换为对象 + Cs340Result> result1; + try { + result1 = JSONObject.parseObject(result, typeRef); + } catch (Exception e) { + log.error("[cs340-batchGetOnSale] JSON解析失败, result={}", result, e); + return Collections.emptyList(); + } + if (result1 == null) { + log.warn("[cs340-batchGetOnSale] 解析结果为null, 原始响应={}", result); + return Collections.emptyList(); + } + if (result1.getCode() != 0) { + log.warn("[cs340-batchGetOnSale] cs2pifa返回错误, code={}, msg={}", result1.getCode(), result1.getMsg()); + return Collections.emptyList(); + } + if (result1.getData() == null || CollectionUtils.isEmpty(result1.getData())) { + log.info("[cs340-batchGetOnSale] cs2pifa返回数据为空, 数量={}", templateHashNames.size()); + return Collections.emptyList(); + } + return result1.getData(); + } + + @Override + public String queryTemplateDownloadUrl() { + String url = cs340Config.getBaseUrl() + "/v1/api/templateQuery"; + Map headers = new HashMap<>(); + // templateQuery 仅需通用请求参数(appKey/timestamp/sign),复用 CheckMerchantBalanceDto 结构 + CheckMerchantBalanceDto dto = new CheckMerchantBalanceDto(); + dto.setTimestamp(LocalDateTime.now().format(DTF)); + dto.setAppKey(cs340Config.getAppKey()); + String body = JSON.toJSONString(dto); + dto.setSign(generateSign(body, cs340Config.getPrivateKey())); + body = JSON.toJSONString(dto); + String result = HttpUtils.sendPostJSONString(url, body, headers); + log.info("[cs340-templateQuery] body={}, result={}", body, result); + TypeReference> typeRef = new TypeReference>() {}; + Cs340Result result1; + try { + result1 = JSONObject.parseObject(result, typeRef); + } catch (Exception e) { + log.error("[cs340-templateQuery] JSON解析失败, result={}", result, e); + return null; + } + if (result1 == null) { + log.warn("[cs340-templateQuery] 解析结果为null, 原始响应={}", result); + return null; + } + if (result1.getCode() != 0) { + log.warn("[cs340-templateQuery] cs2pifa返回错误, code={}, msg={}", result1.getCode(), result1.getMsg()); + return null; + } + return result1.getData(); + } + + @Override + public List queryAllTemplates() { + String downloadUrl = queryTemplateDownloadUrl(); + if (StringUtils.isBlank(downloadUrl)) { + log.warn("[cs340-templateQuery] 未获取到模板下载地址"); + return Collections.emptyList(); + } + log.info("[cs340-templateQuery] 模板下载地址={}", downloadUrl); + String json = HttpUtils.sendGet(downloadUrl); + if (StringUtils.isBlank(json)) { + log.warn("[cs340-templateQuery] 模板文件下载为空, url={}", downloadUrl); + return Collections.emptyList(); + } + try { + List list = JSON.parseArray(json, Cs340TemplateItem.class); + log.info("[cs340-templateQuery] 解析模板数量={}", list == null ? 0 : list.size()); + return list == null ? Collections.emptyList() : list; + } catch (Exception e) { + log.error("[cs340-templateQuery] 模板文件解析失败, url={}", downloadUrl, e); + return Collections.emptyList(); + } + } + + /** + * 生成认证sign信息 + */ + private String generateSign(String body, String privateKey) { + Map map = JSONObject.parseObject(body, Map.class); + String[] keys = map.keySet().toArray(new String[0]); + Arrays.sort(keys); + StringBuilder stringBuilder = new StringBuilder(); + for (String key : keys) { + if (Objects.equals(key, "sign")) { + continue; + } + Object value = map.get(key); + if (value != null) { + stringBuilder.append(key).append(JacksonUtils.writeValueAsString(value)); + } + } + log.debug("[cs340-sign] 签名原文={}", stringBuilder); + try { + return RSAUtils.signByPrivateKey(stringBuilder.toString().getBytes(StandardCharsets.UTF_8), privateKey); + } catch (Exception e) { + log.error("签名失败 is {}", e.getMessage(), e); + throw new RuntimeException(e); + } + } + + public static void main(String[] args) { + String body = "{\"sign\":\"4LL1O++Btn5JgZ6kHuJAN7dEG4cgLd0goo+Cb4wld0ygpU8lU+73mLxXYtN81ApAhWuw9zn+ENrtc2KcrL6XgHMSG8JC/5NjHCGO/3jDUjAac3YMh5vdQjsqhK1IoxdrOXM9QUyUj2kU9CA4eRey5wBx+BjbzHkSXXt6tOk9pDkeg56wuKR9L3g1BldcwBEacW2fU4pfEW9vpJC9QTCU/FAT0JXMTpYOaeIn+uf9eQRlRNguI1eUoWJAiGQgVQ/TlFlkdVqeE0pHp3Ntod2r+pkb4vCKrMDI6HW28XsM0uMupOfBUL8zRNFEOvLtswJduVo5r5Fd1ysHFcFywHOT0A==\",\"timestamp\":\"2023-01-17 16:15:00\",\"appKey\":\"123456\",\"merchantOrderNo\":\"20230112061\",\"tradeLinks\":\"https://steamcommunity.com/tradeoffer/new/?partner=323393236&token=vP12QeQA\",\"commodityId\":\"427620\",\"purchasePrice\":\"0.07\"}"; + ByTemplateCreateOrderDto dtp = JSON.parseObject(body, ByTemplateCreateOrderDto.class); + Cs340ServiceImpl cs340Service = new Cs340ServiceImpl(); + Cs340OnSaleInfoBody cs340OnSaleInfoBody = new Cs340OnSaleInfoBody(); + cs340OnSaleInfoBody.setMarketHashName("Sticker | tabseN | Antwerp 2022"); + BatchGetOnSaleCommodityInfoResult response = cs340Service.queryOnSaleInfo(cs340OnSaleInfoBody); + System.out.println(response != null ? response.toString() : "null"); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/task/Cs340ItemPullTask.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/task/Cs340ItemPullTask.java new file mode 100644 index 0000000..34a76d8 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/task/Cs340ItemPullTask.java @@ -0,0 +1,297 @@ +package com.ruoyi.thirdparty.cs340.task; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.cs340.domain.result.BatchGetOnSaleCommodityInfoResult; +import com.ruoyi.thirdparty.cs340.domain.result.Cs340TemplateItem; +import com.ruoyi.thirdparty.cs340.domain.result.SaleTemplateResponse; +import com.ruoyi.thirdparty.cs340.service.Cs340Service; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Component("Cs340ItemPullTask") +public class Cs340ItemPullTask { + + @Autowired + private TtOrnamentService ornamentsService; + + @Autowired + private Cs340Service cs340Service; + + @Autowired + private ISysConfigService configService; + + public void getCs340OrnamentsData() { + log.info("cs2pifa拉取最新饰品数据任务执行开始..."); + String usePricePremiumRateStr = configService.selectConfigByKey("usePricePremiumRate"); + BigDecimal usePricePremiumRate = new BigDecimal(usePricePremiumRateStr); + + String ZBTParitiesStr = configService.selectConfigByKey("ZBTParities"); + BigDecimal parities = new BigDecimal(ZBTParitiesStr); + + // 0. 静默补充第三方新饰品:拉取全部模板,与本地做 diff,缺失的默认下架插入。 + // 随后由下面的价格同步阶段根据“是否在售”自动决定上架/下架,前端无感知。 + syncNewTemplates(); + + // 查询数据库(包含刚补充的新饰品) + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + List ttOrnamentList = ornamentsService.list(wrapper); + if (CollectionUtils.isEmpty(ttOrnamentList)) { + log.info("ttOrnamentList is empty"); + return; + } + List> splitOrnamentList = ListUtils.partition(ttOrnamentList, 200); + for (List list : splitOrnamentList) { + List templateHashNames = new ArrayList<>(); + Map templateHashMap = new HashMap<>(); + for (TtOrnament ornament : list) { + if (ornament == null || StringUtils.isBlank(ornament.getMarketHashName())) { + continue; + } + templateHashNames.add(ornament.getMarketHashName()); + templateHashMap.put(ornament.getMarketHashName(), ornament); + } + List infos; + try { + infos = cs340Service.batchGetOnSaleCommodityInfoResult(templateHashNames); + } catch (Exception e) { + log.error("cs340 batchGetOnSaleCommodityInfo 调用异常,跳过本批次以避免误下架, size={}, err={}", templateHashNames.size(), e.getMessage(), e); + continue; + } + // 保底:接口整体返回为空时,视为接口异常,跳过本批次,避免把整批饰品误下架 + if (CollectionUtils.isEmpty(infos)) { + log.warn("cs340 batchGetOnSaleCommodityInfo 返回为空,跳过本批次以避免误下架, size={}", templateHashNames.size()); + continue; + } + + // is_putaway: '0'=上架, '1'=下架 + Set onSaleSet = new HashSet<>(); + List updateList = new ArrayList<>(); + // 诊断用:统计第三方 iconUrl 返回为空的数量,用于判断是否需要改走其他取图接口 + int blankIconCount = 0; + + // ① CS340 返回有数据 -> 上架 + 刷新价格/库存 + for (BatchGetOnSaleCommodityInfoResult info : infos) { + if (info == null || info.getSaleCommodityResponse() == null || info.getSaleTemplateResponse() == null) { + continue; + } + String hashName = info.getSaleTemplateResponse().getTemplateHashName(); + if (onSaleSet.contains(hashName)) { + log.info("templateHashName same is {}", hashName); + } + onSaleSet.add(hashName); + TtOrnament ttOrnament = templateHashMap.get(hashName); + if (ttOrnament == null) { + continue; + } + BigDecimal price = new BigDecimal(info.getSaleCommodityResponse().getMinSellPrice()); + BigDecimal storePrice = price.multiply(parities).multiply(usePricePremiumRate); + if (storePrice.compareTo(new BigDecimal("99999999.99")) > 0) { + continue; + } + int quantity = info.getSaleCommodityResponse().getSellNum() != null + ? Integer.parseInt(info.getSaleCommodityResponse().getSellNum()) : 0; + + boolean priceChanged = ttOrnament.getPrice() == null + || !Objects.equals(ttOrnament.getPrice().toString(), info.getSaleCommodityResponse().getMinSellPrice()); + boolean qtyChanged = ttOrnament.getQuantity() == null + || !Objects.equals(ttOrnament.getQuantity(), quantity); + + // 磨损度自愈:cs340 的 saleTemplateResponse.exteriorName 常返回空串, + // 历史数据里 exterior_name 因此为空,这里按 name 括号内容补齐(仅空值才回填,不覆盖已有值)。 + String exteriorName = StringUtils.isBlank(ttOrnament.getExteriorName()) + ? extractExteriorName(ttOrnament.getName()) : null; + + // 图片/稀有度/品质自愈:这三个字段就在已经拉回来的 saleTemplateResponse 里, + // 之前价格同步阶段直接丢掉了没入库,导致全表为空。 + // 统一口径:仅本地为空时才回填,不覆盖后台手改过的值。 + SaleTemplateResponse template = info.getSaleTemplateResponse(); + if (StringUtils.isBlank(template.getIconUrl())) { + blankIconCount++; + } + String imageUrl = fillIfBlank(ttOrnament.getImageUrl(), template.getIconUrl()); + String rarityName = fillIfBlank(ttOrnament.getRarityName(), template.getRarityName()); + String qualityName = fillIfBlank(ttOrnament.getQualityName(), template.getQualityName()); + + if (priceChanged || qtyChanged || exteriorName != null + || imageUrl != null || rarityName != null || qualityName != null) { + // 仅更新价格/库存/商城价/磨损度/图片/稀有度/品质/更新时间,构造只含目标字段的对象, + // 避免整条实体回写把后台手改的 use_price 等字段用旧快照覆盖掉。 + // 注意:此处不修改上架状态,新补齐的饰品默认下架,由运营后台决定是否上架。 + // 下面几个字段为 null 时,MP 默认 NOT_NULL 策略会跳过,不参与更新。 + TtOrnament update = TtOrnament.builder() + .marketHashName(ttOrnament.getMarketHashName()) + .price(price) + .quantity(quantity) + .storePrice(storePrice) + .exteriorName(exteriorName) + .imageUrl(imageUrl) + .rarityName(rarityName) + .qualityName(qualityName) + .updateTime(DateUtils.getNowDate()) + .build(); + updateList.add(update); + } + } + if (blankIconCount > 0) { + log.warn("cs340 本批次 iconUrl 为空 {} 条(共 {} 条),这部分饰品图片需改用 queryTemplateSaleByCategory 获取", + blankIconCount, infos.size()); + } + + // ② CS340 没返回的 -> 下架,商城不再展示 + for (TtOrnament ornament : list) { + if (ornament == null || StringUtils.isBlank(ornament.getMarketHashName())) { + continue; + } + if (onSaleSet.contains(ornament.getMarketHashName())) { + continue; + } + // 磨损度自愈:与在售分支同口径,已下架的饰品也要补 exterior_name + String exteriorName = StringUtils.isBlank(ornament.getExteriorName()) + ? extractExteriorName(ornament.getName()) : null; + boolean alreadyOff = "1".equals(ornament.getIsPutaway()) + && (ornament.getQuantity() == null || ornament.getQuantity() == 0); + if (alreadyOff && StringUtils.isBlank(exteriorName)) { + continue; + } + // 同样只构造下架所需字段,避免回写覆盖 use_price 等字段 + TtOrnament update = TtOrnament.builder() + .marketHashName(ornament.getMarketHashName()) + .exteriorName(exteriorName) + .updateTime(DateUtils.getNowDate()) + .build(); + if (!alreadyOff) { + update.setQuantity(0); + update.setIsPutaway("1"); // 下架 + } + updateList.add(update); + } + + if (CollectionUtils.isNotEmpty(updateList)) { + batchUpdateOrnaments(updateList); + } + } + + log.info("更新数据完成"); + } + + private void batchUpdateOrnaments(List updateList) { + ornamentsService.updateBatchById(updateList, updateList.size()); + log.info("更新饰品{}条", updateList.size()); + } + + /** + * 拉取 cs2pifa 全部模板列表,把本地不存在的饰品静默补充进来。 + * 新饰品默认下架(isPutaway='1'、quantity=0),由后续价格同步阶段根据第三方是否在售决定上下架, + * 因此商城/前端不会出现“空价格饰品”,做到无感知增量同步。 + */ + private void syncNewTemplates() { + log.info("cs2pifa补充新饰品开始..."); + List allTemplates = cs340Service.queryAllTemplates(); + if (CollectionUtils.isEmpty(allTemplates)) { + log.warn("cs2pifa未获取到模板列表,跳过补充新饰品"); + return; + } + + List existingHashNames = ornamentsService.selectOrnamentsMarketHashNameList(); + Set existingNormalized = existingHashNames.stream() + .filter(StringUtils::isNotBlank) + .map(h -> h.trim().toLowerCase()) + .collect(Collectors.toSet()); + + List newOrnaments = new ArrayList<>(); + Set addedNormalized = new HashSet<>(); + Date now = DateUtils.getNowDate(); + for (Cs340TemplateItem template : allTemplates) { + if (template == null || template.getId() == null || StringUtils.isBlank(template.getHashName())) { + continue; + } + String hashName = template.getHashName().trim(); + String normalized = hashName.toLowerCase(); + if (existingNormalized.contains(normalized) || addedNormalized.contains(normalized)) { + continue; + } + TtOrnament ornament = TtOrnament.builder().build(); + ornament.setId(template.getId()); + ornament.setMarketHashName(hashName); + ornament.setName(StringUtils.trim(template.getName())); + ornament.setTypeName(template.getTypeName()); + ornament.setTypeHashName(template.getTypeHashName()); + ornament.setExteriorName(extractExteriorName(template.getName())); // 从 name 的括号中提取磨损度 + ornament.setIsProprietaryProperty("1"); // 非本网站自定义道具 + ornament.setPrice(BigDecimal.ZERO); + ornament.setQuantity(0); + ornament.setIsPutaway("1"); // 默认下架,由价格同步阶段决定是否上架 + ornament.setCreateTime(now); + ornament.setUpdateTime(now); + newOrnaments.add(ornament); + addedNormalized.add(normalized); // 同一批内规范化去重 + } + + if (CollectionUtils.isEmpty(newOrnaments)) { + log.info("cs2pifa无新增饰品"); + return; + } + + List> batches = ListUtils.partition(newOrnaments, 500); + for (List batch : batches) { + ornamentsService.saveBatch(batch, batch.size()); + log.info("cs2pifa补充新饰品{}条", batch.size()); + } + log.info("cs2pifa补充新饰品完成,共{}条", newOrnaments.size()); + } + + /** + * 标准磨损度白名单:只有这 5 种才写入 exterior_name, + * 避免把 "StatTrak™"、"全息"、"闪亮" 等括号内容误当成磨损度写成脏数据。 + */ + private static final Set STANDARD_EXTERIORS = new HashSet<>(Arrays.asList( + "崭新出厂", "略有磨损", "久经沙场", "破损不堪", "战痕累累")); + + /** + * 从饰品名称中提取磨损度(exterior_name)。 + * 例如 "AWP | 蝮蛇迷彩(崭新出厂)" -> "崭新出厂", + * 也兼容英文括号 "AUG | 夜空沙暴 (略有磨损)" -> "略有磨损"。 + * 括号内不是标准磨损度(或无括号)时返回 null,不写入脏值。 + */ + public static String extractExteriorName(String name) { + if (StringUtils.isBlank(name)) { + return null; + } + String candidate = lastBracketContent(name, '(', ')'); + if (candidate == null) { + candidate = lastBracketContent(name, '(', ')'); + } + return candidate != null && STANDARD_EXTERIORS.contains(candidate) ? candidate : null; + } + + /** 取名称中最后一对指定括号内的内容,取不到返回 null */ + private static String lastBracketContent(String name, char left, char right) { + int l = name.lastIndexOf(left); + int r = name.lastIndexOf(right); + return l >= 0 && r > l ? name.substring(l + 1, r).trim() : null; + } + + /** + * 第三方字段回填口径:仅当本地为空、且第三方给了非空值时才返回待写入值; + * 否则返回 null,MP 默认 NOT_NULL 策略会跳过该字段,不会把后台手改的数据覆盖掉。 + * 注意 cs340 这几个字段常返回空串,必须用 isNotBlank 而不是 != null 判断。 + */ + private static String fillIfBlank(String localValue, String remoteValue) { + return StringUtils.isBlank(localValue) && StringUtils.isNotBlank(remoteValue) + ? remoteValue.trim() : null; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/utils/RSAUtils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/utils/RSAUtils.java new file mode 100644 index 0000000..b2b9cea --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/cs340/utils/RSAUtils.java @@ -0,0 +1,501 @@ +package com.ruoyi.thirdparty.cs340.utils; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.*; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * SHA256withRSA + * + * @author zhumiao + * + */ +public class RSAUtils { + + private static final Logger logger = LoggerFactory.getLogger(RSAUtils.class); + + // MAX_DECRYPT_BLOCK应等于密钥长度/8(1byte=8bit),所以当密钥位数为2048时,最大解密长度应为256. +// 128 对应 1024,256对应2048 + private static final int KEYSIZE = 2048; + + // RSA最大加密明文大小 + private static final int MAX_ENCRYPT_BLOCK = 117; + + // RSA最大解密密文大小 + private static final int MAX_DECRYPT_BLOCK = 256; + + // 不仅可以使用DSA算法,同样也可以使用RSA算法做数字签名 + private static final String KEY_ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; + + + // 默认种子 请根据自己的需要定义 + public static final String DEFAULT_SEED = "xxxxxxxxxxxxxxxxxx"; + + /** + * 目前固定公钥、私钥,有需求再改动 + */ + public static String PUBLIC_KEY_LZ = "***************"; + public static String PRIVATE_KEY_LZ = "*******************************************="; + + public static final String PUBLIC_KEY = "PublicKey"; + public static final String PRIVATE_KEY = "PrivateKey"; + + /** + * + * 生成密钥 + * + * @param seed 种子 + * + * @return 密钥对象 + * @throws Exception + * + */ + public static Map initKey(String seed) throws Exception { + logger.info("生成密钥"); + KeyPairGenerator keygen = KeyPairGenerator.getInstance(KEY_ALGORITHM); + SecureRandom secureRandom = new SecureRandom(); +// 如果指定seed,那么secureRandom结果是一样的,所以生成的公私钥也永远不会变 + secureRandom.setSeed(seed.getBytes()); +// Modulus size must range from 512 to 1024 and be a multiple of 64 + keygen.initialize(KEYSIZE, secureRandom); + KeyPair keys = keygen.genKeyPair(); + PrivateKey privateKey = keys.getPrivate(); + PublicKey publicKey = keys.getPublic(); + Map map = new HashMap<>(2); + map.put(PUBLIC_KEY, publicKey); + map.put(PRIVATE_KEY, privateKey); + return map; + } + + /** + * + * 生成默认密钥 + * + * + * @return 密钥对象 + * @throws Exception + * + */ + + public static Map initKey() throws Exception { + return initKey(DEFAULT_SEED); + } + + /** + * + * 取得私钥 + * + * @param keyMap + * + * @return + * @throws Exception + * + */ + public static String getPrivateKey(Map keyMap) throws Exception { + Key key = (Key) keyMap.get(PRIVATE_KEY); + // return encryptBASE64(key.getEncoded()); // base64加密私钥 + return base64ToStr(key.getEncoded()); // base64加密私钥 + } + + private static String base64ToStr(byte[] encoded) { + return java.util.Base64.getEncoder().encodeToString(encoded); + } + + /** + * + * 取得公钥 + * + * @param keyMap + * + * @return + * @throws Exception + * + */ + public static String getPublicKey(Map keyMap) throws Exception { + Key key = (Key) keyMap.get(PUBLIC_KEY); + // return encryptBASE64(key.getEncoded()); // base64加密公钥 + return base64ToStr(key.getEncoded()); // base64加密公钥 + } + + /** + * + * 用私钥对信息进行数字签名 + * + * @param data 加密数据 + * + * @param privateKey 私钥-base64加密的 + * + * @return + * + * @throws Exception + * + */ + public static String signByPrivateKey(byte[] data, String privateKey) throws Exception { + byte[] keyBytes = decryptBASE64(privateKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM); + PrivateKey priKey = factory.generatePrivate(keySpec);// 生成私钥 + // 用私钥对信息进行数字签名 + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initSign(priKey); + signature.update(data); + return encryptBASE64(signature.sign()); + + } + + /** + * + * BASE64Encoder 加密 + * + * @param data 要加密的数据 + * + * @return 加密后的字符串 + * + */ + private static String encryptBASE64(byte[] data) { + // BASE64Encoder encoder = new BASE64Encoder(); + // String encode = encoder.encode(data); + // return encode; + return new String(Base64.encodeBase64(data)); + } + + private static byte[] decryptBASE64(String data) { + // BASE64Decoder 每76个字符换行 + // BASE64Decoder decoder = new BASE64Decoder(); + // byte[] buffer = decoder.decodeBuffer(data); + // return buffer; + // codec 的 Base64 不换行 + return Base64.decodeBase64(data); + } + + // TODO rsa 2 对比是否更优 + public static boolean verifyByPublicKey(byte[] data, String publicKey, String sign) throws Exception { + byte[] keyBytes = decryptBASE64(publicKey); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + PublicKey pubKey = keyFactory.generatePublic(keySpec); + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initVerify(pubKey); + signature.update(data); + return signature.verify(decryptBASE64(sign)); // 验证签名 + } + + /** + * RSA公钥加密 + * + * @param str 加密字符串 + * @param publicKey 公钥 + * @return 密文 + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws InvalidKeyException + * @throws UnsupportedEncodingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws Exception 加密过程中的异常信息 + */ + public static String encryptByPublicKey(String str, String publicKey) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException { + // base64编码的公钥 + byte[] keyBytes = decryptBASE64(publicKey); + RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance(KEY_ALGORITHM) + .generatePublic(new X509EncodedKeySpec(keyBytes)); + // RSA加密 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, pubKey); + logger.info(publicKey); + logger.info("provider: {}", cipher.getProvider().getClass().getName()); + byte[] data = str.getBytes("UTF-8"); + // 加密时超过117字节就报错。为此采用分段加密的办法来加密 + byte[] enBytes = null; + for (int i = 0; i < data.length; i += MAX_ENCRYPT_BLOCK) { + // 注意要使用2的倍数,否则会出现加密后的内容再解密时为乱码 + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_ENCRYPT_BLOCK)); + enBytes = ArrayUtils.addAll(enBytes, doFinal); + } + logger.info(enBytes.length + ""); + String outStr = encryptBASE64(enBytes); + return outStr; + } + + /** + * RSA私钥加密 + * + * @param str 加密字符串 + * @param privateKey 公钥 + * @return 密文 + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws InvalidKeyException + * @throws UnsupportedEncodingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws Exception 加密过程中的异常信息 + */ + public static String encryptByPrivateKey(String str, String privateKey) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException { + // base64编码的公钥 + byte[] keyBytes = decryptBASE64(privateKey); + RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(KEY_ALGORITHM) + .generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + // RSA加密 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, priKey); + + byte[] data = str.getBytes("UTF-8"); + // 加密时超过117字节就报错。为此采用分段加密的办法来加密 + byte[] enBytes = null; + for (int i = 0; i < data.length; i += MAX_ENCRYPT_BLOCK) { + // 注意要使用2的倍数,否则会出现加密后的内容再解密时为乱码 + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_ENCRYPT_BLOCK)); + enBytes = ArrayUtils.addAll(enBytes, doFinal); + } + String outStr = encryptBASE64(enBytes); + return outStr; + } + + /** + * 读取公钥 + * + * @param publicKeyPath + * @return + */ + public static PublicKey readPublic(String publicKeyPath) { + if (publicKeyPath != null) { + try (FileInputStream bais = new FileInputStream(publicKeyPath)) { + CertificateFactory certificatefactory = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) certificatefactory.generateCertificate(bais); + return cert.getPublicKey(); + } catch (CertificateException e) { + logger.error(e.getMessage(), e); + } catch (FileNotFoundException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + return null; + } + + /** + * 读取私钥 + * + * + * @return + */ + public static PrivateKey readPrivate(String privateKeyPath, String privateKeyPwd) { + if (privateKeyPath == null || privateKeyPwd == null) { + return null; + } + try (InputStream stream = new FileInputStream(new File(privateKeyPath));) { + // 获取JKS 服务器私有证书的私钥,取得标准的JKS的 KeyStore实例 + KeyStore store = KeyStore.getInstance("JKS");// JKS,二进制格式,同时包含证书和私钥,一般有密码保护;PKCS12,二进制格式,同时包含证书和私钥,一般有密码保护。 + // jks文件密码,根据实际情况修改 + store.load(stream, privateKeyPwd.toCharArray()); + // 获取jks证书别名 + Enumeration en = store.aliases(); + String pName = null; + while (en.hasMoreElements()) { + String n = (String) en.nextElement(); + if (store.isKeyEntry(n)) { + pName = n; + } + } + // 获取证书的私钥 + PrivateKey key = (PrivateKey) store.getKey(pName, privateKeyPwd.toCharArray()); + return key; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return null; + } + + /** + * RSA私钥解密 + * + * @param encryStr 加密字符串 + * @param privateKey 私钥 + * @return 铭文 + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws InvalidKeyException + * @throws Exception 解密过程中的异常信息 + */ + public static String decryptByPrivateKey(String encryStr, String privateKey) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, + BadPaddingException, InvalidKeyException, UnsupportedEncodingException { + // base64编码的私钥 + byte[] decoded = decryptBASE64(privateKey); + RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(KEY_ALGORITHM) + .generatePrivate(new PKCS8EncodedKeySpec(decoded)); + // RSA解密 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, priKey); + logger.info(privateKey); + logger.info("provider: {}" , cipher.getProvider().getClass().getName()); + // 64位解码加密后的字符串 + byte[] data = decryptBASE64(encryStr); + logger.info(data.length + ""); + // 解密时超过128字节报错。为此采用分段解密的办法来解密 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < data.length; i += MAX_DECRYPT_BLOCK) { + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_DECRYPT_BLOCK)); + sb.append(new String(doFinal)); + } + return sb.toString(); + } + + /** + * RSA公钥解密 + * + * @param encryStr 加密字符串 + * + * @return 铭文 + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws InvalidKeyException + * @throws Exception 解密过程中的异常信息 + */ + public static String decryptByPublicKey(String encryStr, String publicKey) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, + BadPaddingException, InvalidKeyException { + // base64编码的私钥 + byte[] decoded = decryptBASE64(publicKey); + RSAPublicKey priKey = (RSAPublicKey) KeyFactory.getInstance(KEY_ALGORITHM) + .generatePublic(new X509EncodedKeySpec(decoded)); + // RSA解密 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, priKey); + + // 64位解码加密后的字符串 + byte[] data = decryptBASE64(encryStr); + // 解密时超过128字节报错。为此采用分段解密的办法来解密 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < data.length; i += MAX_DECRYPT_BLOCK) { + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_DECRYPT_BLOCK)); + sb.append(new String(doFinal)); + } + + return sb.toString(); + } + + /** + * 加密 + * @param key + * @param data + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws InvalidKeyException + * @throws IOException + */ + public static String testEncrypt(String key,String data) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException{ + byte[] decode = Base64.decodeBase64(key); + PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(decode); + KeyFactory kf = KeyFactory.getInstance(KEY_ALGORITHM); + PrivateKey generatePrivate = kf.generatePrivate(pkcs8EncodedKeySpec); + Cipher ci = Cipher.getInstance(KEY_ALGORITHM); + ci.init(Cipher.ENCRYPT_MODE, generatePrivate); + + byte[] bytes = data.getBytes(); + int inputLen = bytes.length; + int offLen = 0;//偏移量 + int i = 0; + ByteArrayOutputStream bops = new ByteArrayOutputStream(); + while(inputLen - offLen > 0){ + byte [] cache; + if(inputLen - offLen > 117){ + cache = ci.doFinal(bytes, offLen,117); + }else{ + cache = ci.doFinal(bytes, offLen,inputLen - offLen); + } + bops.write(cache); + i++; + offLen = 117 * i; + } + bops.close(); + byte[] encryptedData = bops.toByteArray(); + String encodeToString = Base64.encodeBase64String(encryptedData); + return encodeToString; + } + + + + + /** + * 解密 + * @param key + * @param data + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeyException + * @throws NoSuchPaddingException + * @throws InvalidKeySpecException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws IOException + */ + public static String testDecrypt(String key,String data) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, InvalidKeySpecException, IllegalBlockSizeException, BadPaddingException, IOException{ + byte[] decode = Base64.decodeBase64(key); + // PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(decode); //java底层 RSA公钥只支持X509EncodedKeySpec这种格式 + X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(decode); + KeyFactory kf = KeyFactory.getInstance(KEY_ALGORITHM); + PublicKey generatePublic = kf.generatePublic(x509EncodedKeySpec); + Cipher ci = Cipher.getInstance(KEY_ALGORITHM); + ci.init(Cipher.DECRYPT_MODE,generatePublic); + + int inputLen = data.getBytes().length; + byte[] bytes = data.getBytes(); + int offLen = 0; + int i = 0; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + while(inputLen - offLen > 0){ + byte[] cache; + if(inputLen - offLen > 128){ + cache = ci.doFinal(bytes,offLen,128); + }else{ + cache = ci.doFinal(bytes,offLen,inputLen - offLen); + } + byteArrayOutputStream.write(cache); + i++; + offLen = 128 * i; + + } + byteArrayOutputStream.close(); + byte[] byteArray = byteArrayOutputStream.toByteArray(); + return new String(byteArray); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/config/FourSmsConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/config/FourSmsConfig.java new file mode 100644 index 0000000..733f0bd --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/config/FourSmsConfig.java @@ -0,0 +1,41 @@ +package com.ruoyi.thirdparty.foursms.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 4SMS 短信服务配置 + * 文档:https://4sms.cn + * 鉴权方式:Header 签名鉴权(X-Api-Key + X-Timestamp + X-Signature),签名算法 HMAC-SHA256 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "foursms") +public class FourSmsConfig { + + /** + * Base URL,默认 https://4sms.cn + */ + private String baseUrl = "https://4sms.cn"; + + /** + * API Key(平台分配,形如 bk_xxxxxxxx),作为 X-Api-Key 请求头及签名 payload 的 key_id + */ + private String keyId; + + /** + * API 密钥(在「API 密钥」页面获取的 key_secret),用于 HMAC-SHA256 计算签名 + */ + private String keySecret; + + /** + * 默认短信模板 ID(发送验证码使用),可在「获取签名与模板」接口查询 + */ + private Integer templateId; + + /** + * 默认签名 ID(可选,不传则使用该模板默认签名) + */ + private Integer signatureId; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/controller/FourSmsController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/controller/FourSmsController.java new file mode 100644 index 0000000..cad8085 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/controller/FourSmsController.java @@ -0,0 +1,39 @@ +package com.ruoyi.thirdparty.foursms.controller; + +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.thirdparty.foursms.service.FourSmsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 4SMS 短信平台测试接口 + */ +@RestController +@RequestMapping("/admin/foursms") +public class FourSmsController extends BaseController { + + @Autowired + private FourSmsService fourSmsService; + + /** + * 测试发送验证码短信 + */ + @Anonymous + @GetMapping("/test") + public AjaxResult test(String mobile, String code) { + return success(fourSmsService.sendVerifyCode(mobile, code)); + } + + /** + * 查询当前账号可用的签名与模板列表 + */ + @Anonymous + @GetMapping("/templates") + public AjaxResult templates() { + return success(fourSmsService.listTemplates()); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/service/FourSmsService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/service/FourSmsService.java new file mode 100644 index 0000000..f67feac --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/service/FourSmsService.java @@ -0,0 +1,36 @@ +package com.ruoyi.thirdparty.foursms.service; + +import java.util.Map; + +/** + * 4SMS 短信服务接口 + */ +public interface FourSmsService { + + /** + * 发送验证码短信(使用配置中的默认模板与签名) + * + * @param mobile 接收手机号 + * @param code 验证码 + * @return "0" 表示发送成功;否则返回错误信息 + */ + String sendVerifyCode(String mobile, String code); + + /** + * 发送短信(指定模板与签名) + * + * @param mobile 接收手机号 + * @param templateId 模板 ID + * @param signatureId 签名 ID(可为 null,为空则使用模板默认签名) + * @param params 模板变量键值对,如 {"code":"123456"} + * @return "0" 表示发送成功;否则返回错误信息 + */ + String sendSms(String mobile, Integer templateId, Integer signatureId, Map params); + + /** + * 获取当前账号可用的签名与模板列表(原始响应) + * + * @return 响应体 JSON 字符串 + */ + String listTemplates(); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/service/impl/FourSmsServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/service/impl/FourSmsServiceImpl.java new file mode 100644 index 0000000..729842e --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/service/impl/FourSmsServiceImpl.java @@ -0,0 +1,101 @@ +package com.ruoyi.thirdparty.foursms.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.thirdparty.foursms.config.FourSmsConfig; +import com.ruoyi.thirdparty.foursms.service.FourSmsService; +import com.ruoyi.thirdparty.foursms.util.FourSmsUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * 4SMS 短信服务实现 + *

+ * 发送验证码:POST /api/v1/sms/send + * 查询模板:GET /api/v1/templates + * 响应中 code=0 且 data.status=success 视为发送成功。 + */ +@Slf4j +@Service +public class FourSmsServiceImpl implements FourSmsService { + + /** 发送短信接口路径 */ + private static final String SEND_PATH = "/api/v1/sms/send"; + + /** 获取签名与模板接口路径 */ + private static final String TEMPLATES_PATH = "/api/v1/templates"; + + @Autowired + private FourSmsConfig fourSmsConfig; + + @Override + public String sendVerifyCode(String mobile, String code) { + Map params = new HashMap<>(4); + params.put("code", code); + // 模板 param_schema 为 "code,time",time 为验证码有效分钟数(与 Redis 缓存 5 分钟一致) + params.put("time", "5"); + return sendSms(mobile, fourSmsConfig.getTemplateId(), fourSmsConfig.getSignatureId(), params); + } + + @Override + public String sendSms(String mobile, Integer templateId, Integer signatureId, Map params) { + if (StringUtils.isEmpty(fourSmsConfig.getKeyId()) || StringUtils.isEmpty(fourSmsConfig.getKeySecret())) { + log.error("[4SMS] 未配置 keyId / keySecret,请检查 foursms 配置"); + return "CONFIG_ERROR"; + } + if (templateId == null) { + log.error("[4SMS] 未配置 templateId,请检查 foursms 配置或调用参数"); + return "TEMPLATE_ERROR"; + } + + // 构造请求体 + JSONObject body = new JSONObject(); + body.put("mobile", mobile); + body.put("template_id", templateId); + if (signatureId != null) { + body.put("signature_id", signatureId); + } + body.put("params", params == null ? new HashMap<>() : params); + + String result = FourSmsUtil.post(fourSmsConfig.getBaseUrl(), SEND_PATH, + fourSmsConfig.getKeyId(), fourSmsConfig.getKeySecret(), body.toJSONString()); + log.info("[4SMS] 发送短信 mobile={}, templateId={}, signatureId={}, response={}", + mobile, templateId, signatureId, result); + + if (StringUtils.isEmpty(result)) { + return "EMPTY_RESPONSE"; + } + try { + JSONObject json = JSONObject.parseObject(result); + Integer respCode = json.getInteger("code"); + if (respCode != null && respCode == 0) { + JSONObject data = json.getJSONObject("data"); + if (data != null && "success".equalsIgnoreCase(data.getString("status"))) { + return "0"; + } + } + // 返回错误信息,便于上层判断 + String message = json.getString("message"); + return StringUtils.isNotEmpty(message) ? message : result; + } catch (Exception e) { + log.error("[4SMS] 解析响应异常 mobile={}, response={}", mobile, result, e); + return "PARSE_ERROR"; + } + } + + @Override + public String listTemplates() { + if (StringUtils.isEmpty(fourSmsConfig.getKeyId()) || StringUtils.isEmpty(fourSmsConfig.getKeySecret())) { + log.error("[4SMS] 未配置 keyId / keySecret,请检查 foursms 配置"); + return "CONFIG_ERROR"; + } + String result = FourSmsUtil.get(fourSmsConfig.getBaseUrl(), TEMPLATES_PATH, + fourSmsConfig.getKeyId(), fourSmsConfig.getKeySecret()); + log.info("[4SMS] 查询模板列表 response={}", result); + return result; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/util/FourSmsUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/util/FourSmsUtil.java new file mode 100644 index 0000000..f5c1076 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/foursms/util/FourSmsUtil.java @@ -0,0 +1,119 @@ +package com.ruoyi.thirdparty.foursms.util; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HexFormat; + +/** + * 4SMS 短信平台工具类 + *

+ * 负责 HMAC-SHA256 签名计算与 HTTP 请求发送。 + * 签名算法:payload = key_id + ":" + timestamp + ":" + method + ":" + path, + * signature = HMAC_SHA256(key_secret, payload),hex 小写。 + * 注意:path 不包含查询参数;method 需大写;签名有效期 5 分钟。 + */ +public class FourSmsUtil { + + /** 复用单例 HttpClient */ + private static final HttpClient CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + private FourSmsUtil() { + } + + /** + * 计算 HMAC-SHA256 签名(hex 小写) + * + * @param keySecret 密钥 + * @param payload 待签名内容 + * @return 十六进制小写签名字符串 + */ + public static String sign(String keySecret, String payload) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(keySecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + return HexFormat.of().formatHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8))); + } catch (Exception e) { + throw new RuntimeException("[4SMS] 计算签名失败", e); + } + } + + /** + * 构造签名 payload + * + * @param keyId API Key + * @param timestamp Unix 时间戳(秒) + * @param method 请求方法(大写,如 POST / GET) + * @param path 请求路径(不含查询参数,如 /api/v1/sms/send) + */ + public static String buildPayload(String keyId, String timestamp, String method, String path) { + return keyId + ":" + timestamp + ":" + method + ":" + path; + } + + /** + * 发送 POST(application/json)请求 + * + * @param baseUrl 基础地址(如 https://4sms.cn) + * @param path 请求路径(如 /api/v1/sms/send) + * @param keyId API Key + * @param keySecret API 密钥 + * @param body JSON 请求体 + * @return 响应体字符串;网络异常时返回 null + */ + public static String post(String baseUrl, String path, String keyId, String keySecret, String body) { + try { + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + String signature = sign(keySecret, buildPayload(keyId, timestamp, "POST", path)); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .timeout(Duration.ofSeconds(15)) + .header("X-Api-Key", keyId) + .header("X-Timestamp", timestamp) + .header("X-Signature", signature) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + return response.body(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 发送 GET 请求 + * + * @param baseUrl 基础地址(如 https://4sms.cn) + * @param path 请求路径(不含查询参数,如 /api/v1/templates) + * @param keyId API Key + * @param keySecret API 密钥 + * @return 响应体字符串;网络异常时返回 null + */ + public static String get(String baseUrl, String path, String keyId, String keySecret) { + try { + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + String signature = sign(keySecret, buildPayload(keyId, timestamp, "GET", path)); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .timeout(Duration.ofSeconds(15)) + .header("X-Api-Key", keyId) + .header("X-Timestamp", timestamp) + .header("X-Signature", signature) + .GET() + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + return response.body(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/config/GfhtConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/config/GfhtConfig.java new file mode 100644 index 0000000..fa61ab5 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/config/GfhtConfig.java @@ -0,0 +1,36 @@ +package com.ruoyi.thirdparty.gfht.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "gfht") +public class GfhtConfig { + + /** + * 服务器URL + */ + private String serverUrl; + + /** + * 商户ID + */ + private String pid; + + /** + * 签名 + */ + private String sign; + + /** + * 通知URL + */ + private String notifyUrl; + + /** + * 返回URL + */ + private String returnUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/controller/GfhtController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/controller/GfhtController.java new file mode 100644 index 0000000..b2d9cdc --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/controller/GfhtController.java @@ -0,0 +1,51 @@ +package com.ruoyi.thirdparty.gfht.controller; + +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.thirdparty.gfht.service.GfhtService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Api(tags = "国富汇通") +@Slf4j +@RestController +@RequestMapping("api/gfht") +public class GfhtController extends BaseController { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private GfhtService gfhtService; + + @ApiOperation("支付") + @UserPermission + @PostMapping(value = "/pay") + public R pay(@RequestBody @Validated CreateOrderParam param) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + // 是否实名认证(0未认证 1已认证) + /*if ("0".equals(ttUser.getIsRealCheck())) { + return R.fail("未实名认证"); + }*/ + String ip = IpUtils.getIpAddr(); + return gfhtService.pay(param, ttUser, ip); + } + + // @GetMapping("/notify") + public String notify(@RequestParam Map params) { + return gfhtService.notify(params); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/domain/GfhtResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/domain/GfhtResponse.java new file mode 100644 index 0000000..196d1a1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/domain/GfhtResponse.java @@ -0,0 +1,19 @@ +package com.ruoyi.thirdparty.gfht.domain; + +import lombok.Data; + +@Data +public class GfhtResponse { + + private String code; + + private String msg; + + private String tradeNo; + + private String payurl; + + private String qrcode; + + private String urlscheme; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/service/GfhtService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/service/GfhtService.java new file mode 100644 index 0000000..01b4dfb --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/service/GfhtService.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.gfht.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; + +import java.util.Map; + +public interface GfhtService { + + R pay(CreateOrderParam param, TtUser ttUser, String ip); + + String notify(Map params); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/service/impl/GfhtServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/service/impl/GfhtServiceImpl.java new file mode 100644 index 0000000..528b506 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/service/impl/GfhtServiceImpl.java @@ -0,0 +1,448 @@ +package com.ruoyi.thirdparty.gfht.service.impl; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.common.constant.*; +import com.ruoyi.domain.entity.*; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.baidu.service.BaiduService; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import com.ruoyi.thirdparty.gfht.config.GfhtConfig; +import com.ruoyi.thirdparty.gfht.domain.GfhtResponse; +import com.ruoyi.thirdparty.gfht.service.GfhtService; +import com.ruoyi.thirdparty.gfht.util.GfhtUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class GfhtServiceImpl implements GfhtService { + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private GfhtConfig gfhtConfig; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtRechargeProdMapper rechargeListMapper; + + @Autowired + private TtBonusService bonusService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private TtPromotionLevelMapper ttPromotionLevelMapper; + + @Autowired + private TtFirstRechargeMapper ttFirstRechargeMapper; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private TtRechargeConfigMapper ttRechargeConfigMapper; + + @Autowired + private BaiduService baiduService; + + @Override + public R pay(CreateOrderParam param, TtUser ttUser, String ip) { + TtRechargeProd goods; + if (param.getPayWay() != null && Objects.equals(2, param.getPayWay())) { + // 1.查询商品信息 + TtRechargeConfig config = new LambdaQueryChainWrapper<>(ttRechargeConfigMapper) + .eq(TtRechargeConfig::getId, param.getGoodsId()) + .eq(TtRechargeConfig::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(config)) { + return R.fail("不存在的价格配置"); + } + if (config.getPrice().compareTo(param.getGoodsPrice()) != 0) { + return R.fail("配置的价格不一致"); + } + goods = new TtRechargeProd(); + BeanUtils.copyProperties(config, goods); + } else { + // 1.查询商品信息 + goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, param.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品"); + } + if (goods.getPrice().compareTo(param.getGoodsPrice()) != 0) { + return R.fail("商品价格不一致"); + } + } + + List orders = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getUserId, ttUser.getUserId()) + .eq(TtOrder::getType, PayType.GFHT_PAY.getCode()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .gt(TtOrder::getCreateTime, new Date(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10))) + .list(); + if (CollectionUtils.isNotEmpty(orders) && orders.size() > 2) { + return R.fail("操作频繁!"); + } + + // 计算总价值 + BigDecimal totalAmount = param.getGoodsPrice().multiply(new BigDecimal(param.getGoodsNum())); + + // 2.构建支付参数 + Map map = new HashMap<>(); + map.put("pid", gfhtConfig.getPid()); + map.put("type", param.getPayType()); + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + map.put("out_trade_no", orderId); + map.put("notify_url", gfhtConfig.getNotifyUrl()); + map.put("return_url", gfhtConfig.getReturnUrl()); + map.put("name", goods.getName()); + map.put("money", String.valueOf(totalAmount)); + map.put("clientip", ip); + String key = gfhtConfig.getSign(); + GfhtUtil signUtil = new GfhtUtil(key); + String sign = signUtil.getSign(map); + map.put("sign", sign); + map.put("sign_type", "MD5"); + + // 3.发送请求 + // HttpRequest post = HttpUtil.createPost(gfhtConfig.getServerUrl()); + // post.header("Content-Type","application/x-www-form-urlencoded"); + // post.formStr(map); + // HttpResponse res = post.execute(); + + // 切换请求 + // 获取服务器 URL + String url = gfhtConfig.getServerUrl(); + + // 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + // 构建表单数据 + MultiValueMap formParams = new LinkedMultiValueMap<>(); + for (Map.Entry entry : map.entrySet()) { + formParams.add(entry.getKey(), entry.getValue()); + } + + // 构建请求实体 + HttpEntity> requestEntity = new HttpEntity<>(formParams, headers); + + // 发送 POST 请求 + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + + // 获取响应 + String result = response.getBody(); + + log.info("result is {}", result); + + // 4.解析响应 + GfhtResponse resBody = JSONUtil.toBean(result, GfhtResponse.class); + if (!resBody.getCode().equals("1")) { + return R.fail(resBody.getMsg()); + } + + // 5.创建订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setThirdParty(PartyType.INNER_PICTURE_PAY.getCode() + ""); + order.setOutTradeNo(resBody.getTradeNo()); + order.setUserId(ttUser.getUserId()); + order.setType(PayType.GFHT_PAY.getCode()); + order.setGoodsId(param.getGoodsId()); + order.setGoodsPrice(param.getGoodsPrice()); + order.setGoodsNum(param.getGoodsNum()); + order.setTotalAmount(totalAmount); + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + order.setBdVid(param.getBdVid()); + orderMapper.insert(order); + + return R.ok(resBody); + } + + @Override + public String notify(Map params) { + log.info("支付成功通知"); + String result = "failure"; + + // 校验 + String tradeStatus = params.get("trade_status"); + if (!"TRADE_SUCCESS".equals(tradeStatus)) { + log.error("交易失败"); + return result; + } + + // 查询订单信息 + String outTradeNo = params.get("trade_no"); + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOutTradeNo, outTradeNo) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .one(); + // 防止重复通知 + if (!PayOrderStatus.NO_PAY.getCode().equals(order.getStatus())) { + return result; + } + + // 查询用户信息 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + + // 3.查询商品信息 + TtRechargeProd goods; + boolean innerPicturePay = Objects.equals(PartyType.INNER_PICTURE_PAY.getCode() + "", order.getThirdParty()); + if (innerPicturePay) { + goods = new TtRechargeProd(); + TtRechargeConfig config = new LambdaQueryChainWrapper<>(ttRechargeConfigMapper) + .eq(TtRechargeConfig::getId, order.getGoodsId()) + .eq(TtRechargeConfig::getStatus, 0) + .one(); + BeanUtils.copyProperties(config, goods); + } else { + goods = new LambdaQueryChainWrapper<>(rechargeListMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + } + + // 4.账户结算 + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + + try { + // 首充赠送 + firstChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广等级充值赠送 + promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广充值返佣 + promotionChargeCommission(user, goods, order.getGoodsNum()); + + // 回传百度推广信息 + baiduService.upload(user.getUserId(), 10, order.getBdVid()); + } catch (Exception e) { + log.error("happen exception is {}", e.getMessage(), e); + } + + // 5.更新订单 + boolean update = new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .update(); + if (update) { + result = "success"; + } + + // 6.发送充值通知 + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(order.getUserId().toString(), order.getGoodsPrice()); + + return result; + } + + // 账户结算 + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + + // 加钱(充值不加积分:productA + productC 全部计入 account_amount) + // BigDecimal totalAmount = order.getTotalAmount(); + BigDecimal productAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal productGift = goods.getProductC().multiply(new BigDecimal(goodsNumber)); + BigDecimal totalAmount = productAmount.add(productGift); + BigDecimal totalCredits = BigDecimal.ZERO; + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount.toString()); + userService.update(userUpdate); + + user = userService.getById(user.getUserId()); + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + + .amount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? totalAmount : null) + .finalAmount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountAmount().add(totalAmount) : null) + + .credits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? totalCredits : null) + .finalCredits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountCredits().add(totalCredits) : null) + + .total(totalAmount.add(totalCredits)) // 收支合计 + + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + userBlendErcashMapper.insert(blendErcash); + + // 充值记录 + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(order.getUserId()); + ttRechargeRecord.setParentId(user.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(user.getAccountAmount()); + ttRechargeRecord.setOrderId(order.getOrderId()); + ttRechargeRecord.setOutTradeNo(order.getOutTradeNo()); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("1"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + ttRechargeRecordMapper.insert(ttRechargeRecord); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } + + /** + * 首充赠送 + */ + private void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.判断是否为首充 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtUserBlendErcash::getSource, 1) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()); + List ttUserBlendErcashes = userBlendErcashMapper.selectList(wrapper); + if (ttUserBlendErcashes.size() > 0) { + return; + } + + // 2.加钱 + // 充值金额 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + // 查询赠送比例 + BigDecimal firstChargeAmountRatio = ttFirstRechargeMapper.selectRatioByMinAmount(totalAmount); + if (Objects.isNull(firstChargeAmountRatio)) { + return; + } + // 计算赠送金额 + BigDecimal giftAmount = totalAmount.multiply(firstChargeAmountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.FIRST_CHARGE.getCode()) + .remark(TtAccountRecordSource.FIRST_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广等级充值赠送 + */ + @Async + public void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.查询用户推广等级对应的赠送比例 + TtPromotionLevel ttPromotionLevel = ttPromotionLevelMapper.selectById(ttUser.getPromotionLevel()); + if (Objects.isNull(ttPromotionLevel)) + return; + + // 2.加钱 + BigDecimal amountRatio = ttPromotionLevel.getRechargeGiftRatio(); + if (Objects.isNull(amountRatio)) + return; + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal giftAmount = totalAmount.multiply(amountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getCode()) + .remark(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广充值返佣 + */ + private void promotionChargeCommission(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + if (Objects.isNull(ttUser.getParentId())) { + return; + } + // 充值返佣统一走 TtBonusService:按各级上级 commissionRate 逐级向上实时返佣、招商周结, + // 写 source=13,与卡密充值(ApiRechargeServiceImpl→bonus)口径一致,避免使用推广等级费率造成口径不符。 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + bonusService.distributeRechargeCommission(ttUser.getUserId(), totalAmount); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/util/GfhtUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/util/GfhtUtil.java new file mode 100644 index 0000000..88ca5ea --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/gfht/util/GfhtUtil.java @@ -0,0 +1,76 @@ +package com.ruoyi.thirdparty.gfht.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.TreeMap; + +public class GfhtUtil { + + private String key; + + public GfhtUtil(String key) { + this.key = key; + } + + /** + * 生成MD5签名 + * + * @param params 参数列表 + * @return 生成的签名 + */ + public String getSign(Map params) { + // 排序参数 + Map sortedParams = new TreeMap<>(params); + sortedParams.remove("sign"); + sortedParams.remove("sign_type"); + + // 构建签名字符串 + StringBuilder signStr = new StringBuilder(); + for (Map.Entry entry : sortedParams.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + signStr.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); + } + } + + if (signStr.length() > 0) { + signStr.setLength(signStr.length() - 1); // 移除最后一个& + } + + // 添加商户密钥 + signStr.append(this.key); + + // 计算MD5签名 + return md5(signStr.toString()).toLowerCase(); + } + + /** + * 计算MD5哈希值 + * + * @param input 输入字符串 + * @return MD5哈希值 + */ + private static String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(input.getBytes()); + return bytesToHex(messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 algorithm not found", e); + } + } + + /** + * 将字节数组转换为十六进制字符串 + * + * @param bytes 字节数组 + * @return 十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/config/HipayProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/config/HipayProperties.java new file mode 100644 index 0000000..81b56f3 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/config/HipayProperties.java @@ -0,0 +1,34 @@ +package com.ruoyi.thirdparty.hipay.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Hipay 充值配置,对应 application.yml 中 hipay 节点 + */ +@Data +@ConfigurationProperties(prefix = "hipay") +public class HipayProperties { + + /** 创建支付订单完整 URL */ + private String payUrl; + + /** 商户编号 */ + private String recvId; + + /** API 密钥 */ + private String apiKey; + + /** 支付通道 ID 1(默认/小额) */ + private String payChannelId1; + + /** 支付通道 ID 2(大额) */ + private String payChannelId2; + + /** 支付回调通知地址 */ + private String notifyUrl; + + /** 前端支付完成跳转地址 */ + private String returnUrl; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/controller/HiPayController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/controller/HiPayController.java new file mode 100644 index 0000000..517ca73 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/controller/HiPayController.java @@ -0,0 +1,52 @@ +package com.ruoyi.thirdparty.hipay.controller; + +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.thirdparty.hipay.service.HipayPaymentService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Api(tags = "") +@Slf4j +@RestController +@RequestMapping("/api/hipay") +public class HiPayController { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private HipayPaymentService hipayPaymentService; + + @ApiOperation("支付") + @UserPermission + // @PostMapping(value = "/pay") + public R pay(@RequestBody @Validated PayRequest param) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + // 是否实名认证(0未认证 1已认证) + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired(); + } + String ip = IpUtils.getIpAddr(); + return hipayPaymentService.pay(userId, param); + } + + // @PostMapping("/notify") + public R notify(@RequestBody Map params) { + log.info("HiPay notify params: {}", JSONUtil.toJsonStr(params)); + return hipayPaymentService.notifyCallback(params); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/controller/PayRequest.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/controller/PayRequest.java new file mode 100644 index 0000000..db7b0a8 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/controller/PayRequest.java @@ -0,0 +1,22 @@ +package com.ruoyi.thirdparty.hipay.controller; + +import io.swagger.annotations.ApiModelProperty; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class PayRequest { + @NotNull(message = "商品id不能为空。") + private Integer goodsId; + + @NotNull(message = "商品数量不能为空。") + @Min(value = 1, message = "最小值1") + private Integer goodsNum; + + @ApiModelProperty("1:支付宝1 2:支付宝2") + private Integer payType; + + @ApiModelProperty("1:原有支付,2:图片支付") + private Integer payWay = 1; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/controller/PayResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/controller/PayResponse.java new file mode 100644 index 0000000..d76dc18 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/controller/PayResponse.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.hipay.controller; + +import lombok.Data; + +@Data +public class PayResponse { + private String qrUrl; + private String payUrl; + private String orderId; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/enums/StateEnum.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/enums/StateEnum.java new file mode 100644 index 0000000..de6afd7 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/enums/StateEnum.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.hipay.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum StateEnum { + CREATE(2, "已创建"), + EXCHANGE(5, "已转币"), + CANCEL(9, "已取消"), + ERROR(99, "错误"), + ; + private final int code; + private final String desc; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/model/NotifyData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/model/NotifyData.java new file mode 100644 index 0000000..c510027 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/model/NotifyData.java @@ -0,0 +1,26 @@ +package com.ruoyi.thirdparty.hipay.model; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class NotifyData { + + private BigDecimal amount; + private long createtime; + private String id; + private String note; + private String notifyurl; + private String orderid; + private int ordertype; + private BigDecimal recvcharge; + private String recvid; + private String recvnickname; + private String resign; + private String returnurl; + private String sign; + private int state; + private long transtime; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/model/PaymentResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/model/PaymentResponse.java new file mode 100644 index 0000000..d022014 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/model/PaymentResponse.java @@ -0,0 +1,13 @@ +package com.ruoyi.thirdparty.hipay.model; + +import lombok.Data; + +@Data +public class PaymentResponse { + private int code; + private String msg; + private String payUrl; + private String qrcode; + private String tradeNo; +} + diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/model/PaymentResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/model/PaymentResult.java new file mode 100644 index 0000000..fc2a419 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/model/PaymentResult.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.hipay.model; + +import lombok.Data; + +@Data +public class PaymentResult { + private int code; + private String msg; + private PaymentData data; + + @Data + public static class PaymentData { + private String qrurl; + private String payurl; + private String orderid; + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/service/HipayPaymentService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/service/HipayPaymentService.java new file mode 100644 index 0000000..4defaf9 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/hipay/service/HipayPaymentService.java @@ -0,0 +1,310 @@ +package com.ruoyi.thirdparty.hipay.service; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MoneyUtil; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import com.ruoyi.thirdparty.hipay.config.HipayProperties; +import com.ruoyi.thirdparty.hipay.controller.PayRequest; +import com.ruoyi.thirdparty.hipay.controller.PayResponse; +import com.ruoyi.thirdparty.hipay.enums.StateEnum; +import com.ruoyi.thirdparty.hipay.model.NotifyData; +import com.ruoyi.thirdparty.hipay.model.PaymentResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.sql.Timestamp; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +@EnableConfigurationProperties(HipayProperties.class) +public class HipayPaymentService { + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private HipayProperties hipayProperties; + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + @Autowired + private TtUserMapper userMapper; + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + @Autowired + private TtUserService userService; + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private TtBonusService bonusService; + + public R pay(Long uid, PayRequest request) { + // 1.查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, request.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品"); + } + + // 计算总价值 + BigDecimal totalAmount = goods.getPrice().multiply(new BigDecimal(request.getGoodsNum())); + + + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + + String amount = MoneyUtil.toStr(totalAmount); + + // MD5签名 + String signText = "pay" + hipayProperties.getRecvId() + orderId + amount + hipayProperties.getApiKey(); + String sign = DigestUtils.md5DigestAsHex(signText.getBytes(StandardCharsets.UTF_8)); + + String paychannelid; + if (request.getPayType() == 2) { + paychannelid = hipayProperties.getPayChannelId2(); + } else { + paychannelid = hipayProperties.getPayChannelId1(); + } + + Map data = new HashMap<>(); + data.put("recvid", hipayProperties.getRecvId()); + data.put("orderid", orderId); + data.put("amount", amount); + data.put("sign", sign); + data.put("paychannelid", paychannelid); + data.put("memuid", DigestUtils.md5DigestAsHex((uid + "").getBytes(StandardCharsets.UTF_8))); + data.put("notifyurl", hipayProperties.getNotifyUrl()); + data.put("returnurl", hipayProperties.getReturnUrl()); + data.put("note", "充值" + amount); + + log.info("Hipay request data: {}", JSONUtil.toJsonStr(data)); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + + HttpEntity> entity = new HttpEntity<>(data, headers); + + ResponseEntity response = restTemplate.exchange( + hipayProperties.getPayUrl(), + HttpMethod.POST, + entity, + String.class + ); + + String result = response.getBody(); + + log.info("Hipay response data: {}", result); + + PaymentResult resBody = JSONUtil.toBean(result, PaymentResult.class); + if (resBody.getCode() != 1) { + return R.fail(resBody.getMsg()); + } + + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(resBody.getData().getOrderid()); + order.setUserId(Math.toIntExact(uid)); + order.setType(PayType.HI_PAY.getCode()); + order.setGoodsId(request.getGoodsId()); + order.setGoodsPrice(goods.getPrice()); + order.setGoodsNum(request.getGoodsNum()); + order.setTotalAmount(totalAmount); + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + + PayResponse resp = new PayResponse(); + resp.setQrUrl(resBody.getData().getQrurl()); + resp.setPayUrl(resBody.getData().getPayurl()); + resp.setOrderId(orderId); + + return R.ok(resp); + } + + + public R notifyCallback(Map params) { + log.info("支付回调通知"); + + String code = params.get("code"); + if (!"1".equals(code)) { + log.error("支付失败,code={}, msg={}", code, params.get("msg")); + return R.fail("支付失败"); + } + NotifyData resBody = JSONUtil.toBean(params.get("data"), NotifyData.class); + // 校验 + int tradeStatus = resBody.getState(); + if (StateEnum.EXCHANGE.getCode() != tradeStatus) { + log.error("交易失败"); + return R.fail("交易失败"); + } + String sign = resBody.getSign(); + String resign = resBody.getResign(); + String d = DigestUtils.md5DigestAsHex((sign + hipayProperties.getApiKey()).getBytes(StandardCharsets.UTF_8)); + if (!d.equals(resign)) { + return R.fail("签名验证失败"); + } + + // 查询订单信息 + String outTradeNo = resBody.getOrderid(); + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOutTradeNo, outTradeNo) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .one(); + if (order == null) { + return R.fail("订单不存在"); + } + // 防止重复通知 + if (!PayOrderStatus.NO_PAY.getCode().equals(order.getStatus())) { + return R.fail("重复通知"); + } + + // 查询用户信息 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + + // 3.查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + + // 4.账户结算 + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + +// try { +// // 首充赠送 +// firstChargeGiftAmount(user, goods, order.getGoodsNum()); +// +// // 推广等级充值赠送 +// promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum()); +// +// // 推广充值返佣 +// promotionChargeCommission(user, goods, order.getGoodsNum()); +// } catch (Exception e) { +// e.printStackTrace(); +// } + + // 5.更新订单 + new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .update(); + + // 6.发送充值通知 + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(order.getUserId().toString(), order.getGoodsPrice()); + + return R.ok(); + } + + // 账户结算 + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + // 加钱 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + + BigDecimal oldAmount = user.getAccountAmount(); + if (oldAmount == null) { + oldAmount = BigDecimal.ZERO; + } + BigDecimal newAmount = oldAmount.add(totalAmount); + + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount); + userService.update(userUpdate); + + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + + .amount(totalAmount) + .finalAmount(newAmount) + + .total(totalAmount) // 收支合计 + + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + userBlendErcashMapper.insert(blendErcash); + + + // 充值记录 + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(order.getUserId()); + ttRechargeRecord.setParentId(user.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(newAmount); + ttRechargeRecord.setOrderId(order.getOrderId()); + ttRechargeRecord.setOutTradeNo(order.getOutTradeNo()); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("1"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + ttRechargeRecordMapper.insert(ttRechargeRecord); + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + +// AsyncManager.me().run(() -> { +// bonusService.bonus(user.getUserId(), ttRechargeRecord.getId()); +// }); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/config/ItPayProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/config/ItPayProperties.java new file mode 100644 index 0000000..00ccb3f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/config/ItPayProperties.java @@ -0,0 +1,33 @@ +package com.ruoyi.thirdparty.itpay.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * itpay 支付渠道配置,对应 application.yml 中 itpay 节点 + */ +@Data +@ConfigurationProperties(prefix = "itpay") +public class ItPayProperties { + + /** API基础地址 */ + private String baseUrl; + + /** 商户号(mid),由平台提供 —— 部署时填写 */ + private String mid; + + /** 商户密钥(Secret),由平台提供 —— 部署时填写 */ + private String secret; + + /** 通道编码(channelCode),测试用 9999 */ + private String channelCode; + + /** 商户端会员IP(clientIp),回调/风控使用 —— 部署时填写 */ + private String clientIp; + + /** 支付回调通知地址(notifyUrl),不允许携带参数 */ + private String notifyUrl; + + /** 前端支付完成跳转地址(returnUrl,可空) */ + private String returnUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/controller/ItPayController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/controller/ItPayController.java new file mode 100644 index 0000000..0524f7a --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/controller/ItPayController.java @@ -0,0 +1,97 @@ +package com.ruoyi.thirdparty.itpay.controller; + +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.chengduhipay.controller.OrderStatusRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse; +import com.ruoyi.thirdparty.itpay.model.ItPayCreateResult; +import com.ruoyi.thirdparty.itpay.service.ItPayPaymentService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * itpay 支付渠道接口。 + * 接口清单: + * pay —— 创建充值订单(一般由统一充值按权重路由调用) + * notify —— 接收平台异步回调(平台调用,需返回字符串 success) + * orderStatus —— 查询本地订单支付状态(供前端轮询) + * queryPay —— 主动查询平台订单 + */ +@Api(tags = "itpay充值") +@Slf4j +@RestController +@RequestMapping("/api/itpay") +public class ItPayController { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private ItPayPaymentService itPayPaymentService; + + /** + * 创建充值订单 + */ + @ApiOperation("创建充值订单") + @UserPermission + // @PostMapping("/pay") + public R pay(@RequestBody @Validated PayRequest param) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired("未实名认证"); + } + return itPayPaymentService.pay(userId, param); + } + + /** + * 异步回调通知(application/json)。 + * 平台订单成功或失败都会回调,商户须返回字符串 "success"。 + * 处理失败(验签失败/订单不存在等)返回非 success,促使平台重推。 + */ + @ApiOperation("充值回调通知") + @Anonymous + @PostMapping("/notify") + @ResponseBody + public String notify(@RequestBody Map params) { + log.info("ItPay notify params: {}", JSONUtil.toJsonStr(params)); + R result = itPayPaymentService.notifyCallback(params); + if (!R.isSuccess(result)) { + log.error("ItPay notify 处理失败: {}", result.getMsg()); + return "fail"; + } + return "success"; + } + + /** + * 查询本地订单支付状态(供前端轮询) + */ + @ApiOperation("查询本地订单支付状态") + @UserPermission + @PostMapping("/orderStatus") + public R> orderStatus(@RequestBody @Validated OrderStatusRequest param) { + Long userId = SecurityUtils.getUserId(); + return itPayPaymentService.queryOrderStatus(userId, param.getOrderId()); + } + + /** + * 主动查询平台订单 + */ + @ApiOperation("查询充值订单") + @GetMapping("/queryPay") + public R queryPay(@RequestParam String merOrderTid) { + return itPayPaymentService.queryPay(merOrderTid); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/model/ItPayCreateResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/model/ItPayCreateResult.java new file mode 100644 index 0000000..bbe23ce --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/model/ItPayCreateResult.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.itpay.model; + +import lombok.Data; + +/** + * itpay 创建订单/订单查询接口通用返回对象 + */ +@Data +public class ItPayCreateResult { + /** 状态:0表示正常,1表示错误 */ + private int status; + /** status=1 时返回的错误内容 */ + private String errMsg; + /** 业务数据 */ + private ItPayResult result; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/model/ItPayNotifyData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/model/ItPayNotifyData.java new file mode 100644 index 0000000..54a049f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/model/ItPayNotifyData.java @@ -0,0 +1,22 @@ +package com.ruoyi.thirdparty.itpay.model; + +import lombok.Data; + +/** + * itpay 异步回调通知数据(对接文档回调参数) + */ +@Data +public class ItPayNotifyData { + /** 商户单号 */ + private String merOrderTid; + /** 系统单号(平台单号) */ + private String tid; + /** 订单金额 */ + private String money; + /** 状态:0待处理 1成功 2失败 3异常 4超时关闭 */ + private String status; + /** 付款人备注(下单有传时回调原样返回) */ + private String clientUserPayRemark; + /** 签名 */ + private String sign; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/model/ItPayResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/model/ItPayResult.java new file mode 100644 index 0000000..6979935 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/model/ItPayResult.java @@ -0,0 +1,22 @@ +package com.ruoyi.thirdparty.itpay.model; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * itpay 创建订单/订单查询的 result 业务数据 + */ +@Data +public class ItPayResult { + /** 平台单号 */ + private String tid; + /** 商户单号 */ + private String merOrderTid; + /** 支付地址 */ + private String payUrl; + /** 订单状态:0待处理 1成功 2失败 3异常 4超时关闭 */ + private Integer payOrderStatus; + /** 订单金额 */ + private BigDecimal money; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/service/ItPayPaymentService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/service/ItPayPaymentService.java new file mode 100644 index 0000000..f26d0d1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/service/ItPayPaymentService.java @@ -0,0 +1,386 @@ +package com.ruoyi.thirdparty.itpay.service; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MoneyUtil; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse; +import com.ruoyi.thirdparty.common.service.PayFrequencyGuard; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import com.ruoyi.thirdparty.itpay.config.ItPayProperties; +import com.ruoyi.thirdparty.itpay.model.ItPayCreateResult; +import com.ruoyi.thirdparty.itpay.utils.ItPaySignUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * itpay 支付渠道充值服务。 + * 实现:创建订单(CreateOrderPay)、异步回调(notify)、订单查询(QueryPayOrder)、本地订单状态查询。 + * 附带:拉单频控(x分钟y次未支付 -> 封禁z分钟)、回调只在 status==1 加币、DB条件CAS防重复加币。 + */ +@Slf4j +@Service +@EnableConfigurationProperties(ItPayProperties.class) +public class ItPayPaymentService { + + @Autowired + private ItPayProperties properties; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private PayFrequencyGuard payFrequencyGuard; + + @Autowired + private PlatformTransactionManager transactionManager; + + // ======================================================================== + // 创建订单:CreateOrderPay + // ======================================================================== + + public R pay(Long uid, PayRequest request) { + // 1. 查询商品 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, request.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品"); + } + + // 2. 生成商户单号(雪花ID,纯数字,长度<=30且无特殊字符,满足平台要求) + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + + // 3. 计算金额(元,最多2位小数) + BigDecimal totalAmount = goods.getPrice(); + String amount = MoneyUtil.toStr(totalAmount); + + // 4. 组装参与签名的参数 + Map params = new LinkedHashMap<>(); + params.put("mid", properties.getMid()); + params.put("merOrderTid", orderId); + params.put("money", amount); + params.put("channelCode", properties.getChannelCode()); + if (StrUtil.isNotBlank(properties.getClientIp())) { + params.put("clientIp", properties.getClientIp()); + } + params.put("notifyUrl", properties.getNotifyUrl()); + if (StrUtil.isNotBlank(properties.getReturnUrl())) { + params.put("returnUrl", properties.getReturnUrl()); + } + + // 5. 计算签名 + String sign = ItPaySignUtil.sign(params, properties.getSecret()); + params.put("sign", sign); + + // 6. 发起下单请求(application/x-www-form-urlencoded) + String createUrl = properties.getBaseUrl() + "/api/services/app/Api_PayOrder/CreateOrderPay"; + log.info("ItPay createOrder url={}, params={}", createUrl, JSONUtil.toJsonStr(params)); + try (HttpResponse response = HttpRequest.post(createUrl) + .header("Content-Type", "application/x-www-form-urlencoded") + .form(new HashMap<>(params)) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("ItPay createOrder response status={}, body={}", response.getStatus(), body); + + ItPayCreateResult resBody = JSONUtil.toBean(body, ItPayCreateResult.class); + if (resBody.getStatus() != 0 || resBody.getResult() == null) { + log.error("ItPay 下单失败, status={}, errMsg={}", resBody.getStatus(), resBody.getErrMsg()); + return R.fail(StrUtil.isNotBlank(resBody.getErrMsg()) ? resBody.getErrMsg() : "下单失败"); + } + + // 7. 保存订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(resBody.getResult().getTid()); + order.setUserId(Math.toIntExact(uid)); + order.setType(PayType.IT_PAY.getCode()); + order.setGoodsId(request.getGoodsId()); + order.setGoodsPrice(goods.getPrice()); + order.setGoodsNum(1); + order.setTotalAmount(totalAmount); + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + + // 8. 返回支付链接 + PayResponse resp = new PayResponse(); + resp.setPayUrl(resBody.getResult().getPayUrl()); + resp.setOrderId(orderId); + resp.setPlatformOrderId(resBody.getResult().getTid()); + + log.info("ItPay 下单成功, orderId={}, tid={}", orderId, resBody.getResult().getTid()); + return R.ok(resp); + } catch (Exception e) { + log.error("ItPay 下单异常", e); + return R.fail("支付通道异常,请稍后重试"); + } + } + + // ======================================================================== + // 异步回调:notify + // ======================================================================== + + /** + * 处理 itpay 异步回调。 + * 防错加币:仅验签通过且 status==1(成功) 才结算;其它状态仅应答不加币。 + * 防重复加币:DB 条件CAS(NO_PAY -> PAY_COMPLE)取影响行数,仅抢占成功者加币,与结算同事务。 + */ + public R notifyCallback(Map params) { + log.info("ItPay notify callback: {}", JSONUtil.toJsonStr(params)); + + // 1. 验签,失败直接拒绝,不加币 + if (!ItPaySignUtil.verify(params, properties.getSecret())) { + log.error("ItPay 回调签名验证失败"); + return R.fail("签名验证失败"); + } + + String merOrderTid = params.get("merOrderTid"); + String status = params.get("status"); + + // 2. 确认项1:只有明确的成功状态(status==1)才加币,杜绝"来个回调就当成功" + if (!"1".equals(status)) { + log.warn("ItPay 回调非成功状态,不加币, merOrderTid={}, status={}", merOrderTid, status); + return R.ok(); + } + + // 3. 查询本地订单(orderId == merOrderTid) + if (StrUtil.isBlank(merOrderTid)) { + log.error("ItPay 回调缺少 merOrderTid"); + return R.fail("回调数据不完整"); + } + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, merOrderTid) + .eq(TtOrder::getType, PayType.IT_PAY.getCode()) + .one(); + if (order == null) { + log.error("ItPay 回调订单不存在, merOrderTid={}", merOrderTid); + return R.fail("订单不存在"); + } + + // 4. 确认项2:DB条件CAS + 事务,保证同一成功回调重复推送只加一次币 + Boolean credited = new TransactionTemplate(transactionManager).execute(txStatus -> { + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(TtOrder::getId, order.getId()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()); + int rows = orderMapper.update(null, uw); + if (rows <= 0) { + // 已被处理,幂等跳过 + return Boolean.FALSE; + } + + // 抢占成功,执行结算 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + if (user == null) { + // 结算前置数据缺失,回滚以便后续重试/排查 + throw new IllegalStateException("ItPay 回调用户不存在, userId=" + order.getUserId()); + } + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (goods == null) { + throw new IllegalStateException("ItPay 回调商品不存在, goodsId=" + order.getGoodsId()); + } + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + return Boolean.TRUE; + }); + + if (Boolean.TRUE.equals(credited)) { + // 支付成功:清空该用户的未支付次数缓存 + payFrequencyGuard.clearOnPaid(order.getUserId().longValue()); + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice( + order.getUserId().toString(), order.getGoodsPrice()); + log.info("ItPay 回调处理成功, orderId={}", order.getOrderId()); + } else { + log.info("ItPay 回调幂等跳过(订单已处理), orderId={}", order.getOrderId()); + } + return R.ok(); + } + + // ======================================================================== + // 本地订单状态查询(供前端轮询) + // ======================================================================== + + public R> queryOrderStatus(Long userId, String orderId) { + if (ObjectUtil.isEmpty(orderId)) { + return R.fail("订单号不能为空"); + } + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, orderId) + .one(); + if (order == null) { + return R.fail("订单不存在"); + } + if (!Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) { + return R.fail("无权查询该订单"); + } + + String status = order.getStatus(); + String message; + if (PayOrderStatus.PAY_COMPLE.getCode().equals(status)) { + message = "充值成功"; + } else if (PayOrderStatus.NO_PAY.getCode().equals(status)) { + message = "支付中"; + } else if (PayOrderStatus.CANCEL.getCode().equals(status)) { + message = "订单已取消"; + } else if (PayOrderStatus.CALL_BACK_ERRO.getCode().equals(status)) { + message = "回调处理异常"; + } else { + message = "支付失败"; + } + + Map data = new HashMap<>(); + data.put("status", status); + data.put("amount", order.getGoodsPrice()); + data.put("message", message); + return R.ok(data); + } + + // ======================================================================== + // 订单查询:QueryPayOrder + // ======================================================================== + + public R queryPay(String merOrderTid) { + Map params = new LinkedHashMap<>(); + params.put("mid", properties.getMid()); + params.put("merOrderTid", merOrderTid); + String sign = ItPaySignUtil.sign(params, properties.getSecret()); + params.put("sign", sign); + + String queryUrl = properties.getBaseUrl() + "/api/services/app/Api_PayOrder/QueryPayOrder"; + log.info("ItPay queryOrder url={}, params={}", queryUrl, JSONUtil.toJsonStr(params)); + try (HttpResponse response = HttpRequest.post(queryUrl) + .header("Content-Type", "application/x-www-form-urlencoded") + .form(new HashMap<>(params)) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("ItPay queryOrder response status={}, body={}", response.getStatus(), body); + ItPayCreateResult resBody = JSONUtil.toBean(body, ItPayCreateResult.class); + if (resBody.getStatus() != 0) { + return R.fail(StrUtil.isNotBlank(resBody.getErrMsg()) ? resBody.getErrMsg() : "查询失败"); + } + return R.ok(resBody); + } catch (Exception e) { + log.error("ItPay 查单异常", e); + return R.fail("查询失败"); + } + } + + // ======================================================================== + // 账户结算(与现有 hipay/richku 逻辑保持一致) + // ======================================================================== + + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + + BigDecimal oldAmount = user.getAccountAmount(); + if (oldAmount == null) { + oldAmount = BigDecimal.ZERO; + } + BigDecimal newAmount = oldAmount.add(totalAmount); + + // 更新用户余额 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount); + userService.update(userUpdate); + + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + .amount(totalAmount) + .finalAmount(newAmount) + .total(totalAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + + // 充值记录 + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(order.getUserId()); + ttRechargeRecord.setParentId(user.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(newAmount); + ttRechargeRecord.setOrderId(order.getOrderId()); + ttRechargeRecord.setOutTradeNo(order.getOutTradeNo()); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("1"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + ttRechargeRecordMapper.insert(ttRechargeRecord); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/utils/ItPaySignUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/utils/ItPaySignUtil.java new file mode 100644 index 0000000..de1ca28 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/itpay/utils/ItPaySignUtil.java @@ -0,0 +1,69 @@ +package com.ruoyi.thirdparty.itpay.utils; + +import cn.hutool.crypto.digest.DigestUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.TreeMap; + +/** + * itpay 签名工具。 + * 签名算法(对接文档):非空参数按参数名 ASCII 升序 -> key1=value1&key2=value2&... -> + * 末尾直接追加商户密钥(&{secret},注意不是 &key=secret) -> MD5 -> 32位不区分大小写。 + */ +@Slf4j +public class ItPaySignUtil { + + /** + * 生成签名 + * + * @param params 参与签名的参数(不包含 sign 本身) + * @param secret 商户密钥 + * @return MD5 大写签名字符串 + */ + public static String sign(Map params, String secret) { + TreeMap sorted = new TreeMap<>(); + for (Map.Entry entry : params.entrySet()) { + // 空值不参与签名 + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + sorted.put(entry.getKey(), entry.getValue()); + } + } + + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : sorted.entrySet()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + // 末尾直接追加商户密钥 + sb.append("&").append(secret); + + String signStr = sb.toString(); + // 注意:签名原文含商户密钥,禁止打印到日志 + return DigestUtil.md5Hex(signStr).toUpperCase(); + } + + /** + * 验证回调签名(大小写不敏感) + * + * @param params 完整回调参数(包含 sign) + * @param secret 商户密钥 + * @return 签名是否匹配 + */ + public static boolean verify(Map params, String secret) { + String receivedSign = params.get("sign"); + if (receivedSign == null || receivedSign.isEmpty()) { + return false; + } + TreeMap signParams = new TreeMap<>(params); + signParams.remove("sign"); + String calculatedSign = sign(signParams, secret); + boolean valid = calculatedSign.equalsIgnoreCase(receivedSign); + if (!valid) { + log.error("ItPay 签名验证失败, expected={}, actual={}", calculatedSign, receivedSign); + } + return valid; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/config/JiuJiaProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/config/JiuJiaProperties.java new file mode 100644 index 0000000..ad998b0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/config/JiuJiaProperties.java @@ -0,0 +1,17 @@ +package com.ruoyi.thirdparty.jiujia.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "jiu-jia-pay") +public class JiuJiaProperties { + + private String aliPayUrl; + private String memberId; + private String appKey; + private String apiSecret; + private String apiDomain; + private String callbackUrl; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/controller/JiuJiaPayController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/controller/JiuJiaPayController.java new file mode 100644 index 0000000..3d0090b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/controller/JiuJiaPayController.java @@ -0,0 +1,56 @@ +package com.ruoyi.thirdparty.jiujia.controller; + +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.jiujia.domain.CallbackBody; +import com.ruoyi.thirdparty.jiujia.service.JiuJiaPayService; +import com.ruoyi.domain.other.CreateOrderParam; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Api(tags = "九嘉支付") +@RestController +@RequestMapping("/api/jiuJiaPay") +public class JiuJiaPayController extends BaseController { + + private final ISysConfigService sysConfigService; + private final TtUserService userService; + private final JiuJiaPayService jiuJiaPayService; + + public JiuJiaPayController(ISysConfigService sysConfigService, + TtUserService userService, + JiuJiaPayService jiuJiaPayService) { + this.sysConfigService = sysConfigService; + this.userService = userService; + this.jiuJiaPayService = jiuJiaPayService; + } + + // TODO: 2024/4/11 要修改!!! + @ApiOperation("回滚") + @Anonymous + @PostMapping("/callback") + public String callback(@RequestBody CallbackBody callbackBody) { + return jiuJiaPayService.callback(callbackBody); + } + + @ApiOperation("九嘉预下单") + @PostMapping("/createPay") + public R createPay(@RequestBody CreateOrderParam createOrderBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + String msg = jiuJiaPayService.createPay(createOrderBody, ttUser); + return msg.startsWith("https") ? R.ok(msg) : R.fail(msg); + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CallbackBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CallbackBody.java new file mode 100644 index 0000000..0347866 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CallbackBody.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.jiujia.domain; + +import lombok.Data; + +@Data +public class CallbackBody { + + private String member_id; + private String total_fee; + private String result_code; + private String trade_no; + private String out_trade_no; + private String time_end; + private String pay_type; + private String ext_sign; + private String sign; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CheckOrderRequestParam.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CheckOrderRequestParam.java new file mode 100644 index 0000000..dd55872 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CheckOrderRequestParam.java @@ -0,0 +1,21 @@ +package com.ruoyi.thirdparty.jiujia.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +@Builder +public class CheckOrderRequestParam { + + @JsonProperty("app_key") + private String appKey; + + @JsonProperty("member_id") + private String memberId; + + @JsonProperty("out_trade_no") + private String outTradeNo; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CheckOrderResponseData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CheckOrderResponseData.java new file mode 100644 index 0000000..fd33982 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CheckOrderResponseData.java @@ -0,0 +1,12 @@ +package com.ruoyi.thirdparty.jiujia.domain; + +import lombok.Data; + +@Data +public class CheckOrderResponseData { + private String total_amount; + private String out_trade_no; + private String trade_no; + private String pay_time; + private String status; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CheckOrderResponseResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CheckOrderResponseResult.java new file mode 100644 index 0000000..f45bed7 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CheckOrderResponseResult.java @@ -0,0 +1,11 @@ +package com.ruoyi.thirdparty.jiujia.domain; + +import lombok.Data; + +@Data +public class CheckOrderResponseResult { + + private Integer code; + private String msg; + private T data; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CreateOrderRequestParam.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CreateOrderRequestParam.java new file mode 100644 index 0000000..0db81c1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CreateOrderRequestParam.java @@ -0,0 +1,58 @@ +package com.ruoyi.thirdparty.jiujia.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CreateOrderRequestParam { + + @JsonProperty("app_key") + private String appKey; + + @JsonProperty("api_domain") + private String apiDomain; + + @JsonProperty("subject") + private String subject; + + @JsonProperty("pay_type") + private Integer payType; + + @JsonProperty("goods_id") + private Integer goodsId; + + @JsonProperty("goods_price") + private BigDecimal goodsPrice; + + @JsonProperty("goods_num") + private Integer goodsNum; + + @JsonProperty("order_id") + private String orderId; + + @JsonProperty("total_amount") + private BigDecimal totalAmount; + + @JsonProperty("member_id") + private String memberId; + + @JsonProperty("callback_url") + private String callbackUrl; + + @JsonProperty("sign") + private String sign; + + @JsonProperty("user_ip") + private String userIp; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CreateOrderResponseResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CreateOrderResponseResult.java new file mode 100644 index 0000000..b3aae2a --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/CreateOrderResponseResult.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.jiujia.domain; + +import lombok.Data; + +@Data +public class CreateOrderResponseResult { + + private Integer code; + private String msg; + private String data; + private String url; + private Integer wait; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/OrderBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/OrderBody.java new file mode 100644 index 0000000..6770e08 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/OrderBody.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.jiujia.domain; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class OrderBody { + + private BigDecimal totalAmount; + private String orderId; + + public OrderBody(BigDecimal totalAmount, String orderId) { + this.totalAmount = totalAmount; + this.orderId = orderId; + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/VisaVerificationBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/VisaVerificationBody.java new file mode 100644 index 0000000..82946a7 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/domain/VisaVerificationBody.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.jiujia.domain; + +import lombok.Builder; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +@Builder +public class VisaVerificationBody { + + private String totalFee; + private String resultCode; + private String tradeNo; + private String outTradeNo; + + private String sign; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/service/JiuJiaPayService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/service/JiuJiaPayService.java new file mode 100644 index 0000000..ad336c4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/service/JiuJiaPayService.java @@ -0,0 +1,21 @@ +package com.ruoyi.thirdparty.jiujia.service; + +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.jiujia.domain.CallbackBody; +import com.ruoyi.domain.other.CreateOrderParam; + +public interface JiuJiaPayService { + + /** + * 创建订单 + * @param createOrderBody 创建支付订单参数数据 + * @param ttUser 用户信息 + */ + String createPay(CreateOrderParam createOrderBody, TtUser ttUser); + + /** + * 支付回调 + * @param callbackBody 回调参数 + */ + String callback(CallbackBody callbackBody); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/service/impl/JiuJiaPayServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/service/impl/JiuJiaPayServiceImpl.java new file mode 100644 index 0000000..f810d82 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/service/impl/JiuJiaPayServiceImpl.java @@ -0,0 +1,376 @@ +package com.ruoyi.thirdparty.jiujia.service.impl; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.domain.common.constant.PartyType; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import com.ruoyi.thirdparty.jiujia.config.JiuJiaProperties; +import com.ruoyi.thirdparty.jiujia.domain.CallbackBody; +import com.ruoyi.thirdparty.jiujia.domain.CheckOrderRequestParam; +import com.ruoyi.thirdparty.jiujia.domain.CheckOrderResponseData; +import com.ruoyi.thirdparty.jiujia.domain.CheckOrderResponseResult; +import com.ruoyi.thirdparty.jiujia.domain.CreateOrderRequestParam; +import com.ruoyi.thirdparty.jiujia.domain.CreateOrderResponseResult; +import com.ruoyi.thirdparty.jiujia.domain.OrderBody; +import com.ruoyi.thirdparty.jiujia.domain.VisaVerificationBody; +import com.ruoyi.thirdparty.jiujia.service.JiuJiaPayService; +import com.ruoyi.thirdparty.jiujia.util.JiuJiaUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; +import java.util.concurrent.Executor; + +@Service +@EnableConfigurationProperties(value = JiuJiaProperties.class) +@Slf4j +public class JiuJiaPayServiceImpl implements JiuJiaPayService { + + private final TtBonusService bonusService; + private final JiuJiaProperties jiuJiaProperties; + private final Executor customThreadPoolExecutor; + private final TtRechargeProdMapper rechargeListMapper; + private final TtOrderMapper orderMapper; + private final TtUserService userService; + private final TtRechargeRecordMapper rechargeRecordMapper; + + public JiuJiaPayServiceImpl(TtBonusService bonusService, + JiuJiaProperties jiuJiaProperties, + Executor customThreadPoolExecutor, + TtRechargeProdMapper rechargeListMapper, + TtOrderMapper orderMapper, + TtUserService userService, + TtRechargeRecordMapper rechargeRecordMapper) { + + this.bonusService = bonusService; + this.jiuJiaProperties = jiuJiaProperties; + this.customThreadPoolExecutor = customThreadPoolExecutor; + this.rechargeListMapper = rechargeListMapper; + this.orderMapper = orderMapper; + this.userService = userService; + this.rechargeRecordMapper = rechargeRecordMapper; + } + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Autowired + private ISysConfigService configService; + + @Override + public String createPay(CreateOrderParam createOrderBody, TtUser ttUser) { + + Integer goodsId = createOrderBody.getGoodsId(); + BigDecimal goodsPrice = createOrderBody.getGoodsPrice().setScale(2, RoundingMode.HALF_UP); + Integer goodsNum = createOrderBody.getGoodsNum(); + BigDecimal totalAmount = goodsPrice.multiply(new BigDecimal(goodsNum)).setScale(2, RoundingMode.HALF_UP); + + // 本站充值产品 + TtRechargeProd ttRechargeProd = rechargeListMapper.selectById(goodsId); + if (goodsPrice.compareTo(ttRechargeProd.getPrice()) != 0) return "客户端传参产品价格不一致异常!"; + if (ttRechargeProd.getPrice().multiply(new BigDecimal(goodsNum)).compareTo(totalAmount) != 0) + return "客户端传参产品价格不一致异常!"; + + // 是否有购买相同商品并未超时的订单 + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getUserId, ttUser.getUserId()) + .eq(TtOrder::getThirdParty, PartyType.JIU_JIA_ZFB.getCode()) + .eq(TtOrder::getType, PayType.JIU_JIA_ZFB.getCode()) + .eq(TtOrder::getGoodsId, goodsId) + .eq(TtOrder::getGoodsPrice, goodsPrice) + .eq(TtOrder::getGoodsNum, goodsNum) + .eq(TtOrder::getTotalAmount, totalAmount) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .one(); + + if (ObjectUtil.isNotNull(order)) { + + // 已存在相同订单 + Date orderCreateTime = order.getCreateTime(); + DateTime timeout = DateUtil.offset(orderCreateTime, DateField.MINUTE, 5); + if (DateUtils.getNowDate().compareTo(DateUtil.date(timeout)) < 0) { + // 订单未超时直接返回支付url + return order.getPayUrl(); + } else { + // 超时 + // order.setStatus(PayOrderStatus.CANCEL.getCode()); + // order.setUpdateTime(DateUtils.getNowDate()); + // orderMapper.updateById(order); + new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .eq(TtOrder::getOrderId, order.getOrderId()) + .set(TtOrder::getStatus, PayOrderStatus.CANCEL.getCode()) + .set(TtOrder::getUpdateTime, DateUtils.getNowDate()) + .update(); + // if (orderMapper.updateById(order) > 0) { + // log.info("订单超时已关闭!"); + // }else { + // return "订单超时关闭失败,请联系管理员!"; + // } + } + + } + + // 初始化预下单请求参数 + String orderId = IdUtils.fastSimpleUUID().toUpperCase().substring(0, 20); + OrderBody orderBody = new OrderBody(totalAmount, orderId); + String sign = JiuJiaUtils.createSign(jiuJiaProperties, orderBody); + + // CreateOrderRequestParam requestParam = CreateOrderRequestParam.builder().build(); + CreateOrderRequestParam apiParam = new CreateOrderRequestParam(); + apiParam.setAppKey(jiuJiaProperties.getAppKey()); + apiParam.setApiDomain(jiuJiaProperties.getApiDomain()); + apiParam.setSubject(JiuJiaUtils.createSubject() + "*" + goodsNum); + apiParam.setPayType(Integer.valueOf(PayType.JIU_JIA_ZFB.getCode())); + apiParam.setGoodsId(goodsId); + apiParam.setGoodsPrice(goodsPrice); + apiParam.setGoodsNum(goodsNum); + apiParam.setOrderId(orderId); + apiParam.setTotalAmount(totalAmount); + apiParam.setMemberId(jiuJiaProperties.getMemberId()); + apiParam.setCallbackUrl(jiuJiaProperties.getCallbackUrl()); + apiParam.setSign(sign); + apiParam.setUserIp(IpUtils.getIpAddr()); + String jsonParam = JSONObject.toJSONString(apiParam); + + // 请求api + String result = HttpUtils.sendPostJSONString(jiuJiaProperties.getAliPayUrl() + "/create_pay", jsonParam); + + CreateOrderResponseResult responseResult = JSONObject.parseObject(result, CreateOrderResponseResult.class); + if (responseResult.getCode() > 0) return responseResult.getMsg(); + if (responseResult.getCode() < 0) return "创建订单异常,请联系管理员!"; + + // 构建新订单 + TtOrder ttOrder = new TtOrder(); + ttOrder.setUserId(ttUser.getUserId()); + ttOrder.setThirdParty(String.valueOf(PartyType.JIU_JIA_ZFB.getCode())); + ttOrder.setType(PayType.JIU_JIA_ZFB.getCode()); + ttOrder.setGoodsId(goodsId); + ttOrder.setGoodsPrice(goodsPrice); + ttOrder.setGoodsNum(goodsNum); + ttOrder.setTotalAmount(totalAmount); + ttOrder.setOrderId(orderId); + ttOrder.setSign(sign); + ttOrder.setStatus(PayOrderStatus.NO_PAY.getCode()); + ttOrder.setOutTradeNo(responseResult.getData()); + ttOrder.setPayUrl(responseResult.getUrl()); + ttOrder.setCreateTime(DateUtils.getNowDate()); + + int isSuccess = orderMapper.insert(ttOrder); + if (isSuccess <= 0) { + return "创建订单异常,请联系管理员!"; + } + + // 发送充值成功通知 + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(ttUser.getUserId().toString(), order.getGoodsPrice()); + + return responseResult.getUrl(); + } + + @Override + public String callback(CallbackBody callbackBody) { + + String resultCode = callbackBody.getResult_code(); + if (!"success".equals(resultCode)) { + log.error("九嘉回调出现异常,请联系客服!"); + return "fail"; + } + + String outTradeNo = callbackBody.getOut_trade_no(); + String orderId = callbackBody.getTrade_no(); + // 查询创建的订单(带渠道type隔离,避免跨渠道串单;不带状态,用于取id做CAS) + TtOrder ttOrder = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOutTradeNo, outTradeNo) + .eq(TtOrder::getOrderId, orderId) + .eq(TtOrder::getType, PayType.JIU_JIA_ZFB.getCode()) + .one(); + + if (StringUtils.isNull(ttOrder)) { + log.error("不存在的订单。"); + return "fail"; + } + + // 查询用户 + TtUser user = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getUserId, ttOrder.getUserId()) + .eq(TtUser::getDelFlag, "0") + .one(); + if (ObjectUtil.isNull(user)) { + log.error("不存在的订单支付用户。"); + return "fail"; + } + + // 验签 + String sign = callbackBody.getSign(); + VisaVerificationBody vvBody = VisaVerificationBody.builder().build(); + vvBody.setTotalFee(String.valueOf(ttOrder.getTotalAmount())); + vvBody.setResultCode(resultCode); + vvBody.setTradeNo(ttOrder.getOrderId()); + vvBody.setOutTradeNo(ttOrder.getOutTradeNo()); + vvBody.setSign(sign); + if (!JiuJiaUtils.visaVerification(jiuJiaProperties, vvBody)) { + log.warn("九嘉支付回调,验签失败。"); + return "fail"; + } + + // 主动回查平台支付情况,不轻信回调 + CheckOrderResponseData check = checkPay(outTradeNo); + if (ObjectUtil.isNull(check)) { + log.error("九嘉支付回调,订单支付情况复查无结果!"); + return "fail"; + } + + // 状态:TRADE_FINISHED、TRADE_SUCCESS、TRADE_CLOSED、WAIT_BUYER_PAY + String status = check.getStatus(); + if (!status.equals("TRADE_SUCCESS") && !status.equals("TRADE_FINISHED")) { + // 非成功:条件更新为失败(仅未支付订单,避免覆盖已完成订单) + new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, ttOrder.getId()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_FAIL.getCode()) + .set(TtOrder::getUpdateTime, DateUtils.getNowDate()) + .update(); + return "fail"; + } + + // DB条件CAS + 事务:NO_PAY->PAY_COMPLE 抢占成功者才加币,防成功回调重复推送双倍加币 + final TtUser payUser = user; + Boolean credited = new TransactionTemplate(transactionManager).execute(txStatus -> { + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(TtOrder::getId, ttOrder.getId()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, DateUtils.getNowDate()); + int rows = orderMapper.update(null, uw); + if (rows <= 0) { + // 已被处理,幂等跳过 + return Boolean.FALSE; + } + rechargeSuccess(ttOrder, payUser); + return Boolean.TRUE; + }); + + if (Boolean.TRUE.equals(credited)) { + log.info("九嘉回调处理成功, orderId={}", ttOrder.getOrderId()); + } else { + log.info("九嘉回调幂等跳过(订单已处理), orderId={}", ttOrder.getOrderId()); + } + + return resultCode; + } + + private CheckOrderResponseData checkPay(String outTradeNo) { + + // 查询订单 + CheckOrderRequestParam requestParam = CheckOrderRequestParam.builder() + .appKey(jiuJiaProperties.getAppKey()) + .memberId(jiuJiaProperties.getMemberId()) + .outTradeNo(outTradeNo) + .build(); + String jsonParam = JSONObject.toJSONString(requestParam); + + String result = HttpUtils.sendPostJSONString(jiuJiaProperties.getAliPayUrl() + "/check_pay", jsonParam); + + CheckOrderResponseResult responseResult = + JSONObject.parseObject(result, new TypeReference>() { + }); + + return responseResult.getCode() == 0 ? responseResult.getData() : null; + } + + private void rechargeSuccess(TtOrder ttOrder, TtUser user) { + + // 订单状态由回调中的DB条件CAS原子更新为PAY_COMPLE,这里不再重复更新状态,只做加币等结算 + + + // TODO: 2024/4/12 这张表弃用 + // TtRechargeRecord rechargeRecord = new LambdaQueryChainWrapper<>(rechargeRecordMapper) + // .eq(TtRechargeRecord::getOrderId, ttOrder.getOrderId()) + // .eq(TtRechargeRecord::getOutTradeNo, ttOrder.getOutTradeNo()) + // .one(); + // if (StringUtils.isNotNull(rechargeRecord)) return; + + // 本站产品 + TtRechargeProd goods = rechargeListMapper.selectById(ttOrder.getGoodsId()); + + // 更新用户账户 + BigDecimal prodA = goods.getProductA().multiply(new BigDecimal(ttOrder.getGoodsNum())); + BigDecimal prodC = goods.getProductC().multiply(new BigDecimal(ttOrder.getGoodsNum())); + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + prodA.toString() + + ",account_credits = account_credits + " + prodC.toString()); + userService.update(wrapper); + + user = userService.getById(user.getUserId()); + // 更新充值记录 + userService.insertUserAmountRecords( + user.getUserId(), + TtAccountRecordType.INPUT, + TtAccountRecordSource.RECHARGE, + prodA, + user.getAccountAmount()); + userService.insertUserCreditsRecords( + user.getUserId(), + TtAccountRecordType.INPUT, + TtAccountRecordSource.RECHARGE, + prodC, + user.getAccountCredits()); + // TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + // ttRechargeRecord.setUserId(ttOrder.getUserId()); + // ttRechargeRecord.setParentId(user.getParentId()); + // ttRechargeRecord.setArrivalAmount(totalAmount); + // ttRechargeRecord.setAmountActuallyPaid(totalAmount); + // ttRechargeRecord.setFinallyPrice(user.getAccountAmount()); + // ttRechargeRecord.setOrderId(ttOrder.getOrderId()); + // ttRechargeRecord.setOutTradeNo(ttOrder.getOutTradeNo()); + // ttRechargeRecord.setStatus("0"); + // ttRechargeRecord.setChannelType("1"); + // ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + // if (rechargeRecordMapper.insert(ttRechargeRecord) > 0) { + // + // // todo 奖励津贴? + // // bonusService.bonus(user.getUserId(), ttRechargeRecord.getId()); + // } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/util/JiuJiaUtils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/util/JiuJiaUtils.java new file mode 100644 index 0000000..a838ef0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/jiujia/util/JiuJiaUtils.java @@ -0,0 +1,48 @@ +package com.ruoyi.thirdparty.jiujia.util; + +import com.ruoyi.common.utils.sign.Md5Utils; +import com.ruoyi.thirdparty.jiujia.config.JiuJiaProperties; +import com.ruoyi.thirdparty.jiujia.domain.OrderBody; +import com.ruoyi.thirdparty.jiujia.domain.VisaVerificationBody; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class JiuJiaUtils { + + public static String createSign(JiuJiaProperties jiuJiaProperties, OrderBody orderBody) { + String param = "member_id=" + jiuJiaProperties.getMemberId() + "&app_key=" + jiuJiaProperties.getAppKey() + + "&api_domain=" + jiuJiaProperties.getApiDomain() + "&total_amount=" + orderBody.getTotalAmount() + + "&callback_url=" + jiuJiaProperties.getCallbackUrl() + "&order_id=" + orderBody.getOrderId() + + "&key=" + jiuJiaProperties.getApiSecret(); + return Md5Utils.hash(param); + } + + public static boolean visaVerification(JiuJiaProperties jiuJiaProperties, VisaVerificationBody visaVerificationBody) { + String param = "member_id=" + jiuJiaProperties.getMemberId() + "&total_fee=" + visaVerificationBody.getTotalFee() + + "&result_code=" + visaVerificationBody.getResultCode() + "&trade_no=" + visaVerificationBody.getTradeNo() + + "&out_trade_no=" + visaVerificationBody.getOutTradeNo() + "&key=" + jiuJiaProperties.getApiSecret(); + String hash = Md5Utils.hash(param); + return visaVerificationBody.getSign().equals(hash); + } + + public static String createSubject() { + List list = new ArrayList<>(); + list.add("水晶箱子"); + list.add("木箱子"); + list.add("手工箱子"); + list.add("坚毅的箱子"); + list.add("金箱子"); + list.add("木钥匙"); + list.add("水晶钥匙"); + list.add("铁箱子"); + list.add("手工钥匙"); + list.add("金钥匙"); + list.add("纸箱子"); + list.add("万能钥匙"); + list.add("生锈的钥匙"); + int randomIndex = new Random().nextInt(list.size()); + return list.get(randomIndex); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/config/YunXinProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/config/YunXinProperties.java new file mode 100644 index 0000000..5923bdc --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/config/YunXinProperties.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.note.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "yun-xin") +public class YunXinProperties { + + private String appKey; + private String appSecret; + private String templateId; + private String serverUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/domain/YunXinNoteResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/domain/YunXinNoteResult.java new file mode 100644 index 0000000..1724464 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/domain/YunXinNoteResult.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.note.domain; + +import lombok.Data; + +@Data +public class YunXinNoteResult { + private Integer code; + private String msg; + private String obj; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/service/YunXinNoteService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/service/YunXinNoteService.java new file mode 100644 index 0000000..affb929 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/service/YunXinNoteService.java @@ -0,0 +1,6 @@ +package com.ruoyi.thirdparty.note.service; + +public interface YunXinNoteService { + + String sendNote(String mobile, String nonce); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/service/impl/YunXinNoteServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/service/impl/YunXinNoteServiceImpl.java new file mode 100644 index 0000000..d6520d8 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/service/impl/YunXinNoteServiceImpl.java @@ -0,0 +1,62 @@ +package com.ruoyi.thirdparty.note.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.thirdparty.note.config.YunXinProperties; +import com.ruoyi.thirdparty.note.domain.YunXinNoteResult; +import com.ruoyi.thirdparty.note.service.YunXinNoteService; +import com.ruoyi.thirdparty.note.util.CheckSumBuilder; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Service +@EnableConfigurationProperties(value = YunXinProperties.class) +@Slf4j +public class YunXinNoteServiceImpl implements YunXinNoteService { + + private final YunXinProperties yunXinProperties; + + public YunXinNoteServiceImpl(YunXinProperties yunXinProperties) { + this.yunXinProperties = yunXinProperties; + } + + @Override + public String sendNote(String mobile, String nonce) { + CloseableHttpClient httpClient = HttpClients.createDefault(); + HttpPost httpPost = new HttpPost(yunXinProperties.getServerUrl() + "/sendcode.action"); + String curTime = String.valueOf((new Date()).getTime() / 1000L); + try { + String checkSum = CheckSumBuilder.getCheckSum(yunXinProperties.getAppSecret(), nonce, curTime); + httpPost.addHeader("AppKey", yunXinProperties.getAppKey()); + httpPost.addHeader("Nonce", nonce); + httpPost.addHeader("CurTime", curTime); + httpPost.addHeader("CheckSum", checkSum); + httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + List nvps = new ArrayList(); + nvps.add(new BasicNameValuePair("templateid", yunXinProperties.getTemplateId())); + nvps.add(new BasicNameValuePair("mobile", mobile)); + nvps.add(new BasicNameValuePair("authCode", nonce)); + httpPost.setEntity(new UrlEncodedFormEntity(nvps, "utf-8")); + HttpResponse response = httpClient.execute(httpPost); + String responseEntity = EntityUtils.toString(response.getEntity()); + YunXinNoteResult result = JSONObject.parseObject(responseEntity, YunXinNoteResult.class); + if (result.getCode() == 200) return result.getObj(); + } catch (IOException e) { + return ""; + } + return ""; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/util/CheckSumBuilder.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/util/CheckSumBuilder.java new file mode 100644 index 0000000..f8d76dd --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/note/util/CheckSumBuilder.java @@ -0,0 +1,41 @@ +package com.ruoyi.thirdparty.note.util; + +import java.security.MessageDigest; + +public class CheckSumBuilder { + + private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + public static String getCheckSum(String appSecret, String nonce, String curTime) { + return encode("sha1", appSecret + nonce + curTime); + } + + public static String getMD5(String requestBody) { + return encode("md5", requestBody); + } + + private static String encode(String algorithm, String value) { + if (value == null) { + return null; + } + try { + MessageDigest messageDigest + = MessageDigest.getInstance(algorithm); + messageDigest.update(value.getBytes()); + return getFormattedText(messageDigest.digest()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String getFormattedText(byte[] bytes) { + int len = bytes.length; + StringBuilder buf = new StringBuilder(len * 2); + for (int j = 0; j < len; j++) { + buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]); + buf.append(HEX_DIGITS[bytes[j] & 0x0f]); + } + return buf.toString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/config/RichKuProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/config/RichKuProperties.java new file mode 100644 index 0000000..a212b9d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/config/RichKuProperties.java @@ -0,0 +1,30 @@ +package com.ruoyi.thirdparty.richku.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "richku") +public class RichKuProperties { + + /** API基础地址 */ + private String baseUrl; + + /** 商户号 */ + private String memberId; + + /** 秘钥 */ + private String apiKey; + + /** 支付宝银行编码 */ + private String alipayBankcode; + + /** 银联银行编码 */ + private String unionpayBankcode; + + /** 支付回调通知地址 */ + private String notifyUrl; + + /** 前端支付完成跳转地址 */ + private String returnUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/controller/RichKuController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/controller/RichKuController.java new file mode 100644 index 0000000..cb27f9c --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/controller/RichKuController.java @@ -0,0 +1,104 @@ +package com.ruoyi.thirdparty.richku.controller; + +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.chengduhipay.controller.OrderStatusRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse; +import com.ruoyi.thirdparty.richku.service.RichKuPaymentService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * RichKu 充值接口 + */ +@Api(tags = "RichKu充值") +@Slf4j +@RestController +@RequestMapping("/api/richku") +public class RichKuController { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private RichKuPaymentService richKuPaymentService; + + /** + * 创建充值订单 + */ + @ApiOperation("创建充值订单") + @UserPermission + // @PostMapping("/pay") + public R pay(@RequestBody @Validated PayRequest param, HttpServletRequest request) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired("未实名认证"); + } + String ip = IpUtils.getIpAddr(request); + return richKuPaymentService.pay(userId, param, ip); + } + + /** + * 充值回调通知 + * RichKu 平台在用户支付成功后通过 x-www-form-urlencoded 方式 POST 通知 + * 必须返回纯文本 "OK" + */ + @ApiOperation("充值回调通知") + @Anonymous + @PostMapping(value = "/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + @ResponseBody + public String notify(@RequestParam Map params) { + log.info("RichKu notify params: {}", JSONUtil.toJsonStr(params)); + R result = richKuPaymentService.notifyCallback(params); + if (!R.isSuccess(result)) { + log.error("RichKu notify 处理失败: {}", result.getMsg()); + } + return "OK"; + } + + /** + * 查询本地订单支付状态(供前端轮询) + */ + @ApiOperation("查询本地订单支付状态") + @UserPermission + @PostMapping("/orderStatus") + public R> orderStatus(@RequestBody @Validated OrderStatusRequest param) { + Long userId = SecurityUtils.getUserId(); + return richKuPaymentService.queryOrderStatus(userId, param.getOrderId()); + } + + /** + * 查询外部订单 + */ + @ApiOperation("查询充值订单") + @GetMapping("/queryPay") + public R> queryPay(@RequestParam String orderId) { + return richKuPaymentService.queryPay(orderId); + } + + /** + * 查询商户余额 + */ + @ApiOperation("查询商户余额") + @GetMapping("/balance") + public R balance() { + return richKuPaymentService.getBalance(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/model/RichKuNotifyData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/model/RichKuNotifyData.java new file mode 100644 index 0000000..650b119 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/model/RichKuNotifyData.java @@ -0,0 +1,34 @@ +package com.ruoyi.thirdparty.richku.model; + +import lombok.Data; + +/** + * RichKu 支付回调通知数据 + */ +@Data +public class RichKuNotifyData { + + /** 商户编号 */ + private String memberid; + + /** 订单号 */ + private String orderid; + + /** 订单金额 */ + private String amount; + + /** 交易流水号 */ + private String transaction_id; + + /** 交易时间 */ + private String datetime; + + /** 交易状态,"00" 为成功 */ + private String returncode; + + /** 扩展返回 */ + private String attach; + + /** 签名 */ + private String sign; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/model/RichKuPayResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/model/RichKuPayResponse.java new file mode 100644 index 0000000..be819d1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/model/RichKuPayResponse.java @@ -0,0 +1,37 @@ +package com.ruoyi.thirdparty.richku.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * RichKu 下单接口响应 + */ +@Data +public class RichKuPayResponse { + + /** 状态码,"1" 为下单成功 */ + private String status; + + /** 状态信息 */ + private String msg; + + /** 商户订单号 */ + @JsonProperty("order_id") + private String orderId; + + /** 平台订单号 */ + @JsonProperty("mch_order_id") + private String mchOrderId; + + /** H5支付链接 */ + @JsonProperty("h5_url") + private String h5Url; + + /** SDK请求参数 */ + @JsonProperty("sdk_url") + private String sdkUrl; + + /** 付款链接(HTML格式取此参数) */ + @JsonProperty("pay_url") + private String payUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/service/RichKuPaymentService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/service/RichKuPaymentService.java new file mode 100644 index 0000000..c380537 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/service/RichKuPaymentService.java @@ -0,0 +1,455 @@ +package com.ruoyi.thirdparty.richku.service; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MoneyUtil; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse; +import com.ruoyi.thirdparty.common.service.PayFrequencyGuard; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import com.ruoyi.thirdparty.richku.config.RichKuProperties; +import com.ruoyi.thirdparty.richku.model.RichKuNotifyData; +import com.ruoyi.thirdparty.richku.model.RichKuPayResponse; +import com.ruoyi.thirdparty.richku.utils.RichKuSignUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * RichKu 充值服务 + */ +@Slf4j +@Service +@EnableConfigurationProperties(RichKuProperties.class) +public class RichKuPaymentService { + + @Autowired + private RichKuProperties properties; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Autowired + private PayFrequencyGuard payFrequencyGuard; + + /** + * 根据支付方式获取银行编码 + */ + private String selectBankCode(Integer payType) { + if (payType != null && payType == 2) { + return properties.getUnionpayBankcode(); + } + return properties.getAlipayBankcode(); + } + + /** + * 创建充值订单 + */ + public R pay(Long uid, PayRequest request, String clientIp) { + // 1. 查询商品 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, request.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品"); + } + + // 2. 生成订单号 + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + + // 3. 计算金额 + BigDecimal totalAmount = goods.getPrice(); + String amount = MoneyUtil.toStr(totalAmount); + String applyDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + + // 4. 构建请求参数(参与签名的字段) + Map params = new LinkedHashMap<>(); + params.put("pay_memberid", properties.getMemberId()); + params.put("pay_orderid", orderId); + params.put("pay_applydate", applyDate); + params.put("pay_bankcode", selectBankCode(request.getPayType())); + params.put("pay_notifyurl", properties.getNotifyUrl()); + params.put("pay_callbackurl", properties.getReturnUrl()); + params.put("pay_amount", amount); + + // 5. 计算签名 + String sign = RichKuSignUtil.sign(params, properties.getApiKey()); + + // 6. 添加不参与签名的字段 + params.put("pay_md5sign", sign); + params.put("pay_productname", goods.getName()); + params.put("pay_ip", clientIp != null ? clientIp : "127.0.0.1"); + + // 7. 发起下单请求 + String payUrl = properties.getBaseUrl() + "/Pay_Index.html"; + log.info("RichKu createPay url={}, params={}", payUrl, JSONUtil.toJsonStr(params)); + try (HttpResponse response = HttpRequest.post(payUrl) + .header("Content-Type", "application/x-www-form-urlencoded") + .formStr(params) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("RichKu createPay response status={}, body={}", response.getStatus(), body); + + RichKuPayResponse resBody = JSONUtil.toBean(body, RichKuPayResponse.class); + if (!"1".equals(resBody.getStatus())) { + log.error("RichKu 下单失败, status={}, msg={}", resBody.getStatus(), resBody.getMsg()); + return R.fail(resBody.getMsg() != null ? resBody.getMsg() : "下单失败"); + } + + // 8. 保存订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(resBody.getMchOrderId()); + order.setUserId(Math.toIntExact(uid)); + order.setType(PayType.RICH_KU.getCode()); + order.setGoodsId(request.getGoodsId()); + order.setGoodsPrice(goods.getPrice()); + order.setGoodsNum(1); + order.setTotalAmount(totalAmount); + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + + // 9. 返回支付链接 + PayResponse resp = new PayResponse(); + resp.setPayUrl(resBody.getPayUrl() != null ? resBody.getPayUrl() : resBody.getH5Url()); + resp.setOrderId(orderId); + resp.setPlatformOrderId(resBody.getMchOrderId()); + + log.info("RichKu 下单成功, orderId={}, platformOrderId={}", orderId, resBody.getMchOrderId()); + return R.ok(resp); + } catch (Exception e) { + log.error("RichKu 下单异常", e); + return R.fail("支付通道异常,请稍后重试"); + } + } + + /** + * 处理支付回调通知 + */ + public R notifyCallback(Map params) { + log.info("RichKu notify callback: {}", JSONUtil.toJsonStr(params)); + + // 1. 验证签名 + if (!RichKuSignUtil.verify(params, properties.getApiKey())) { + log.error("RichKu 回调签名验证失败"); + return R.fail("签名验证失败"); + } + + // 2. 解析回调数据 + RichKuNotifyData notifyData = new RichKuNotifyData(); + notifyData.setMemberid(params.get("memberid")); + notifyData.setOrderid(params.get("orderid")); + notifyData.setAmount(params.get("amount")); + notifyData.setTransaction_id(params.get("transaction_id")); + notifyData.setDatetime(params.get("datetime")); + notifyData.setReturncode(params.get("returncode")); + notifyData.setAttach(params.get("attach")); + notifyData.setSign(params.get("sign")); + + // 3. 检查交易状态 + if (!"00".equals(notifyData.getReturncode())) { + log.error("RichKu 回调交易失败, returncode={}", notifyData.getReturncode()); + return R.fail("支付失败"); + } + + // 4. 查询本地订单(按商户订单号查询,不带状态,用于取 id 做 CAS) + String orderId = notifyData.getOrderid(); + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, orderId) + .eq(TtOrder::getType, PayType.RICH_KU.getCode()) + .one(); + if (order == null) { + log.error("RichKu 回调: 订单不存在, orderId={}", orderId); + return R.fail("订单不存在"); + } + + // 4.1 校验回调金额与订单金额一致,防止金额篡改;金额缺失直接拒绝 + String callbackAmountStr = notifyData.getAmount(); + if (StrUtil.isBlank(callbackAmountStr)) { + log.error("RichKu 回调金额缺失, orderId={}", orderId); + return R.fail("回调金额缺失"); + } + BigDecimal callbackAmount; + try { + callbackAmount = new BigDecimal(callbackAmountStr); + } catch (NumberFormatException e) { + log.error("RichKu 回调金额格式异常, orderId={}, amount={}", orderId, callbackAmountStr); + return R.fail("回调金额格式异常"); + } + if (callbackAmount.compareTo(order.getTotalAmount()) != 0) { + log.error("RichKu 回调金额与订单金额不一致, orderId={}, callback={}, order={}", + orderId, callbackAmountStr, order.getTotalAmount()); + return R.fail("回调金额与订单金额不一致"); + } + + // 5. DB条件CAS + 事务:NO_PAY->PAY_COMPLE 抢占成功者才结算,防成功回调重复推送双倍加币 + final String transactionId = notifyData.getTransaction_id(); + Boolean credited = new TransactionTemplate(transactionManager).execute(txStatus -> { + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(TtOrder::getId, order.getId()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .set(TtOrder::getOutTradeNo, + transactionId != null ? transactionId : order.getOutTradeNo()); + int rows = orderMapper.update(null, uw); + if (rows <= 0) { + return Boolean.FALSE; + } + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + if (user == null) { + throw new IllegalStateException("RichKu 回调用户不存在, userId=" + order.getUserId()); + } + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (goods == null) { + throw new IllegalStateException("RichKu 回调商品不存在, goodsId=" + order.getGoodsId()); + } + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + return Boolean.TRUE; + }); + + // 6. 发送充值成功通知(幂等:已处理则跳过) + if (Boolean.TRUE.equals(credited)) { + // 支付成功:清空该用户的未支付次数缓存 + payFrequencyGuard.clearOnPaid(order.getUserId().longValue()); + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice( + order.getUserId().toString(), order.getGoodsPrice()); + log.info("RichKu 回调处理成功, orderId={}", order.getOrderId()); + } else { + log.info("RichKu 回调幂等跳过(订单已处理), orderId={}", order.getOrderId()); + } + return R.ok(); + } + + /** + * 查询本地订单支付状态(供前端轮询) + */ + public R> queryOrderStatus(Long userId, String orderId) { + if (ObjectUtil.isEmpty(orderId)) { + return R.fail("订单号不能为空"); + } + + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, orderId) + .one(); + if (order == null) { + return R.fail("订单不存在"); + } + if (!Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) { + return R.fail("无权查询该订单"); + } + + String status = order.getStatus(); + String message; + if (PayOrderStatus.PAY_COMPLE.getCode().equals(status)) { + message = "充值成功"; + } else if (PayOrderStatus.NO_PAY.getCode().equals(status)) { + message = "支付中"; + } else if (PayOrderStatus.CANCEL.getCode().equals(status)) { + message = "订单已取消"; + } else if (PayOrderStatus.CALL_BACK_ERRO.getCode().equals(status)) { + message = "回调处理异常"; + } else { + message = "支付失败"; + } + + Map data = new HashMap<>(); + data.put("status", status); + data.put("amount", order.getGoodsPrice()); + data.put("message", message); + return R.ok(data); + } + + /** + * 查询外部订单 + */ + public R> queryPay(String orderId) { + Map params = new LinkedHashMap<>(); + params.put("pay_memberid", properties.getMemberId()); + params.put("pay_orderid", orderId); + + String sign = RichKuSignUtil.sign(params, properties.getApiKey()); + params.put("pay_md5sign", sign); + + String queryUrl = properties.getBaseUrl() + "/Pay_Trade_query.html"; + log.info("RichKu queryPay url={}, params={}", queryUrl, JSONUtil.toJsonStr(params)); + + try (HttpResponse response = HttpRequest.post(queryUrl) + .header("Content-Type", "application/x-www-form-urlencoded") + .formStr(params) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("RichKu queryPay response status={}, body={}", response.getStatus(), body); + + Map result = new HashMap<>(); + // 解析 URL 编码的响应 + String[] pairs = body.split("&"); + for (String pair : pairs) { + String[] kv = pair.split("=", 2); + if (kv.length == 2) { + result.put(kv[0], kv[1]); + } + } + return R.ok(result); + } catch (Exception e) { + log.error("RichKu 查单异常", e); + return R.fail("查询失败"); + } + } + + /** + * 查询商户余额 + */ + public R getBalance() { + String orderId = String.valueOf(IdUtil.getSnowflake(1, 1).nextId()); + String applyDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + + Map params = new LinkedHashMap<>(); + params.put("pay_memberid", properties.getMemberId()); + params.put("pay_orderid", orderId); + params.put("pay_applydate", applyDate); + + String sign = RichKuSignUtil.sign(params, properties.getApiKey()); + params.put("pay_md5sign", sign); + + String balanceUrl = properties.getBaseUrl() + "/Pay_Trade_querymoney.html"; + log.info("RichKu getBalance url={}, params={}", balanceUrl, JSONUtil.toJsonStr(params)); + + try (HttpResponse response = HttpRequest.post(balanceUrl) + .header("Content-Type", "application/x-www-form-urlencoded") + .formStr(params) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("RichKu getBalance response status={}, body={}", response.getStatus(), body); + + Map result = new HashMap<>(); + String[] pairs = body.split("&"); + for (String pair : pairs) { + String[] kv = pair.split("=", 2); + if (kv.length == 2) { + result.put(kv[0], kv[1]); + } + } + return R.ok(result.get("balance")); + } catch (Exception e) { + log.error("RichKu 余额查询异常", e); + return R.fail("查询失败"); + } + } + + /** + * 支付成功后的账户结算 + */ + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + + BigDecimal oldAmount = user.getAccountAmount(); + if (oldAmount == null) { + oldAmount = BigDecimal.ZERO; + } + BigDecimal newAmount = oldAmount.add(totalAmount); + + // 更新用户余额 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount); + userService.update(userUpdate); + + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + .amount(totalAmount) + .finalAmount(newAmount) + .total(totalAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + + // 充值记录 + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(order.getUserId()); + ttRechargeRecord.setParentId(user.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(newAmount); + ttRechargeRecord.setOrderId(order.getOrderId()); + ttRechargeRecord.setOutTradeNo(order.getOutTradeNo()); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("1"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + ttRechargeRecordMapper.insert(ttRechargeRecord); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/utils/RichKuSignUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/utils/RichKuSignUtil.java new file mode 100644 index 0000000..42334d0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/richku/utils/RichKuSignUtil.java @@ -0,0 +1,65 @@ +package com.ruoyi.thirdparty.richku.utils; + +import cn.hutool.crypto.digest.DigestUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.TreeMap; + +/** + * RichKu 签名工具 + * 签名算法:非空参数按 ASCII 字典序排序 -> key1=value1&key2=value2 -> 末尾拼 &key=秘钥 -> MD5 大写 + */ +@Slf4j +public class RichKuSignUtil { + + /** + * 生成签名 + * @param params 参与签名的参数(不包含 sign 本身) + * @param apiKey 商户秘钥 + * @return MD5 大写的签名字符串 + */ + public static String sign(Map params, String apiKey) { + TreeMap sorted = new TreeMap<>(); + for (Map.Entry entry : params.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + sorted.put(entry.getKey(), entry.getValue()); + } + } + + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : sorted.entrySet()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + sb.append("&key=").append(apiKey); + + String signStr = sb.toString(); + // 注意:签名原文含商户密钥,禁止打印到日志 + return DigestUtil.md5Hex(signStr).toUpperCase(); + } + + /** + * 验证回调签名 + * @param params 完整的回调参数(包含 sign) + * @param apiKey 商户秘钥 + * @return 签名是否匹配 + */ + public static boolean verify(Map params, String apiKey) { + String receivedSign = params.get("sign"); + if (receivedSign == null) { + return false; + } + Map signParams = new TreeMap<>(params); + signParams.remove("sign"); + signParams.remove("attach"); // 附加字段不参与签名 + String calculatedSign = sign(signParams, apiKey); + boolean valid = calculatedSign.equals(receivedSign); + if (!valid) { + log.error("RichKu 签名验证失败, expected={}, actual={}", calculatedSign, receivedSign); + } + return valid; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/config/SmsbaoConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/config/SmsbaoConfig.java new file mode 100644 index 0000000..4ae597a --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/config/SmsbaoConfig.java @@ -0,0 +1,20 @@ +package com.ruoyi.thirdparty.smsbao.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "smsbao") +public class SmsbaoConfig { + + private String httpUrl; + + private String username; + + /** + * 短信宝万能接口的 API Key(直接作为 p 参数传递,无需 MD5) + */ + private String apiKey; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/controller/SmsbaoController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/controller/SmsbaoController.java new file mode 100644 index 0000000..2a7ab41 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/controller/SmsbaoController.java @@ -0,0 +1,22 @@ +package com.ruoyi.thirdparty.smsbao.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.thirdparty.smsbao.service.SmsbaoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin/smsbao") +public class SmsbaoController extends BaseController { + + @Autowired + private SmsbaoService smsbaoService; + + @GetMapping("/test") + public AjaxResult test(String mobile, String content) { + return success(smsbaoService.sendSms(mobile, content)); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/service/SmsbaoService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/service/SmsbaoService.java new file mode 100644 index 0000000..62727e1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/service/SmsbaoService.java @@ -0,0 +1,6 @@ +package com.ruoyi.thirdparty.smsbao.service; + +public interface SmsbaoService { + + public String sendSms(String mobile, String content); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/service/impl/SmsbaoServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/service/impl/SmsbaoServiceImpl.java new file mode 100644 index 0000000..a11a171 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/service/impl/SmsbaoServiceImpl.java @@ -0,0 +1,30 @@ +package com.ruoyi.thirdparty.smsbao.service.impl; + +import com.ruoyi.thirdparty.smsbao.config.SmsbaoConfig; +import com.ruoyi.thirdparty.smsbao.service.SmsbaoService; +import com.ruoyi.thirdparty.smsbao.util.SmsbaoUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class SmsbaoServiceImpl implements SmsbaoService { + + @Autowired + private SmsbaoConfig smsbaoConfig; + + @Override + public String sendSms(String mobile, String content) { + StringBuffer httpArg = new StringBuffer(); + httpArg.append("u=").append(smsbaoConfig.getUsername()).append("&"); + // 万能接口:p 参数直接传 API Key,无需 MD5 + httpArg.append("p=").append(smsbaoConfig.getApiKey()).append("&"); + httpArg.append("m=").append(mobile).append("&"); + httpArg.append("c=").append(SmsbaoUtil.encodeUrlString(content, "UTF-8")); + + String result = SmsbaoUtil.request(smsbaoConfig.getHttpUrl(), httpArg.toString()); + log.info("[短信宝] mobile={}, httpUrl={}, response={}", mobile, smsbaoConfig.getHttpUrl(), result); + return result; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/util/SmsbaoUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/util/SmsbaoUtil.java new file mode 100644 index 0000000..b64345b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/smsbao/util/SmsbaoUtil.java @@ -0,0 +1,76 @@ +package com.ruoyi.thirdparty.smsbao.util; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class SmsbaoUtil { + + public static String md5(String plainText) { + StringBuffer buf = null; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(plainText.getBytes()); + byte b[] = md.digest(); + int i; + buf = new StringBuffer(""); + for (int offset = 0; offset < b.length; offset++) { + i = b[offset]; + if (i < 0) + i += 256; + if (i < 16) + buf.append("0"); + buf.append(Integer.toHexString(i)); + } + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return buf.toString(); + } + + public static String encodeUrlString(String str, String charset) { + String strret = null; + if (str == null) + return str; + try { + strret = java.net.URLEncoder.encode(str, charset); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return strret; + } + + public static String request(String httpUrl, String httpArg) { + BufferedReader reader = null; + String result = null; + StringBuffer sbf = new StringBuffer(); + httpUrl = httpUrl + "?" + httpArg; + + try { + URL url = new URL(httpUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + InputStream is = connection.getInputStream(); + reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); + String strRead = reader.readLine(); + if (strRead != null) { + sbf.append(strRead); + while ((strRead = reader.readLine()) != null) { + sbf.append("\n"); + sbf.append(strRead); + } + } + reader.close(); + result = sbf.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/config/SnapPayProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/config/SnapPayProperties.java new file mode 100644 index 0000000..9269b71 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/config/SnapPayProperties.java @@ -0,0 +1,47 @@ +package com.ruoyi.thirdparty.snappay.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.math.BigDecimal; + +/** + * SnapPay 支付渠道配置,对应 application.yml 中 snappay 节点。 + * 仅代收(充值):下单/查单/余额分别拼接 + * baseUrl + /api/v2/transactions/deposit、/api/v2/transactions/query、/api/v2/balances。 + */ +@Data +@ConfigurationProperties(prefix = "snappay") +public class SnapPayProperties { + + /** API 基础地址(代收),例如 https://api.snappay168.store */ + private String baseUrl; + + /** + * 商户ID(merchant_id,m 开头字符串)。 + * 登录商户后台 https://web.snappay168.store 获取后填写 —— 部署时填写。 + * 为空时视为未启用,金额段不会强制路由到本渠道。 + */ + private String merchantId; + + /** 商户密钥(secretKey),参与 SHA256 签名 —— 部署时填写 */ + private String secret; + + /** 代收通道编码(channel,number 型别):900=支付宝、901=微信 */ + private Integer channel; + + /** 充值金额下限(含)。低于此值不路由到本渠道,默认 1000 */ + private BigDecimal minAmount = new BigDecimal("1000"); + + /** 充值金额上限(含)。高于此值不路由到本渠道,默认 50000 */ + private BigDecimal maxAmount = new BigDecimal("50000"); + + /** 无邮箱用户补全必填 email 字段用的默认域名(保证 email 格式合法) */ + private String emailDomain = "snap-user.com"; + + /** 支付回调通知地址(notify_url),只有传该值平台才发起回调 */ + private String notifyUrl; + + /** 前端支付完成跳转地址(redirect_url,可空) */ + private String redirectUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/controller/SnapPayController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/controller/SnapPayController.java new file mode 100644 index 0000000..84b20e2 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/controller/SnapPayController.java @@ -0,0 +1,108 @@ +package com.ruoyi.thirdparty.snappay.controller; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.chengduhipay.controller.OrderStatusRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse; +import com.ruoyi.thirdparty.snappay.service.SnapPayPaymentService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * SnapPay 支付渠道接口(仅代收)。 + * 接口清单: + * pay —— 创建充值订单(一般由统一充值按金额段路由调用) + * notify —— 接收平台异步回调(平台调用,需返回字符串 "success") + * orderStatus —— 查询本地订单支付状态(供前端轮询) + * queryPay —— 主动查询平台交易 + */ +@Api(tags = "snappay充值") +@Slf4j +@RestController +@RequestMapping("/api/snappay") +public class SnapPayController { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private SnapPayPaymentService snapPayPaymentService; + + /** + * 创建充值订单(默认由统一充值路由调用) + */ + @ApiOperation("创建充值订单") + @UserPermission + @PostMapping("/pay") + public R pay(@RequestBody @Validated PayRequest param) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + if (ttUser == null) { + return R.fail("用户不存在"); + } + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired("未实名认证"); + } + return snapPayPaymentService.pay(userId, param); + } + + /** + * 异步回调通知(application/json)。 + * 交易进入终态时平台会回调,商户须返回字符串 "success"。 + * 处理失败(验签失败/订单不存在等)返回非 success,促使平台重推。 + */ + @ApiOperation("充值回调通知") + @Anonymous + @PostMapping("/notify") + @ResponseBody + public String notify(@RequestBody Map params) { + log.info("SnapPay notify params: {}", JSONUtil.toJsonStr(params)); + R result = snapPayPaymentService.notifyCallback(params); + if (!R.isSuccess(result)) { + log.error("SnapPay notify 处理失败: {}", result.getMsg()); + return "fail"; + } + return "success"; + } + + /** + * 查询本地订单支付状态(供前端轮询) + */ + @ApiOperation("查询本地订单支付状态") + @UserPermission + @PostMapping("/orderStatus") + public R> orderStatus(@RequestBody @Validated OrderStatusRequest param) { + Long userId = SecurityUtils.getUserId(); + return snapPayPaymentService.queryOrderStatus(userId, param.getOrderId()); + } + + /** + * 主动查询平台交易(仅允许查询本人订单) + */ + @ApiOperation("查询充值订单") + @UserPermission + @GetMapping("/queryPay") + public R queryPay(@RequestParam String merchantOrderId) { + Long userId = SecurityUtils.getUserId(); + return snapPayPaymentService.queryPay(userId, merchantOrderId); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/service/SnapPayPaymentService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/service/SnapPayPaymentService.java new file mode 100644 index 0000000..adbaacf --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/service/SnapPayPaymentService.java @@ -0,0 +1,585 @@ +package com.ruoyi.thirdparty.snappay.service; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest; +import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse; +import com.ruoyi.thirdparty.common.service.PayFrequencyGuard; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import com.ruoyi.thirdparty.snappay.config.SnapPayProperties; +import com.ruoyi.thirdparty.snappay.utils.SnapPaySignUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * SnapPay 支付渠道充值服务(仅代收)。 + * 实现:创建代收订单(/api/v2/transactions/deposit)、异步回调(notify)、 + * 交易查询(/api/v2/transactions/query)、余额查询(/api/v2/balances)、本地订单状态查询。 + *

+ * 关键约定(对接文档): + * - 请求 application/json;channel 为 number;amt/amount 金额为字符串(最多三位小数)。 + * - 签名 SHA256(见 {@link SnapPaySignUtil});签名/验签统一使用 Object 类型 Map 以保留 channel 数字型别。 + * - 交易状态以 status 判定:PENDING/SUCCESS/FAILED;4xx/5xx 视为请求被拒或未知。 + * - 回调订单号字段为 merchant_order_id;成功状态为 SUCCESS;商户须应答字符串 "success"。 + * - 回调可能带 original_amount(实付金额与订单金额不同),本地以订单金额校验、按商品配置入账。 + *

+ * 安全:回调只在 status==SUCCESS 加币、校验回调订单金额与本地一致、DB 条件 CAS 防重复加币、验签。 + * 路由:仅在金额位于 [minAmount, maxAmount] 且已配置 merchantId、未被 sys_config 关闭时才可被强制路由。 + */ +@Slf4j +@Service +@EnableConfigurationProperties(SnapPayProperties.class) +public class SnapPayPaymentService { + + /** sys_config 开关键:值为 "0" 时停用 SnapPay 强制路由(回落到权重路由),其余(含缺省)视为启用 */ + private static final String CONFIG_KEY_ENABLED = "snappayEnabled"; + + @Autowired + private SnapPayProperties properties; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private PayFrequencyGuard payFrequencyGuard; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Autowired + private ISysConfigService sysConfigService; + + // ======================================================================== + // 路由判定:金额段 + 开关 + // ======================================================================== + + /** + * 判断指定金额是否应强制路由到 SnapPay。 + * 条件:已配置 merchantId + 金额位于 [minAmount, maxAmount] + 未被 sys_config(snappayEnabled=0) 停用。 + */ + public boolean isRoutable(BigDecimal amount) { + if (StrUtil.isBlank(properties.getMerchantId())) { + return false; + } + if (amount == null) { + return false; + } + if (properties.getMinAmount() != null && amount.compareTo(properties.getMinAmount()) < 0) { + return false; + } + if (properties.getMaxAmount() != null && amount.compareTo(properties.getMaxAmount()) > 0) { + return false; + } + String flag = sysConfigService.selectConfigByKey(CONFIG_KEY_ENABLED); + // 默认启用;仅当显式配置为 "0" 时停用 + return !"0".equals(flag); + } + + // ======================================================================== + // 创建订单:/api/v2/transactions/deposit + // ======================================================================== + + public R pay(Long uid, PayRequest request) { + // 1. 查询商品 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, request.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品"); + } + + // 2. 配置校验:merchantId 未配置则视为通道不可用 + if (StrUtil.isBlank(properties.getMerchantId())) { + log.error("SnapPay merchantId 未配置,无法下单"); + return R.fail("支付通道未配置"); + } + + // 3. 金额范围校验(例如 1000~50000) + BigDecimal totalAmount = goods.getPrice(); + if (properties.getMinAmount() != null && totalAmount.compareTo(properties.getMinAmount()) < 0 + || properties.getMaxAmount() != null && totalAmount.compareTo(properties.getMaxAmount()) > 0) { + return R.fail("充值金额需在 " + plain(properties.getMinAmount()) + + "-" + plain(properties.getMaxAmount()) + " 之间"); + } + + // 4. 查询用户(补全必填的 name/email/phone 字段) + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, uid) + .eq(TtUser::getDelFlag, 0) + .one(); + + // 5. 生成商户单号(雪花ID,纯数字) + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + + // 6. 组装参与签名的参数(channel 为 number 型别,务必用 Integer) + Map params = new LinkedHashMap<>(); + params.put("channel", properties.getChannel()); + params.put("merchant_id", properties.getMerchantId()); + params.put("order_id", orderId); + params.put("amt", formatAmount(totalAmount)); + // name 含下单用户ID便于渠道侧对账;文档要求正常英文姓名,故用 User{uid} 形式而非纯数字 + params.put("name", "User" + uid); + params.put("email", buildEmail(user, uid)); + params.put("phone", buildPhone(user)); + if (StrUtil.isNotBlank(properties.getNotifyUrl())) { + params.put("notify_url", properties.getNotifyUrl()); + } + if (StrUtil.isNotBlank(properties.getRedirectUrl())) { + params.put("redirect_url", properties.getRedirectUrl()); + } + + // 7. 计算签名 + String signature = SnapPaySignUtil.sign(params, properties.getSecret()); + params.put("signature", signature); + + // 8. 发起下单请求(application/json) + String createUrl = properties.getBaseUrl() + "/api/v2/transactions/deposit"; + String bodyJson = SnapPaySignUtil.toJson(params); + log.info(bodyJson); + // 请求体含客户 PII,仅记录关键字段 + log.info("SnapPay createOrder url={}, orderId={}, channel={}, amt={}", + createUrl, orderId, properties.getChannel(), params.get("amt")); + try (HttpResponse response = HttpRequest.post(createUrl) + .header("Content-Type", "application/json; charset=utf-8") + .body(bodyJson) + .timeout(15000) + .execute()) { + String respBody = response.body(); + log.info("SnapPay createOrder response httpStatus={}, body={}", response.getStatus(), respBody); + + JSONObject obj = JSONUtil.parseObj(respBody); + String status = obj.getStr("status"); + // 成功建单:status 为 PENDING(待支付)或 SUCCESS;其余(FAILED/4xx/5xx)视为下单失败 + if (!"PENDING".equals(status) && !"SUCCESS".equals(status)) { + String msg = obj.getStr("message"); + log.error("SnapPay 下单失败, status={}, message={}", status, msg); + return R.fail(StrUtil.isNotBlank(msg) ? msg : "下单失败"); + } + + String platformId = obj.getStr("id"); + String paymentUrl = obj.getStr("payment_url"); + String qrcode = obj.getStr("qrcode"); + + // 9. 保存订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(platformId); + order.setUserId(Math.toIntExact(uid)); + order.setType(PayType.SNAP_PAY.getCode()); + order.setGoodsId(request.getGoodsId()); + order.setGoodsPrice(goods.getPrice()); + order.setGoodsNum(1); + order.setTotalAmount(totalAmount); + order.setSign(signature); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + + // 10. 返回支付链接/二维码 + PayResponse resp = new PayResponse(); + resp.setPayUrl(paymentUrl); + resp.setQrUrl(qrcode); + resp.setOrderId(orderId); + resp.setPlatformOrderId(platformId); + + log.info("SnapPay 下单成功, orderId={}, platformId={}", orderId, platformId); + return R.ok(resp); + } catch (Exception e) { + log.error("SnapPay 下单异常", e); + return R.fail("支付通道异常,请稍后重试"); + } + } + + // ======================================================================== + // 异步回调:notify + // ======================================================================== + + /** + * 处理 SnapPay 异步回调。 + * 防错加币:验签通过且 status==SUCCESS 才结算;其它状态仅应答不加币。 + * 防金额篡改:以回调携带的订单原金额(original_amount 优先,否则 amount)与本地订单金额比对。 + * 防重复加币:DB 条件 CAS(NO_PAY -> PAY_COMPLE)取影响行数,仅抢占成功者加币,与结算同事务。 + */ + public R notifyCallback(Map params) { + log.info("SnapPay notify callback: {}", JSONUtil.toJsonStr(params)); + + // 1. 验签,失败直接拒绝,不加币 + if (!SnapPaySignUtil.verify(params, properties.getSecret())) { + log.error("SnapPay 回调签名验证失败"); + return R.fail("签名验证失败"); + } + + String merchantOrderId = asStr(params.get("merchant_order_id")); + String status = asStr(params.get("status")); + + // 2. 只有明确的成功状态(status==SUCCESS)才加币 + if (!"SUCCESS".equals(status)) { + log.warn("SnapPay 回调非成功状态,不加币, merchantOrderId={}, status={}", merchantOrderId, status); + return R.ok(); + } + + // 3. 查询本地订单(orderId == merchant_order_id) + if (StrUtil.isBlank(merchantOrderId)) { + log.error("SnapPay 回调缺少 merchant_order_id"); + return R.fail("回调数据不完整"); + } + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, merchantOrderId) + .eq(TtOrder::getType, PayType.SNAP_PAY.getCode()) + .one(); + if (order == null) { + log.error("SnapPay 回调订单不存在, merchantOrderId={}", merchantOrderId); + return R.fail("订单不存在"); + } + + // 3.1 校验回调订单金额与本地一致,防篡改。 + // original_amount 存在时代表实付与订单金额不同,其值为订单原金额;否则 amount 即订单金额。 + String orderAmountStr = asStr(params.get("original_amount")); + if (StrUtil.isBlank(orderAmountStr)) { + orderAmountStr = asStr(params.get("amount")); + } + if (StrUtil.isBlank(orderAmountStr)) { + log.error("SnapPay 回调缺少金额, merchantOrderId={}", merchantOrderId); + return R.fail("回调金额缺失"); + } + BigDecimal callbackOrderAmount; + try { + callbackOrderAmount = new BigDecimal(orderAmountStr); + } catch (NumberFormatException e) { + log.error("SnapPay 回调金额格式非法, merchantOrderId={}, amount={}", merchantOrderId, orderAmountStr); + return R.fail("回调金额格式非法"); + } + if (order.getTotalAmount() == null || callbackOrderAmount.compareTo(order.getTotalAmount()) != 0) { + log.error("SnapPay 回调订单金额与本地不一致, merchantOrderId={}, callback={}, order={}", + merchantOrderId, callbackOrderAmount, order.getTotalAmount()); + return R.fail("回调金额与订单金额不一致"); + } + + // 4. DB条件CAS + 事务,保证同一成功回调重复推送只加一次币 + Boolean credited = new TransactionTemplate(transactionManager).execute(txStatus -> { + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(TtOrder::getId, order.getId()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()); + int rows = orderMapper.update(null, uw); + if (rows <= 0) { + // 已被处理,幂等跳过 + return Boolean.FALSE; + } + + // 抢占成功,执行结算 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + if (user == null) { + throw new IllegalStateException("SnapPay 回调用户不存在, userId=" + order.getUserId()); + } + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (goods == null) { + throw new IllegalStateException("SnapPay 回调商品不存在, goodsId=" + order.getGoodsId()); + } + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + return Boolean.TRUE; + }); + + if (Boolean.TRUE.equals(credited)) { + // 支付成功:清空该用户的未支付次数缓存 + payFrequencyGuard.clearOnPaid(order.getUserId().longValue()); + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice( + order.getUserId().toString(), order.getGoodsPrice()); + log.info("SnapPay 回调处理成功, orderId={}", order.getOrderId()); + } else { + log.info("SnapPay 回调幂等跳过(订单已处理), orderId={}", order.getOrderId()); + } + return R.ok(); + } + + // ======================================================================== + // 本地订单状态查询(供前端轮询) + // ======================================================================== + + public R> queryOrderStatus(Long userId, String orderId) { + if (ObjectUtil.isEmpty(orderId)) { + return R.fail("订单号不能为空"); + } + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, orderId) + .one(); + if (order == null) { + return R.fail("订单不存在"); + } + if (!Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) { + return R.fail("无权查询该订单"); + } + + String status = order.getStatus(); + String message; + if (PayOrderStatus.PAY_COMPLE.getCode().equals(status)) { + message = "充值成功"; + } else if (PayOrderStatus.NO_PAY.getCode().equals(status)) { + message = "支付中"; + } else if (PayOrderStatus.CANCEL.getCode().equals(status)) { + message = "订单已取消"; + } else if (PayOrderStatus.CALL_BACK_ERRO.getCode().equals(status)) { + message = "回调处理异常"; + } else { + message = "支付失败"; + } + + Map data = new HashMap<>(); + data.put("status", status); + data.put("amount", order.getGoodsPrice()); + data.put("message", message); + return R.ok(data); + } + + // ======================================================================== + // 交易查询:/api/v2/transactions/query + // ======================================================================== + + public R queryPay(Long userId, String merchantOrderId) { + if (StrUtil.isBlank(merchantOrderId)) { + return R.fail("订单号不能为空"); + } + // 归属校验:只允许查询本人的 SnapPay 订单 + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, merchantOrderId) + .eq(TtOrder::getType, PayType.SNAP_PAY.getCode()) + .one(); + if (order == null) { + return R.fail("订单不存在"); + } + if (userId == null || !Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) { + return R.fail("无权查询该订单"); + } + + Map params = new LinkedHashMap<>(); + params.put("type", "API_DEPOSIT"); + params.put("merchant_id", properties.getMerchantId()); + params.put("merchant_order_id", merchantOrderId); + String signature = SnapPaySignUtil.sign(params, properties.getSecret()); + params.put("signature", signature); + + String queryUrl = properties.getBaseUrl() + "/api/v2/transactions/query"; + log.info("SnapPay queryOrder url={}, merchantOrderId={}", queryUrl, merchantOrderId); + try (HttpResponse response = HttpRequest.post(queryUrl) + .header("Content-Type", "application/json; charset=utf-8") + .body(SnapPaySignUtil.toJson(params)) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("SnapPay queryOrder response httpStatus={}, body={}", response.getStatus(), body); + JSONObject obj = JSONUtil.parseObj(body); + String status = obj.getStr("status"); + // PENDING/SUCCESS/FAILED 为有效交易状态;4xx/5xx 视为查询失败 + if (!"PENDING".equals(status) && !"SUCCESS".equals(status) && !"FAILED".equals(status)) { + return R.fail(StrUtil.isNotBlank(obj.getStr("message")) ? obj.getStr("message") : "查询失败"); + } + return R.ok(obj); + } catch (Exception e) { + log.error("SnapPay 查单异常", e); + return R.fail("查询失败"); + } + } + + // ======================================================================== + // 余额查询:/api/v2/balances + // ======================================================================== + + public R getBalance() { + Map params = new LinkedHashMap<>(); + params.put("channel", properties.getChannel()); + params.put("merchant_id", properties.getMerchantId()); + String signature = SnapPaySignUtil.sign(params, properties.getSecret()); + params.put("signature", signature); + + String balanceUrl = properties.getBaseUrl() + "/api/v2/balances"; + log.info("SnapPay getBalance url={}, channel={}", balanceUrl, properties.getChannel()); + try (HttpResponse response = HttpRequest.post(balanceUrl) + .header("Content-Type", "application/json; charset=utf-8") + .body(SnapPaySignUtil.toJson(params)) + .timeout(15000) + .execute()) { + String body = response.body(); + log.info("SnapPay getBalance response httpStatus={}, body={}", response.getStatus(), body); + JSONObject obj = JSONUtil.parseObj(body); + String balance = obj.getStr("balance"); + if (StrUtil.isBlank(balance)) { + return R.fail(StrUtil.isNotBlank(obj.getStr("message")) ? obj.getStr("message") : "查询失败"); + } + return R.ok(balance); + } catch (Exception e) { + log.error("SnapPay 余额查询异常", e); + return R.fail("查询失败"); + } + } + + // ======================================================================== + // 账户结算(与现有 hipay/richku/itpay/abkpay 逻辑保持一致) + // ======================================================================== + + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + + BigDecimal oldAmount = user.getAccountAmount(); + if (oldAmount == null) { + oldAmount = BigDecimal.ZERO; + } + BigDecimal newAmount = oldAmount.add(totalAmount); + + // 更新用户余额 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount); + userService.update(userUpdate); + + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + .amount(totalAmount) + .finalAmount(newAmount) + .total(totalAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + + // 充值记录 + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(order.getUserId()); + ttRechargeRecord.setParentId(user.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(newAmount); + ttRechargeRecord.setOrderId(order.getOrderId()); + ttRechargeRecord.setOutTradeNo(order.getOutTradeNo()); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("1"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + ttRechargeRecordMapper.insert(ttRechargeRecord); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } + + // ======================================================================== + // 工具方法 + // ======================================================================== + + /** 金额格式化为最多三位小数的纯字符串(例如 1000 -> "1000.000"),满足 amt 字段要求 */ + private String formatAmount(BigDecimal amount) { + if (amount == null) { + return "0.000"; + } + return amount.setScale(3, RoundingMode.HALF_UP).toPlainString(); + } + + private String plain(BigDecimal v) { + return v == null ? "" : v.stripTrailingZeros().toPlainString(); + } + + private String asStr(Object v) { + return v == null ? null : v.toString(); + } + + /** 构造客户邮箱(必填字段)。使用用户邮箱(ASCII 且含 @),否则用 user{uid}@{emailDomain} 补全 */ + private String buildEmail(TtUser user, Long uid) { + if (user != null) { + String email = user.getEmail(); + if (StrUtil.isNotBlank(email) && email.contains("@") && isAscii(email)) { + return email.trim(); + } + } + String domain = StrUtil.isNotBlank(properties.getEmailDomain()) ? properties.getEmailDomain() : "snap-user.com"; + return "user" + uid + "@" + domain; + } + + /** 构造客户手机号(必填字段)。取用户手机号中的数字,缺失时用占位号 */ + private String buildPhone(TtUser user) { + if (user != null && StrUtil.isNotBlank(user.getPhoneNumber())) { + String digits = user.getPhoneNumber().replaceAll("[^0-9]", ""); + if (digits.length() >= 6) { + return digits; + } + } + return "09000000000"; + } + + private boolean isAscii(String s) { + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) > 127) { + return false; + } + } + return true; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/utils/SnapPaySignUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/utils/SnapPaySignUtil.java new file mode 100644 index 0000000..f0e1074 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/snappay/utils/SnapPaySignUtil.java @@ -0,0 +1,102 @@ +package com.ruoyi.thirdparty.snappay.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Map; +import java.util.TreeMap; + +/** + * SnapPay 签名工具(对接文档「簽名算法」章节)。 + * 算法: + * 1. 移除请求体中的 signature 字段; + * 2. 忽略值为 null、"" (空字符串) 的字段,不参与签名; + * 3. 按字段名字典序(ASCII 升序)排序; + * 4. 标准 JSON 序列化(无多余空白、不转义斜杠 —— 使用 Jackson,与官方 Java/TypeScript 示例一致); + * 5. 序列化结果与 secretKey 直接串接(json + secretKey,无分隔符); + * 6. 计算 SHA256,输出 32 字节的小写十六进制字符串。 + * 注意:channel 为 number 型别,序列化为 900(不带引号);amt/amount 等金额为字符串。 + * 为规避不同语言 JSON 序列化对非 ASCII 字符转义差异带来的签名不一致, + * 参与签名的字符串字段(name/email/phone 等)在业务侧已保证为 ASCII。 + */ +@Slf4j +public class SnapPaySignUtil { + + /** 默认 Jackson:不缩进、不转义斜杠、非 ASCII 原样输出(UTF-8),与官方示例一致 */ + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * 生成签名。 + * + * @param params 参与签名的参数(可包含或不包含 signature,本方法会忽略它) + * @param secret 商户密钥 secretKey + * @return SHA256 小写十六进制签名 + */ + public static String sign(Map params, String secret) { + // 字典序排序 + 过滤 null / 空字符串 / signature + TreeMap sorted = new TreeMap<>(); + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + if ("signature".equals(key)) { + continue; + } + Object value = entry.getValue(); + if (value == null || "".equals(value)) { + continue; + } + sorted.put(key, value); + } + + try { + String json = MAPPER.writeValueAsString(sorted); + // 注意:签名原文含商户密钥,禁止打印到日志 + return sha256Hex(json + secret); + } catch (Exception e) { + throw new RuntimeException("SnapPay 签名序列化失败", e); + } + } + + /** + * 验证回调签名(大小写不敏感)。 + * + * @param params 完整回调参数(包含 signature) + * @param secret 商户密钥 secretKey + * @return 签名是否匹配 + */ + public static boolean verify(Map params, String secret) { + Object received = params.get("signature"); + if (received == null || received.toString().isEmpty()) { + log.error("SnapPay 回调缺少 signature"); + return false; + } + String calculated = sign(params, secret); + boolean valid = calculated.equalsIgnoreCase(received.toString()); + if (!valid) { + log.error("SnapPay 签名验证失败, expected={}, actual={}", calculated, received); + } + return valid; + } + + /** + * 使用与签名一致的 Jackson 序列化生成请求体 JSON(不转义斜杠,避免 notify_url 被转义)。 + */ + public static String toJson(Object obj) { + try { + return MAPPER.writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException("SnapPay JSON 序列化失败", e); + } + } + + private static String sha256Hex(String input) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(hash.length * 2); + for (byte b : hash) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/config/V5ItemConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/config/V5ItemConfig.java new file mode 100644 index 0000000..299ffe3 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/config/V5ItemConfig.java @@ -0,0 +1,21 @@ +package com.ruoyi.thirdparty.v5item.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "v5item") +public class V5ItemConfig { + + private String baseUrl; + + private String merchantKey; + + private String account; + + private String password; + + private String publicKey; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/controller/V5ItemController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/controller/V5ItemController.java new file mode 100644 index 0000000..090a7a1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/controller/V5ItemController.java @@ -0,0 +1,67 @@ +package com.ruoyi.thirdparty.v5item.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.thirdparty.v5item.domain.body.CheckTradeUrlBody; +import com.ruoyi.thirdparty.v5item.domain.body.CreateOrderBody; +import com.ruoyi.thirdparty.v5item.domain.body.GetItemListBody; +import com.ruoyi.thirdparty.v5item.domain.body.QueryOnSaleInfoBody; +import com.ruoyi.thirdparty.v5item.domain.dto.GetItemListDTO; +import com.ruoyi.thirdparty.v5item.domain.result.QueryMerchantInfoResult; +import com.ruoyi.thirdparty.v5item.domain.result.V5CallBackNotifyInfo; +import com.ruoyi.thirdparty.v5item.service.V5ItemService; +import com.ruoyi.thirdparty.zbt.service.ZBTService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/admin/v5item") +public class V5ItemController extends BaseController { + + @Autowired + private V5ItemService v5ItemService; + + @GetMapping("/test") + public AjaxResult test() { + return success(); + } + + @GetMapping("/queryMerchantInfo") + public AjaxResult queryMerchantInfo() { + return success(v5ItemService.queryMerchantInfo()); + } + + @GetMapping("/checkMerchantBalance") + public AjaxResult checkMerchantBalance() { + return success(v5ItemService.checkMerchantBalance()); + } + + @PostMapping("/getItemList") + public AjaxResult getItemList(@RequestBody GetItemListDTO getItemListDTO) { + return success(v5ItemService.getItemList(getItemListDTO)); + } + + @PostMapping("/checkTradeUrl") + public AjaxResult checkTradeUrl(@RequestBody CheckTradeUrlBody checkTradeUrlBody) { + return success(v5ItemService.checkTradeUrl(checkTradeUrlBody.getTradeUrl())); + } + + /** + * 查询库存 + */ + @PostMapping("/queryOnSaleInfo") + public AjaxResult queryOnSaleInfo(@Validated @RequestBody QueryOnSaleInfoBody queryOnSaleInfoBody) { + return success(v5ItemService.queryOnSaleInfo(queryOnSaleInfoBody)); + } + + @PostMapping("/createOrder") + public AjaxResult createOrder(@RequestBody CreateOrderBody createOrderBody) { + return success(v5ItemService.createOrder(createOrderBody)); + } + + @PostMapping("/v5Notify") + public String v5Notify(@RequestBody V5CallBackNotifyInfo v5CallBackNotifyInfo) { + return v5ItemService.v5Notify(v5CallBackNotifyInfo); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/CheckMerchantBalanceBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/CheckMerchantBalanceBody.java new file mode 100644 index 0000000..b4a4997 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/CheckMerchantBalanceBody.java @@ -0,0 +1,9 @@ +package com.ruoyi.thirdparty.v5item.domain.body; + +import lombok.Data; + +@Data +public class CheckMerchantBalanceBody { + + private String merchantKey; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/CheckTradeUrlBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/CheckTradeUrlBody.java new file mode 100644 index 0000000..5c2a30f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/CheckTradeUrlBody.java @@ -0,0 +1,11 @@ +package com.ruoyi.thirdparty.v5item.domain.body; + +import lombok.Data; + +@Data +public class CheckTradeUrlBody { + + private String merchantKey; + + private String tradeUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/CreateOrderBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/CreateOrderBody.java new file mode 100644 index 0000000..80300b4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/CreateOrderBody.java @@ -0,0 +1,17 @@ +package com.ruoyi.thirdparty.v5item.domain.body; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class CreateOrderBody { + + private String marketHashName; + + private BigDecimal purchasePrice; + + private String tradeUrl; + + private String merchantOrderNo; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/GetItemListBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/GetItemListBody.java new file mode 100644 index 0000000..1f33ae9 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/GetItemListBody.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.v5item.domain.body; + +import lombok.Data; + +@Data +public class GetItemListBody { + + private String merchantKey; + + /** 页码 */ + private Integer pageIndex; + + /** 每页条数 */ + private Integer pageNum; + + /** 饰品类型 */ + private String itemType; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/QueryMerchantInfoBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/QueryMerchantInfoBody.java new file mode 100644 index 0000000..2dcba07 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/QueryMerchantInfoBody.java @@ -0,0 +1,11 @@ +package com.ruoyi.thirdparty.v5item.domain.body; + +import lombok.Data; + +@Data +public class QueryMerchantInfoBody { + + private String account; + + private String password; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/QueryOnSaleInfoBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/QueryOnSaleInfoBody.java new file mode 100644 index 0000000..b7ed87b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/QueryOnSaleInfoBody.java @@ -0,0 +1,12 @@ +package com.ruoyi.thirdparty.v5item.domain.body; + +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Data +public class QueryOnSaleInfoBody { + + @NotNull(message = "marketHashName不能为空") + private String marketHashName; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/QueryOrderStatusBody.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/QueryOrderStatusBody.java new file mode 100644 index 0000000..17da6a4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/body/QueryOrderStatusBody.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.v5item.domain.body; + +import lombok.Data; + +@Data +public class QueryOrderStatusBody { + private String merchantKey; + private String merchantOrderNo; + private String orderNo; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/dto/CreateOrderDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/dto/CreateOrderDTO.java new file mode 100644 index 0000000..739f525 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/dto/CreateOrderDTO.java @@ -0,0 +1,21 @@ +package com.ruoyi.thirdparty.v5item.domain.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class CreateOrderDTO { + + private String marketHashName; + + private BigDecimal purchasePrice; + + private String tradeUrl; + + private String merchantOrderNo; + + private String merchantKey; + + private Integer buyType; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/dto/GetItemListDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/dto/GetItemListDTO.java new file mode 100644 index 0000000..ba73e22 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/dto/GetItemListDTO.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.v5item.domain.dto; + +import lombok.Data; + +@Data +public class GetItemListDTO { + + /** 页码 */ + private Integer pageIndex; + + /** 每页条数 */ + private Integer pageNum; + + /** 饰品类型 */ + private String itemType; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/dto/ReceiverInfoDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/dto/ReceiverInfoDTO.java new file mode 100644 index 0000000..1522c52 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/dto/ReceiverInfoDTO.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.v5item.domain.dto; + +import lombok.Data; + +/** + * 收货人信息 + */ +@Data +public class ReceiverInfoDTO { + + private String headPicture; + + private String steamCreateDate; + + private String nickname; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/CheckMerchantBalanceResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/CheckMerchantBalanceResult.java new file mode 100644 index 0000000..8b5ba26 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/CheckMerchantBalanceResult.java @@ -0,0 +1,15 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class CheckMerchantBalanceResult { + + private BigDecimal balance; + + private String merchantName; + + private String account; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/CheckTradeUrlResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/CheckTradeUrlResult.java new file mode 100644 index 0000000..2adced0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/CheckTradeUrlResult.java @@ -0,0 +1,17 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +@Data +public class CheckTradeUrlResult { + + private String steamId; + + private String steamCreateDate; + + private Integer status; + + private String statusMsg; + + private String steamName; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/CreateOrderResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/CreateOrderResult.java new file mode 100644 index 0000000..15d2301 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/CreateOrderResult.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import com.ruoyi.thirdparty.v5item.domain.dto.ReceiverInfoDTO; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class CreateOrderResult { + + private BigDecimal payAmount; + + private String merchantOrderNo; + + private String orderNo; + + private ReceiverInfoDTO receiverInfoDTO; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/GetItemListResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/GetItemListResult.java new file mode 100644 index 0000000..c0118b4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/GetItemListResult.java @@ -0,0 +1,37 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class GetItemListResult { + + private String marketHashName; + + private String itemName; + + private String shortItemName; + + private String itemType; + + private String itemTypeName; + + private String itemRarity; + + private String itemRarityName; + + private String itemExterior; + + private String itemExteriorName; + + private String itemQuality; + + private String itemQualityName; + + private Integer onSaleStock; + + private BigDecimal minSellPrice; + + private String itemIcon; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/QueryMerchantInfoResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/QueryMerchantInfoResult.java new file mode 100644 index 0000000..02a345d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/QueryMerchantInfoResult.java @@ -0,0 +1,11 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +@Data +public class QueryMerchantInfoResult { + + private String tradeToken; + + private String account; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/QueryOnSaleInfoResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/QueryOnSaleInfoResult.java new file mode 100644 index 0000000..78c87e2 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/QueryOnSaleInfoResult.java @@ -0,0 +1,43 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class QueryOnSaleInfoResult { + + private String marketHashName; + + private String itemName; + + private String shortItemName; + + private String itemType; + + private String itemTypeName; + + private String itemRarity; + + private String itemRarityName; + + private String itemExterior; + + private String itemExteriorName; + + private String itemQuality; + + private String itemQualityName; + + private Integer onSaleStock; + + private BigDecimal minSellPrice; + + private String itemIcon; + + private BigDecimal selfPrice; + + private BigDecimal playerPrice; + + private BigDecimal consultPrice; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/QueryOrderStatusResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/QueryOrderStatusResult.java new file mode 100644 index 0000000..c52830b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/QueryOrderStatusResult.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +@Data +public class QueryOrderStatusResult { + private String merchantOrderNo; + private String orderNo; + private Integer orderAmount; + private Integer status; + private String statusMsg; + private Integer deliverStatus; + private String deliverMsg; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5CallBackInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5CallBackInfo.java new file mode 100644 index 0000000..7c12513 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5CallBackInfo.java @@ -0,0 +1,15 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +@Data +public class V5CallBackInfo { + private String orderNo; + private String merchantNo; + private Integer userId; + private Integer orderStatus; + private String orderStatusDesc; + private Integer deliverStatus; + private String deliverStatusDesc; + private String failReason; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5CallBackNotifyInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5CallBackNotifyInfo.java new file mode 100644 index 0000000..45dc67b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5CallBackNotifyInfo.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +@Data +public class V5CallBackNotifyInfo { + private String notifyMsgNo; + private String callBackInfo; + private String sign; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5ItemResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5ItemResult.java new file mode 100644 index 0000000..5e82dba --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5ItemResult.java @@ -0,0 +1,13 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +@Data +public class V5ItemResult { + + private Integer code; + + private String msg; + + private T data; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5ItemResultContainTotal.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5ItemResultContainTotal.java new file mode 100644 index 0000000..f5a36a7 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/domain/result/V5ItemResultContainTotal.java @@ -0,0 +1,15 @@ +package com.ruoyi.thirdparty.v5item.domain.result; + +import lombok.Data; + +@Data +public class V5ItemResultContainTotal { + + private Integer code; + + private String msg; + + private T data; + + private Integer totalCount; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/service/V5ItemService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/service/V5ItemService.java new file mode 100644 index 0000000..d12930c --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/service/V5ItemService.java @@ -0,0 +1,28 @@ +package com.ruoyi.thirdparty.v5item.service; + +import com.ruoyi.thirdparty.v5item.domain.body.CreateOrderBody; +import com.ruoyi.thirdparty.v5item.domain.body.GetItemListBody; +import com.ruoyi.thirdparty.v5item.domain.body.QueryOnSaleInfoBody; +import com.ruoyi.thirdparty.v5item.domain.dto.GetItemListDTO; +import com.ruoyi.thirdparty.v5item.domain.result.*; + +import java.util.List; + +public interface V5ItemService { + + public V5ItemResult queryMerchantInfo(); + + public V5ItemResult checkMerchantBalance(); + + public V5ItemResultContainTotal> getItemList(GetItemListDTO getItemListDTO); + + public V5ItemResult checkTradeUrl(String tradeUrl); + + public QueryOnSaleInfoResult queryOnSaleInfo(QueryOnSaleInfoBody queryOnSaleInfoBody); + + public V5ItemResult createOrder(CreateOrderBody createOrderBody); + + String v5Notify(V5CallBackNotifyInfo v5CallBackNotifyInfo); + + QueryOrderStatusResult queryOrderStatus(String merchantOrderNo, String orderNo); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/service/impl/V5ItemServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/service/impl/V5ItemServiceImpl.java new file mode 100644 index 0000000..5e4046c --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/service/impl/V5ItemServiceImpl.java @@ -0,0 +1,386 @@ +package com.ruoyi.thirdparty.v5item.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.admin.mapper.TtDeliveryRecordMapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.http.HttpUtils; +import com.ruoyi.domain.common.constant.DeliveryOrderStatus; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.thirdparty.v5item.config.V5ItemConfig; +import com.ruoyi.thirdparty.v5item.domain.body.*; +import com.ruoyi.thirdparty.v5item.domain.dto.CreateOrderDTO; +import com.ruoyi.thirdparty.v5item.domain.dto.GetItemListDTO; +import com.ruoyi.thirdparty.v5item.domain.result.*; +import com.ruoyi.thirdparty.v5item.service.V5ItemService; +import com.ruoyi.thirdparty.yyyouping.utils.common.RSAUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Service +public class V5ItemServiceImpl implements V5ItemService { + + private static final Logger log = LoggerFactory.getLogger(V5ItemServiceImpl.class); + + private static final String V5ITEM_TOKEN_KEY = "v5item_token"; + + @Autowired + private V5ItemConfig v5ItemConfig; + + @Autowired + private RedisCache redisCache; + + @Autowired + private TtDeliveryRecordMapper deliveryRecordMapper; + + @Autowired + private TtBoxRecordsMapper boxRecordsMapper; + + @Override + public V5ItemResult queryMerchantInfo() { + String url = v5ItemConfig.getBaseUrl() + "/open/api/queryMerchantInfo"; + QueryMerchantInfoBody queryMerchantInfoBody = new QueryMerchantInfoBody(); + queryMerchantInfoBody.setAccount(v5ItemConfig.getAccount()); + queryMerchantInfoBody.setPassword(v5ItemConfig.getPassword()); + + String result = HttpUtils.sendPostJSONString(url, JSON.toJSONString(queryMerchantInfoBody)); + + // 定义V5ItemResult的泛型类型 + TypeReference> typeRef = + new TypeReference>() {}; + // 使用FastJson将JSON字符串转换为V5ItemResult对象 + V5ItemResult v5ItemResult = JSONObject.parseObject(result, typeRef); + if (v5ItemResult.getCode() != 0) + throw new RuntimeException("查询商户信息失败"); + + // 将v5itemToken存入redis + String token = v5ItemResult.getData().getTradeToken(); + redisCache.setCacheObject(V5ITEM_TOKEN_KEY, token, 10, TimeUnit.MINUTES); + + return v5ItemResult; + } + + @Override + public V5ItemResult checkMerchantBalance() { + String url = v5ItemConfig.getBaseUrl() + "/open/api/checkMerchantBalance"; + + // 查询v5itemToken + String v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + // 处理redis中不存在v5itemToken的情况 + if (Objects.isNull(v5itemToken)) { + queryMerchantInfo(); + v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + } + + // 构建请求头 + Map headers = new HashMap<>(); + headers.put("Authorization", v5itemToken); + + CheckMerchantBalanceBody checkMerchantBalanceBody = new CheckMerchantBalanceBody(); + checkMerchantBalanceBody.setMerchantKey(v5ItemConfig.getMerchantKey()); + String result = HttpUtils.sendPostJSONString(url, JSON.toJSONString(checkMerchantBalanceBody), headers); + + // 定义V5ItemResult的泛型类型 + TypeReference> typeRef = + new TypeReference>() {}; + // 使用FastJson将JSON字符串转换为V5ItemResult对象 + V5ItemResult v5ItemResult = JSONObject.parseObject(result, typeRef); + + // 处理token过期情况 + if (v5ItemResult.getCode() == 401) { + redisCache.deleteObject(V5ITEM_TOKEN_KEY); + return checkMerchantBalance(); + } + + return v5ItemResult; + } + + @Override + public V5ItemResultContainTotal> getItemList(GetItemListDTO getItemListDTO) { + String url = v5ItemConfig.getBaseUrl() + "/open/api/getItemList"; + + // 查询v5itemToken + String v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + // 处理redis中不存在v5itemToken的情况 + if (Objects.isNull(v5itemToken)) { + queryMerchantInfo(); + v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + } + + // 构建请求头 + Map headers = new HashMap<>(); + headers.put("Authorization", v5itemToken); + + GetItemListBody getItemListBody = new GetItemListBody(); + getItemListBody.setMerchantKey(v5ItemConfig.getMerchantKey()); + BeanUtils.copyProperties(getItemListDTO, getItemListBody); + String result = HttpUtils.sendPostJSONString(url, JSON.toJSONString(getItemListBody), headers); + + // 定义V5ItemResult的泛型类型 + TypeReference>> typeRef = + new TypeReference>>() {}; + // 使用FastJson将JSON字符串转换为V5ItemResult对象 + V5ItemResultContainTotal> v5ItemResult = JSONObject.parseObject(result, typeRef); + + // 处理token过期情况 + if (v5ItemResult.getCode() == 401) { + redisCache.deleteObject(V5ITEM_TOKEN_KEY); + return getItemList(getItemListDTO); + } + + return v5ItemResult; + } + + @Override + public V5ItemResult checkTradeUrl(String tradeUrl) { + String url = v5ItemConfig.getBaseUrl() + "/open/api/checkTradeUrl"; + + // 查询v5itemToken + String v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + // 处理redis中不存在v5itemToken的情况 + if (Objects.isNull(v5itemToken)) { + queryMerchantInfo(); + v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + } + + // 构建请求头 + Map headers = new HashMap<>(); + headers.put("Authorization", v5itemToken); + + CheckTradeUrlBody checkTradeUrlBody = new CheckTradeUrlBody(); + checkTradeUrlBody.setMerchantKey(v5ItemConfig.getMerchantKey()); + checkTradeUrlBody.setTradeUrl(tradeUrl); + String result = HttpUtils.sendPostJSONString(url, JSON.toJSONString(checkTradeUrlBody), headers); + + // 定义V5ItemResult的泛型类型 + TypeReference> typeRef = + new TypeReference>() {}; + // 使用FastJson将JSON字符串转换为V5ItemResult对象 + V5ItemResult v5ItemResult = JSONObject.parseObject(result, typeRef); + + // 处理token过期情况 + if (v5ItemResult.getCode() == 401) { + redisCache.deleteObject(V5ITEM_TOKEN_KEY); + return checkTradeUrl(tradeUrl); + } + + return v5ItemResult; + } + + @Override + public QueryOnSaleInfoResult queryOnSaleInfo(QueryOnSaleInfoBody queryOnSaleInfoBody) { + String url = v5ItemConfig.getBaseUrl() + "/open/api/queryOnSaleInfo"; + + // 查询v5itemToken + String v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + // 处理redis中不存在v5itemToken的情况 + if (Objects.isNull(v5itemToken)) { + queryMerchantInfo(); + v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + } + + // 构建请求头 + Map headers = new HashMap<>(); + headers.put("Authorization", v5itemToken); + + List marketHashNameList = new ArrayList<>(); + marketHashNameList.add(queryOnSaleInfoBody.getMarketHashName()); + Map bodyMap = new HashMap<>(); + bodyMap.put("templateHashNameList", marketHashNameList); + bodyMap.put("merchantKey", v5ItemConfig.getMerchantKey()); + String result = HttpUtils.sendPostJSONString(url, JSON.toJSONString(bodyMap), headers); + + // 将JSON转换为对象 + // 定义V5ItemResult的泛型类型 + TypeReference>> typeRef = + new TypeReference>>() {}; + // 使用FastJson将JSON字符串转换为V5ItemResult对象 + V5ItemResult> v5ItemResult = JSONObject.parseObject(result, typeRef); + + // 处理token过期情况 + if (v5ItemResult.getCode() == 401) { + redisCache.deleteObject(V5ITEM_TOKEN_KEY); + return queryOnSaleInfo(queryOnSaleInfoBody); + } + + if (!Objects.isNull(v5ItemResult.getData()) && v5ItemResult.getData().size() > 0) { + return v5ItemResult.getData().get(0); + } + return null; + } + + @Override + public V5ItemResult createOrder(CreateOrderBody createOrderBody) { + String url = v5ItemConfig.getBaseUrl() + "/open/api/createOrderByMarketHashName"; + + // 查询v5itemToken + String v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + // 处理redis中不存在v5itemToken的情况 + if (Objects.isNull(v5itemToken)) { + queryMerchantInfo(); + v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + } + + // 构建请求头 + Map headers = new HashMap<>(); + headers.put("Authorization", v5itemToken); + + CreateOrderDTO createOrderDTO = new CreateOrderDTO(); + BeanUtils.copyProperties(createOrderBody, createOrderDTO); + createOrderDTO.setMerchantKey(v5ItemConfig.getMerchantKey()); + createOrderDTO.setBuyType(0); + String result = HttpUtils.sendPostJSONString(url, JSON.toJSONString(createOrderDTO), headers); + + // 将JSON转换为对象 + // 定义V5ItemResult的泛型类型 + TypeReference> typeRef = + new TypeReference>() {}; + // 使用FastJson将JSON字符串转换为V5ItemResult对象 + V5ItemResult v5ItemResult = JSONObject.parseObject(result, typeRef); + + // 处理token过期情况 + if (v5ItemResult.getCode() == 401) { + redisCache.deleteObject(V5ITEM_TOKEN_KEY); + return createOrder(createOrderBody); + } + + return v5ItemResult; + } + + @Override + public String v5Notify(V5CallBackNotifyInfo v5CallBackNotifyInfo) { + log.info("收到v5订单更改回调通知。入参:{}", v5CallBackNotifyInfo.toString()); + String callBackInfo = v5CallBackNotifyInfo.getCallBackInfo(); + String notifyMsgNo = v5CallBackNotifyInfo.getNotifyMsgNo(); + String sign = v5CallBackNotifyInfo.getSign(); + Map params = new HashMap<>(); + params.put("notifyMsgNo", notifyMsgNo); + params.put("callBackInfo", callBackInfo); + // 第一步:检查参数是否已经排序 + String[] keys = params.keySet().toArray(new String[0]); + Arrays.sort(keys); + // 第二步:把所有参数名和参数值串在一起 + StringBuilder stringBuilder = new StringBuilder(); + for (String key : keys) { + Object value = params.get(key); + if (value != null) { + stringBuilder.append(key).append(value); + } + } + log.info("stringBuilder:{}",stringBuilder); + boolean b = false; + try { + b = RSAUtils.verifyByPublicKey(stringBuilder.toString().getBytes(), v5ItemConfig.getPublicKey(), sign); + } catch (Exception e) { + log.error("v5Notify happen excepiton is {}", e.getMessage(), e); + } + if (!b) { + log.info("b is :{}, v5ItemConfig.getMerchantKey() is {}", b, v5ItemConfig.getPublicKey()); + return "failed"; + } + ObjectMapper objectMapper = new ObjectMapper(); + V5CallBackInfo info = objectMapper.convertValue(callBackInfo, V5CallBackInfo.class); + // 获取提货订单数据 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TtDeliveryRecord::getOrderId, info.getOrderNo()); + TtDeliveryRecord ttDeliveryRecord = deliveryRecordMapper.selectOne(queryWrapper); + if (Objects.equals(info.getOrderStatus(), 2)) { + ttDeliveryRecord.setStatus(DeliveryOrderStatus.DELIVERY_AFTER.getCode()); + ttDeliveryRecord.setMessage(DeliveryOrderStatus.DELIVERY_AFTER.getMsg()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + deliveryRecordMapper.updateById(ttDeliveryRecord); + } else if (Objects.equals(info.getOrderStatus(), 3)) { + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_COMPLETE.getCode()); + ttDeliveryRecord.setMessage(DeliveryOrderStatus.ORDER_COMPLETE.getMsg()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + deliveryRecordMapper.updateById(ttDeliveryRecord); + // 联动:订单完成后将对应 box_records 标为「已经提货」,不再在背包 / 提货背包展示,不可分解 / 转赠 + markBoxAsDelivered(ttDeliveryRecord.getBoxRecordsId()); + } else if (Objects.equals(info.getOrderStatus(), 4)) { + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_CANCEL.getCode()); + ttDeliveryRecord.setMessage(DeliveryOrderStatus.ORDER_CANCEL.getMsg()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + deliveryRecordMapper.updateById(ttDeliveryRecord); + } + log.info("验签结果:{}", b); + return"success"; + } + + /** + * 将提货记录对应的 box_records 状态从 APPLY_DELIVERY(2) 走到 DELIVERY_YET(3)。 + * 仅在当前状态确为「申请提货」时才写入,避免覆盖其他状态。 + */ + private void markBoxAsDelivered(Long boxRecordsId) { + if (boxRecordsId == null) { + return; + } + TtBoxRecords box = boxRecordsMapper.selectById(boxRecordsId); + if (box != null && TtboxRecordStatus.APPLY_DELIVERY.getCode().equals(box.getStatus())) { + box.setStatus(TtboxRecordStatus.DELIVERY_YET.getCode()); + box.setUpdateTime(DateUtils.getNowDate()); + boxRecordsMapper.updateById(box); + } + } + + @Override + public QueryOrderStatusResult queryOrderStatus(String merchantOrderNo, String orderNo) { + String url = v5ItemConfig.getBaseUrl() + "/open/api/queryOrderStatus"; + QueryOrderStatusBody body = new QueryOrderStatusBody(); + body.setMerchantKey(v5ItemConfig.getMerchantKey()); + body.setMerchantOrderNo(merchantOrderNo); + body.setOrderNo(orderNo); + // 查询v5itemToken + String v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + // 处理redis中不存在v5itemToken的情况 + if (Objects.isNull(v5itemToken)) { + queryMerchantInfo(); + v5itemToken = redisCache.getCacheObject(V5ITEM_TOKEN_KEY); + } + + // 构建请求头 + Map headers = new HashMap<>(); + headers.put("Authorization", v5itemToken); + + String postBody = JSON.toJSONString(body); + log.info("query order status body is {}", postBody); + String result = HttpUtils.sendPostJSONString(url, postBody, headers); + log.info("query order status result is {}", result); + if (StringUtils.isBlank(result)) { + return null; + } + // 将JSON转换为对象 + // 定义V5ItemResult的泛型类型 + TypeReference typeRef = + new TypeReference() {}; + // 使用FastJson将JSON字符串转换为V5ItemResult对象 + return JSONObject.parseObject(result, typeRef); + } + + private String postMethod(Map param){ + StringBuilder params = new StringBuilder(); + for (Map.Entry entry : param.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + params.append(key); + params.append("="); + params.append(value); + params.append("&"); + } + params.trimToSize(); + return params.substring(0, params.length() - 1); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/task/V5ItemPullTask.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/task/V5ItemPullTask.java new file mode 100644 index 0000000..b10f4a4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/v5item/task/V5ItemPullTask.java @@ -0,0 +1,146 @@ +package com.ruoyi.thirdparty.v5item.task; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.v5item.domain.dto.GetItemListDTO; +import com.ruoyi.thirdparty.v5item.domain.result.GetItemListResult; +import com.ruoyi.thirdparty.v5item.domain.result.V5ItemResultContainTotal; +import com.ruoyi.thirdparty.v5item.service.V5ItemService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Component("V5ItemPullTask") +public class V5ItemPullTask { + + @Autowired + private TtOrnamentService ornamentsService; + + @Autowired + private V5ItemService v5ItemService; + + @Autowired + private ISysConfigService configService; + + // @Scheduled(cron = "0 0 */3 * * ?") + public void getOrnamentsData() { + log.info("更新数据开始"); + String usePricePremiumRateStr = configService.selectConfigByKey("usePricePremiumRate"); + BigDecimal usePricePremiumRate = new BigDecimal(usePricePremiumRateStr); + + String ZBTParitiesStr = configService.selectConfigByKey("ZBTParities"); + BigDecimal parities = new BigDecimal(ZBTParitiesStr); + + // 查询数据库 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // wrapper.eq(TtOrnament::getIsProprietaryProperty, "1"); + List ttOrnamentList = ornamentsService.list(wrapper); + int i = 1; + while (true) { + List updateList = new ArrayList<>(); + // 获取初次外部数据 + GetItemListDTO getItemListDTO = new GetItemListDTO(); + getItemListDTO.setPageIndex(i); + getItemListDTO.setPageNum(500); + V5ItemResultContainTotal> v5ItemResult = v5ItemService.getItemList(getItemListDTO); + List getItemListResult = v5ItemResult.getData(); + if (CollectionUtils.isEmpty(getItemListResult)) { + break; + } + i++; + // 比对更新数据 + for (GetItemListResult itemListResult : getItemListResult) { + for (TtOrnament ttOrnament : ttOrnamentList) { + if (itemListResult.getMarketHashName().equals(ttOrnament.getMarketHashName())) { + if (!ttOrnament.getPrice().equals(itemListResult.getMinSellPrice()) || + !ttOrnament.getQuantity().equals(itemListResult.getOnSaleStock())) { + // 加入到更新队列 + BigDecimal price = itemListResult.getMinSellPrice(); + BigDecimal userPrice = price.multiply(parities).multiply(usePricePremiumRate); + if (userPrice.compareTo(new BigDecimal("99999999.99")) > 0) + continue; + ttOrnament.setPrice(price); + ttOrnament.setQuantity(itemListResult.getOnSaleStock()); + ttOrnament.setUsePrice(userPrice); + ttOrnament.setUpdateTime(DateUtils.getNowDate()); + updateList.add(ttOrnament); + } + } + } + } + if (CollectionUtils.isNotEmpty(updateList)) { + batchUpdateOrnaments(updateList); + } + } + + /*// 获取剩余外部数据 + Integer total = v5ItemResult.getTotalCount(); + int pageSize = 500; + int pageNumCount = (total % pageSize == 0) ? total / pageSize : total / pageSize + 1; + for (int i = 2; i <= pageNumCount; i++) { + getItemListDTO.setPageIndex(i); + if (i == pageNumCount) + getItemListDTO.setPageNum(total % pageSize); + v5ItemResult = v5ItemService.getItemList(getItemListDTO); + getItemListResult = v5ItemResult.getData(); + // 比对更新数据 + updateList = new ArrayList<>(); // 每请求一批数据清空更新列表 + for (GetItemListResult itemListResult : getItemListResult) { + for (TtOrnament ttOrnament : ttOrnamentList) { + if (itemListResult.getMarketHashName().equals(ttOrnament.getMarketHashName())) { + if (!ttOrnament.getPrice().equals(itemListResult.getMinSellPrice()) || + !ttOrnament.getQuantity().equals(itemListResult.getOnSaleStock())) { + // 加入到更新队列 + BigDecimal price = itemListResult.getMinSellPrice(); + BigDecimal userPrice = price.multiply(parities).multiply(usePricePremiumRate); + if (userPrice.compareTo(new BigDecimal("99999999.99")) > 0) + continue; + ttOrnament.setPrice(price); + ttOrnament.setQuantity(itemListResult.getOnSaleStock()); + ttOrnament.setUsePrice(userPrice); + ttOrnament.setUpdateTime(DateUtils.getNowDate()); + updateList.add(ttOrnament); + } + } + } + } + batchUpdateOrnaments(updateList); + }*/ + + log.info("更新数据完成"); + } + + private void batchInsertOrnaments() { + } + + private void batchUpdateOrnaments(List updateList) { + ornamentsService.updateBatchById(updateList, updateList.size()); + log.info("更新{}条", updateList.size()); + } + + private void batchSoldOutOrnaments() { + } + + private Map objectToMap(List list) { + Map map = new HashMap<>(); + for (SysDictData sysDictData : list) { + map.put(sysDictData.getDictLabel(), sysDictData.getDictValue()); + } + return map; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/config/TianXinProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/config/TianXinProperties.java new file mode 100644 index 0000000..7b476ad --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/config/TianXinProperties.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.wechat.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "tianxin") +public class TianXinProperties { + + private String version; // 版本号 + private String customerid; // 商户编号 + private String paytype; // 支付编号 + private String notifyurl; // 异步通知URL + private String returnurl; // 同步通知URL + private String access_type; // 接入方式 + private String userkey; // 商户Key + private String apiurl; // 请求地址 +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/controller/TianXinController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/controller/TianXinController.java new file mode 100644 index 0000000..9949fe9 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/controller/TianXinController.java @@ -0,0 +1,74 @@ +package com.ruoyi.thirdparty.wechat.controller; + +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.wechat.domain.PayOrderParam; +import com.ruoyi.thirdparty.wechat.service.ApiTokenService; +import com.ruoyi.thirdparty.wechat.service.TianXinService; +import com.ruoyi.thirdparty.wechat.utils.GetIpUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; + +import static com.ruoyi.common.utils.SecurityUtils.getUserId; + +@Api(tags = "田心支付") +@RestController +@RequestMapping("/api/tianxinOrder") +public class TianXinController { + + private final ApiTokenService tokenService; + private final TianXinService tianXinService; + private final TtUserService ttUserService; + + public TianXinController(ApiTokenService tokenService, + TianXinService tianXinService, + TtUserService ttUserService) { + this.tokenService = tokenService; + this.tianXinService = tianXinService; + this.ttUserService = ttUserService; + } + + @ApiOperation("预下单") + @UserPermission + @PostMapping("/createOrder") + public AjaxResult createOrder(@RequestBody PayOrderParam param, HttpServletRequest request) { + + // 判断是否开启田心支付 + // if (!("true".equals(sysConfigService.selectConfigByKey("tianxinpay")))) { + // return AjaxResult.error("当前系统没有开启田心支付!"); + // } + + return AjaxResult.success("尚未开放。"); + + // TtUser ttUser = ttUserService.getById(getUserId()); + // // 是否实名认证0未认证,1已认证' + // if (ttUser.getIsRealCheck().equals("0")) { + // return AjaxResult.error("请实名认证后再进行充值!"); + // } + // + // String ip = GetIpUtil.getIpAddress(request); + // return tianXinService.createOrder(param, ttUser, ip); + } + + @ApiOperation("查询订单状态") + @UserPermission + @PostMapping(value = "/queryOrderStatus") + public AjaxResult queryOrderStatus(String sdorderno) { + return tianXinService.queryOrderStatus(sdorderno); + } + + @ApiOperation("回调") + // @RequestMapping(value = "/callBack") + public String returnCallBack(HttpServletRequest request) { + return tianXinService.callBack(request); + } + +} \ No newline at end of file diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/ApiLoginUser.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/ApiLoginUser.java new file mode 100644 index 0000000..d29d75c --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/ApiLoginUser.java @@ -0,0 +1,237 @@ +package com.ruoyi.thirdparty.wechat.domain; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.ruoyi.domain.entity.sys.TtUser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +/** + * 登录用户身份权限 + * + * @author ruoyi + */ +public class ApiLoginUser implements UserDetails { + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地点 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 用户信息 + */ + private TtUser user; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Long getDeptId() { + return deptId; + } + + public void setDeptId(Long deptId) { + this.deptId = deptId; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public ApiLoginUser() { + } + + public ApiLoginUser(TtUser user, Set permissions) { + this.user = user; + this.permissions = permissions; + } + + public ApiLoginUser(Long userId, Long deptId, TtUser user, Set permissions) { + this.userId = userId; + this.deptId = deptId; + this.user = user; + this.permissions = permissions; + } + + @JSONField(serialize = false) + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUserName(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonExpired() { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonLocked() { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + /** + * 是否可用 ,禁用的用户不能身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isEnabled() { + return true; + } + + public Long getLoginTime() { + return loginTime; + } + + public void setLoginTime(Long loginTime) { + this.loginTime = loginTime; + } + + public String getIpaddr() { + return ipaddr; + } + + public void setIpaddr(String ipaddr) { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) { + this.loginLocation = loginLocation; + } + + public String getBrowser() { + return browser; + } + + public void setBrowser(String browser) { + this.browser = browser; + } + + public String getOs() { + return os; + } + + public void setOs(String os) { + this.os = os; + } + + public Long getExpireTime() { + return expireTime; + } + + public void setExpireTime(Long expireTime) { + this.expireTime = expireTime; + } + + public Set getPermissions() { + return permissions; + } + + public void setPermissions(Set permissions) { + this.permissions = permissions; + } + + public TtUser getUser() { + return user; + } + + public void setUser(TtUser user) { + this.user = user; + this.userId = Long.parseLong(user.getUserId() + ""); + + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/PayOrderParam.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/PayOrderParam.java new file mode 100644 index 0000000..614ab71 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/PayOrderParam.java @@ -0,0 +1,33 @@ +package com.ruoyi.thirdparty.wechat.domain; + +import java.math.BigDecimal; + +public class PayOrderParam { + private Integer coinItemId;//商户商品id + private Integer coinItemNum; //商户商品数量 + private BigDecimal coinItemAmount; //商户商品价格 + + public Integer getCoinItemId() { + return coinItemId; + } + + public void setCoinItemId(Integer coinItemId) { + this.coinItemId = coinItemId; + } + + public Integer getCoinItemNum() { + return coinItemNum; + } + + public void setCoinItemNum(Integer coinItemNum) { + this.coinItemNum = coinItemNum; + } + + public BigDecimal getCoinItemAmount() { + return coinItemAmount; + } + + public void setCoinItemAmount(BigDecimal coinItemAmount) { + this.coinItemAmount = coinItemAmount; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/PtLoginUser.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/PtLoginUser.java new file mode 100644 index 0000000..ba64cd9 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/PtLoginUser.java @@ -0,0 +1,277 @@ +package com.ruoyi.thirdparty.wechat.domain; + +import com.alibaba.fastjson2.annotation.JSONField; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +/** + * 登录用户身份权限 + * + * @author ruoyi + */ +public class PtLoginUser implements UserDetails +{ + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地点 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 平台用户信息 + */ + private PtUserVO user; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public String getToken() + { + return token; + } + + public void setToken(String token) + { + this.token = token; + } + + public PtLoginUser() + { + } + + + public PtLoginUser(PtUserVO user, Set permissions) + { + this.user = user; + this.permissions = permissions; + } + + public PtLoginUser(Long userId, Long deptId, PtUserVO user, Set permissions) + { + this.userId = userId; + this.deptId = deptId; + this.user = user; + this.permissions = permissions; + } + + public PtLoginUser(Long userId, Long deptId, PtUserVO user) + { + this.userId = userId; + this.deptId = deptId; + this.user = user; + } + + @JSONField(serialize = false) + @Override + public String getPassword() + { + return user.getPassword(); + } + + @Override + public String getUsername() + { + return user.getName(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonExpired() + { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonLocked() + { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isCredentialsNonExpired() + { + return true; + } + + /** + * 是否可用 ,禁用的用户不能身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isEnabled() + { + return true; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getExpireTime() + { + return expireTime; + } + + public void setExpireTime(Long expireTime) + { + this.expireTime = expireTime; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + public PtUserVO getUser() + { + return user; + } + + public void setUser(PtUserVO user) + { + this.user = user; + this.userId = user.getId(); + + } + + @Override + public Collection getAuthorities() + { + return Collections.emptyList(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/PtUserVO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/PtUserVO.java new file mode 100644 index 0000000..d04278d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/PtUserVO.java @@ -0,0 +1,576 @@ +package com.ruoyi.thirdparty.wechat.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 平台用户信息管理对象 tt_user + * + * @author junhai + * @date 2023-08-12 + */ +public class PtUserVO extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** $column.columnComment */ + private Long id; + + /** steam id */ + @Excel(name = "steam id") + private Long steamid; + + /** 用户名称 */ + @Excel(name = "用户名称") + private String name; + + /** 用户头像 */ + @Excel(name = "用户头像") + private String portrait; + + /** 手机号 */ + @Excel(name = "手机号") + private String phone; + + /** 登录密码 */ + @Excel(name = "登录密码") + private String password; + + /** 登录密码错误次数(最多四次) */ + @Excel(name = "登录密码错误次数(最多四次)") + private Integer passwordError; + + /** 邮箱账号 */ + @Excel(name = "邮箱账号") + private String email; + + /** 用户余额 */ + @Excel(name = "用户余额") + private BigDecimal bean; + + /** 商城余额 */ + @Excel(name = "商城余额") + private BigDecimal storeBean; + + /** 上级id */ + @Excel(name = "上级id") + private Long superiorId; + + /** 邀请码 */ + @Excel(name = "邀请码") + private String invitationCode; + + /** 交易链接 */ + @Excel(name = "交易链接") + private String transactionLink; + + /** 用户类型 1普通用户 2主播 3推广 4官方机器人 */ + @Excel(name = "用户类型 1普通用户 2主播 3推广 4官方机器人") + private Integer type; + + /** 推广等级 */ + @Excel(name = "推广等级") + private Long spreadGrade; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private BigDecimal spreadAmount; + + /** 爆率等级 */ + @Excel(name = "爆率等级") + private Long winningGrade; + + /** 当前爆率是否增加概率 */ + @Excel(name = "当前爆率是否增加概率") + private Integer winningAdd; + + /** 上次爆率等级结束时间 */ + @Excel(name = "上次爆率等级结束时间") + private Long winningTime; + + /** 充值是否可更改爆率等级 1是 0否 */ + @Excel(name = "充值是否可更改爆率等级 1是 0否") + private Integer winningStatus; + + /** 加入时间 */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "加入时间", width = 30, dateFormat = "yyyy-MM-dd") + private Date time; + + /** 最后登录时间 */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd") + private Date endTime; + + /** vip到期时间 */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "vip到期时间", width = 30, dateFormat = "yyyy-MM-dd") + private Date vipTime; + + /** vip服务 1独特的优质会员标识 2私人资料 3免费赠品 4优质会员箱子 */ + @Excel(name = "vip服务 1独特的优质会员标识 2私人资料 3免费赠品 4优质会员箱子") + private String vipService; + + /** ip地址 */ + @Excel(name = "ip地址") + private String ip; + + /** 登录状态 1正常 0禁止 */ + @Excel(name = "登录状态 1正常 0禁止") + private Integer status; + + /** 充值提货状态 1正常 0禁止 */ + @Excel(name = "充值提货状态 1正常 0禁止") + private Integer status2; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private Integer rewardNewUser; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private Long lotteryTimes; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private BigDecimal shipmentPrice; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private BigDecimal rechargeHour; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private BigDecimal recharge12hour; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private BigDecimal recharge24hour; + + /** 最后登录IP */ + @Excel(name = "最后登录IP") + private String loginIp; + + /** 最后登录时间 */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd") + private Date loginDate; + + /** 删除标志(0代表存在 2代表删除) */ + private Long delFlag; + + /** 真实姓名 */ + @Excel(name = "真实姓名") + private String realName; + + /** 身份证号码 */ + @Excel(name = "身份证号码") + private String idNumber; + + /** 是否实名认证0未认证,1已认证 */ + @Excel(name = "是否实名认证0未认证,1已认证") + private Long isRealCheck; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setSteamid(Long steamid) + { + this.steamid = steamid; + } + + public Long getSteamid() + { + return steamid; + } + public void setName(String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + public void setPortrait(String portrait) + { + this.portrait = portrait; + } + + public String getPortrait() + { + return portrait; + } + public void setPhone(String phone) + { + this.phone = phone; + } + + public String getPhone() + { + return phone; + } + public void setPassword(String password) + { + this.password = password; + } + + public String getPassword() + { + return password; + } + public void setPasswordError(Integer passwordError) + { + this.passwordError = passwordError; + } + + public Integer getPasswordError() + { + return passwordError; + } + public void setEmail(String email) + { + this.email = email; + } + + public String getEmail() + { + return email; + } + public void setBean(BigDecimal bean) + { + this.bean = bean; + } + + public BigDecimal getBean() + { + return bean; + } + public void setStoreBean(BigDecimal storeBean) + { + this.storeBean = storeBean; + } + + public BigDecimal getStoreBean() + { + return storeBean; + } + public void setSuperiorId(Long superiorId) + { + this.superiorId = superiorId; + } + + public Long getSuperiorId() + { + return superiorId; + } + public void setInvitationCode(String invitationCode) + { + this.invitationCode = invitationCode; + } + + public String getInvitationCode() + { + return invitationCode; + } + public void setTransactionLink(String transactionLink) + { + this.transactionLink = transactionLink; + } + + public String getTransactionLink() + { + return transactionLink; + } + public void setType(Integer type) + { + this.type = type; + } + + public Integer getType() + { + return type; + } + public void setSpreadGrade(Long spreadGrade) + { + this.spreadGrade = spreadGrade; + } + + public Long getSpreadGrade() + { + return spreadGrade; + } + public void setSpreadAmount(BigDecimal spreadAmount) + { + this.spreadAmount = spreadAmount; + } + + public BigDecimal getSpreadAmount() + { + return spreadAmount; + } + public void setWinningGrade(Long winningGrade) + { + this.winningGrade = winningGrade; + } + + public Long getWinningGrade() + { + return winningGrade; + } + public void setWinningAdd(Integer winningAdd) + { + this.winningAdd = winningAdd; + } + + public Integer getWinningAdd() + { + return winningAdd; + } + public void setWinningTime(Long winningTime) + { + this.winningTime = winningTime; + } + + public Long getWinningTime() + { + return winningTime; + } + public void setWinningStatus(Integer winningStatus) + { + this.winningStatus = winningStatus; + } + + public Integer getWinningStatus() + { + return winningStatus; + } + public void setTime(Date time) + { + this.time = time; + } + + public Date getTime() + { + return time; + } + public void setEndTime(Date endTime) + { + this.endTime = endTime; + } + + public Date getEndTime() + { + return endTime; + } + public void setVipTime(Date vipTime) + { + this.vipTime = vipTime; + } + + public Date getVipTime() + { + return vipTime; + } + public void setVipService(String vipService) + { + this.vipService = vipService; + } + + public String getVipService() + { + return vipService; + } + public void setIp(String ip) + { + this.ip = ip; + } + + public String getIp() + { + return ip; + } + public void setStatus(Integer status) + { + this.status = status; + } + + public Integer getStatus() + { + return status; + } + public void setStatus2(Integer status2) + { + this.status2 = status2; + } + + public Integer getStatus2() + { + return status2; + } + public void setRewardNewUser(Integer rewardNewUser) + { + this.rewardNewUser = rewardNewUser; + } + + public Integer getRewardNewUser() + { + return rewardNewUser; + } + public void setLotteryTimes(Long lotteryTimes) + { + this.lotteryTimes = lotteryTimes; + } + + public Long getLotteryTimes() + { + return lotteryTimes; + } + public void setShipmentPrice(BigDecimal shipmentPrice) + { + this.shipmentPrice = shipmentPrice; + } + + public BigDecimal getShipmentPrice() + { + return shipmentPrice; + } + public void setRechargeHour(BigDecimal rechargeHour) + { + this.rechargeHour = rechargeHour; + } + + public BigDecimal getRechargeHour() + { + return rechargeHour; + } + public void setRecharge12hour(BigDecimal recharge12hour) + { + this.recharge12hour = recharge12hour; + } + + public BigDecimal getRecharge12hour() + { + return recharge12hour; + } + public void setRecharge24hour(BigDecimal recharge24hour) + { + this.recharge24hour = recharge24hour; + } + + public BigDecimal getRecharge24hour() + { + return recharge24hour; + } + public void setLoginIp(String loginIp) + { + this.loginIp = loginIp; + } + + public String getLoginIp() + { + return loginIp; + } + public void setLoginDate(Date loginDate) + { + this.loginDate = loginDate; + } + + public Date getLoginDate() + { + return loginDate; + } + public void setDelFlag(Long delFlag) + { + this.delFlag = delFlag; + } + + public Long getDelFlag() + { + return delFlag; + } + public void setRealName(String realName) + { + this.realName = realName; + } + + public String getRealName() + { + return realName; + } + public void setIdNumber(String idNumber) + { + this.idNumber = idNumber; + } + + public String getIdNumber() + { + return idNumber; + } + public void setIsRealCheck(Long isRealCheck) + { + this.isRealCheck = isRealCheck; + } + + public Long getIsRealCheck() + { + return isRealCheck; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("steamid", getSteamid()) + .append("name", getName()) + .append("portrait", getPortrait()) + .append("phone", getPhone()) + .append("password", getPassword()) + .append("passwordError", getPasswordError()) + .append("email", getEmail()) + .append("bean", getBean()) + .append("storeBean", getStoreBean()) + .append("superiorId", getSuperiorId()) + .append("invitationCode", getInvitationCode()) + .append("transactionLink", getTransactionLink()) + .append("type", getType()) + .append("spreadGrade", getSpreadGrade()) + .append("spreadAmount", getSpreadAmount()) + .append("winningGrade", getWinningGrade()) + .append("winningAdd", getWinningAdd()) + .append("winningTime", getWinningTime()) + .append("winningStatus", getWinningStatus()) + .append("time", getTime()) + .append("endTime", getEndTime()) + .append("vipTime", getVipTime()) + .append("vipService", getVipService()) + .append("ip", getIp()) + .append("status", getStatus()) + .append("status2", getStatus2()) + .append("rewardNewUser", getRewardNewUser()) + .append("lotteryTimes", getLotteryTimes()) + .append("shipmentPrice", getShipmentPrice()) + .append("rechargeHour", getRechargeHour()) + .append("recharge12hour", getRecharge12hour()) + .append("recharge24hour", getRecharge24hour()) + .append("loginIp", getLoginIp()) + .append("loginDate", getLoginDate()) + .append("delFlag", getDelFlag()) + .append("realName", getRealName()) + .append("idNumber", getIdNumber()) + .append("isRealCheck", getIsRealCheck()) + .toString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/TtCoinFirstChargeBonus.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/TtCoinFirstChargeBonus.java new file mode 100644 index 0000000..ec6cd49 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/TtCoinFirstChargeBonus.java @@ -0,0 +1,29 @@ +package com.ruoyi.thirdparty.wechat.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TtCoinFirstChargeBonus implements Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + private Long cid; // 充值商品ID + private BigDecimal keyCoin; // 充值金额 + private BigDecimal rewards; // 奖励价值 + private Long oid; // 代金券饰品ID + private Integer num; // 奖励数量(默认5) + private Integer status; // 开启状态(默认1) 1开启 0关闭 + private String name; + private String img; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/TtCoinFirstChargeBonusRecord.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/TtCoinFirstChargeBonusRecord.java new file mode 100644 index 0000000..ae4c883 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/TtCoinFirstChargeBonusRecord.java @@ -0,0 +1,30 @@ +package com.ruoyi.thirdparty.wechat.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TtCoinFirstChargeBonusRecord implements Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + private Long uid; // 用户ID + private Long fcbid; // 首充奖励表ID + private BigDecimal rewards; // 奖励价值 + private Integer status; // 冻结状态(默认1) 0解冻 1冻结 2销毁 + private Date createTime; // 创建时间 + private Date latestUseTime; // 最晚使用时间 + private Date useTime; // 使用时间 + private String remark; // 备注 +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/TtCoinFirstChargeBonusRecordParam.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/TtCoinFirstChargeBonusRecordParam.java new file mode 100644 index 0000000..08ec8f1 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/domain/TtCoinFirstChargeBonusRecordParam.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.wechat.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TtCoinFirstChargeBonusRecordParam { + private Long id; + private Long uid; + private Integer status; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TianXinOrder.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TianXinOrder.java new file mode 100644 index 0000000..34926ed --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TianXinOrder.java @@ -0,0 +1,36 @@ +package com.ruoyi.thirdparty.wechat.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Builder +public class TianXinOrder { + private String orderId; // 系统订单号(唯一) + private String payType; // 支付方式:3支付宝H5,4支付宝App + private String goodsId; // 商户商品ID + private BigDecimal goodsPrice; // 商户商品价格 + private Integer goodsNum; // 商户商品数量 + private BigDecimal totalAmount; // 付款总价 + private String userIp; // 用户IP + private Integer payStatus; // 支付状态: 0待支付,1已支付,2,取消支付 + private Date createTime; // 创建时间 + private Date updateTime; // 状态更新时间 + private Long userId; // 订单发起用户ID + private String userName; // 订单发起用户名 + private String remark; // 备注 + private String callBackOrderId; // 回传单号 + private String callBackMsg; // 回传信息 + private String callBackStatus; // 回传状态 + private String sign; // sign + private String subject; // 商品名 +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRechargeParam.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRechargeParam.java new file mode 100644 index 0000000..d53aaad --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRechargeParam.java @@ -0,0 +1,34 @@ +package com.ruoyi.thirdparty.wechat.entity; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TtCoinRechargeParam { + + /** 金币 */ + private BigDecimal coin; + + private BigDecimal minCoin; + + private BigDecimal maxCoin; + + /** 订单状态:0未付款,1已付款,2已取消 */ + private String payStatus; + + /** 用户ID */ + private Long uid; + private Long rid; + + /** 用户名 */ + private String uname; + + /** 订单号 */ + private String orderNo; + + + /** 订单号 */ + private String beginTime; + private String endTime; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRechargeRecord.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRechargeRecord.java new file mode 100644 index 0000000..881bbf6 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRechargeRecord.java @@ -0,0 +1,139 @@ +package com.ruoyi.thirdparty.wechat.entity; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +/** + * 充值记录对象 tt_coin_recharge_record + * + * @author ruoyi + * @date 2023-07-06 + */ +public class TtCoinRechargeRecord extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 金币 */ + @Excel(name = "金币") + private BigDecimal coin; + + /** 订单状态:0未付款,1已付款,2已取消 */ + @Excel(name = "订单状态",dictType = "alipay_status") + private String payStatus; + + /** 用户ID */ + @Excel(name = "用户ID") + private Long uid; + + /** 用户名 */ + @Excel(name = "用户名") + private String uname; + + /** 订单号 */ + @Excel(name = "订单号") + private String orderNo; + + /** 支付宝订单号 */ + @Excel(name = "支付宝订单号") + private String callbackNo; + + /** 回调信息 */ + @Excel(name = "回调信息") + private String callbackMsg; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setCoin(BigDecimal coin) + { + this.coin = coin; + } + + public BigDecimal getCoin() + { + return coin; + } + public void setPayStatus(String payStatus) + { + this.payStatus = payStatus; + } + + public String getPayStatus() + { + return payStatus; + } + public void setUid(Long uid) + { + this.uid = uid; + } + + public Long getUid() + { + return uid; + } + public void setUname(String uname) + { + this.uname = uname; + } + + public String getUname() + { + return uname; + } + public void setOrderNo(String orderNo) + { + this.orderNo = orderNo; + } + + public String getOrderNo() + { + return orderNo; + } + public void setCallbackNo(String callbackNo) + { + this.callbackNo = callbackNo; + } + + public String getCallbackNo() + { + return callbackNo; + } + public void setCallbackMsg(String callbackMsg) + { + this.callbackMsg = callbackMsg; + } + + public String getCallbackMsg() + { + return callbackMsg; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("coin", getCoin()) + .append("payStatus", getPayStatus()) + .append("uid", getUid()) + .append("uname", getUname()) + .append("createTime", getCreateTime()) + .append("updateTime", getUpdateTime()) + .append("orderNo", getOrderNo()) + .append("callbackNo", getCallbackNo()) + .append("callbackMsg", getCallbackMsg()) + .toString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRecord.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRecord.java new file mode 100644 index 0000000..6ecde1c --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRecord.java @@ -0,0 +1,156 @@ +package com.ruoyi.thirdparty.wechat.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 金币变动记录对象 tt_coin_record + * + * @author ruoyi + * @date 2023-07-06 + */ +public class TtCoinRecord extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** */ + private Long id; + + /** 所属用户 */ + @Excel(name = "所属用户") + private Long uid; + + /** 用户名 */ + @Excel(name = "用户名") + private String uname; + + /** 变动类型 */ + @Excel(name = "变动类型",dictType = "change_type") + private String type; + + + /** 变动类型 */ + @Excel(name = "操作场景",dictType = "operation_type") + private String operType; + + /** 奖励类型 */ + @Excel(name = "奖励类型",dictType = "reward_type") + private String rewardType; + + /** 变动金额 */ + @Excel(name = "变动金额") + private BigDecimal money; + + /** 返佣时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date time; + + /** 状态 */ + @Excel(name = "状态") + private Long status; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setUid(Long uid) + { + this.uid = uid; + } + + public Long getUid() + { + return uid; + } + public void setUname(String uname) + { + this.uname = uname; + } + + public String getUname() + { + return uname; + } + public void setType(String type) + { + this.type = type; + } + + public String getType() + { + return type; + } + public void setRewardType(String rewardType) + { + this.rewardType = rewardType; + } + + public String getRewardType() + { + return rewardType; + } + public void setMoney(BigDecimal money) + { + this.money = money; + } + + public BigDecimal getMoney() + { + return money; + } + public void setTime(Date time) + { + this.time = time; + } + + public Date getTime() + { + return time; + } + public void setStatus(Long status) + { + this.status = status; + } + + public Long getStatus() + { + return status; + } + + public String getOperType() { + return operType; + } + + public void setOperType(String operType) { + this.operType = operType; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("uid", getUid()) + .append("uname", getUname()) + .append("type", getType()) + .append("oper_type", getOperType()) + .append("rewardType", getRewardType()) + .append("money", getMoney()) + .append("time", getTime()) + .append("status", getStatus()) + .append("createTime", getCreateTime()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRecordParam.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRecordParam.java new file mode 100644 index 0000000..b19a333 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtCoinRecordParam.java @@ -0,0 +1,103 @@ +package com.ruoyi.thirdparty.wechat.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +import java.util.Date; + +/** + * 充值卡密对象 tt_coin_key + * + * @author ruoyi + * @date 2023-07-06 + */ +public class TtCoinRecordParam extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 所属用户 */ + @Excel(name = "所属用户") + private Long uid; + + + /** 使用状态 */ + @Excel(name = "用户名") + private String uname; + + /** 使用状态 */ + private String changeType; + + /** 使用状态 */ + private String rewardType; + + /** 使用时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "使用时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date beginTime; + + + /** 使用时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "使用时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getUid() { + return uid; + } + + public void setUid(Long uid) { + this.uid = uid; + } + + public String getUname() { + return uname; + } + + public void setUname(String uname) { + this.uname = uname; + } + + public String getChangeType() { + return changeType; + } + + public void setChangeType(String changeType) { + this.changeType = changeType; + } + + public String getRewardType() { + return rewardType; + } + + public void setRewardType(String rewardType) { + this.rewardType = rewardType; + } + + public Date getBeginTime() { + return beginTime; + } + + public void setBeginTime(Date beginTime) { + this.beginTime = beginTime; + } + + public Date getEndTime() { + return endTime; + } + + public void setEndTime(Date endTime) { + this.endTime = endTime; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtUserLsjl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtUserLsjl.java new file mode 100644 index 0000000..8dc987d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtUserLsjl.java @@ -0,0 +1,167 @@ +package com.ruoyi.thirdparty.wechat.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 流水记录对象 tt_user_lsjl + * + * @author junhai + * @date 2023-08-19 + */ +@Data +public class TtUserLsjl +{ + private static final long serialVersionUID = 1L; + + /** 流水id */ + private Long id; + + /** 用户id */ + @Excel(name = "用户id") + private Long ttUserId; + + /** 上上级用户id **/ + private Long ttSsUserId; + + /** 业务类型:充值和消费 */ + @Excel(name = "业务类型:充值和消费") + private String type; + + /** v币变动前 */ + @Excel(name = "v币变动前") + private BigDecimal vCoinBefore; + + /** v币变动后 */ + @Excel(name = "v币变动后") + private BigDecimal vCoinAfter; + + /** v币变动金额 */ + @Excel(name = "v币变动金额") + private BigDecimal vCoinChange; + + /** 钻石变动前的金额 */ + @Excel(name = "钻石变动前的金额") + private BigDecimal dCoinBefore; + + /** 钻石变动金额 */ + @Excel(name = "钻石变动金额") + private BigDecimal dCoinChange; + + /** 钻石变动后的金额 */ + @Excel(name = "钻石变动后的金额") + private BigDecimal dCoinAfter; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setTtUserId(Long ttUserId) + { + this.ttUserId = ttUserId; + } + + public Long getTtUserId() + { + return ttUserId; + } + public void setType(String type) + { + this.type = type; + } + + public String getType() + { + return type; + } + public void setvCoinBefore(BigDecimal vCoinBefore) + { + this.vCoinBefore = vCoinBefore; + } + + public BigDecimal getvCoinBefore() + { + return vCoinBefore; + } + public void setvCoinAfter(BigDecimal vCoinAfter) + { + this.vCoinAfter = vCoinAfter; + } + + public BigDecimal getvCoinAfter() + { + return vCoinAfter; + } + public void setvCoinChange(BigDecimal vCoinChange) + { + this.vCoinChange = vCoinChange; + } + + public BigDecimal getvCoinChange() + { + return vCoinChange; + } + public void setdCoinBefore(BigDecimal dCoinBefore) + { + this.dCoinBefore = dCoinBefore; + } + + public BigDecimal getdCoinBefore() + { + return dCoinBefore; + } + public void setdCoinChange(BigDecimal dCoinChange) + { + this.dCoinChange = dCoinChange; + } + + public BigDecimal getdCoinChange() + { + return dCoinChange; + } + public void setdCoinAfter(BigDecimal dCoinAfter) + { + this.dCoinAfter = dCoinAfter; + } + + public BigDecimal getdCoinAfter() + { + return dCoinAfter; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("ttUserId", getTtUserId()) + .append("type", getType()) + .append("vCoinBefore", getvCoinBefore()) + .append("vCoinAfter", getvCoinAfter()) + .append("vCoinChange", getvCoinChange()) + .append("dCoinBefore", getdCoinBefore()) + .append("dCoinChange", getdCoinChange()) + .append("dCoinAfter", getdCoinAfter()) + .append("createTime", getCreateTime()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtUserLsjlParam.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtUserLsjlParam.java new file mode 100644 index 0000000..6e8e7ef --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/entity/TtUserLsjlParam.java @@ -0,0 +1,63 @@ +package com.ruoyi.thirdparty.wechat.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import lombok.Data; + +import java.util.Date; + +@Data +public class TtUserLsjlParam { + + /** 流水id */ + private Long id; + + /** 用户id */ + @Excel(name = "用户id") + private Long ttUserId; + + /** 上上级用户id **/ + private Long ttSsUserId; + + /** 类型 **/ + private String type; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + @Override + public String toString() { + return "TtUserLsjlParam{" + + "id=" + id + + ", type='" + type + '\'' + + ", createTime=" + createTime + + '}'; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TianXinMapper.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TianXinMapper.java new file mode 100644 index 0000000..6671fa8 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TianXinMapper.java @@ -0,0 +1,17 @@ +package com.ruoyi.thirdparty.wechat.mapper; + +import com.ruoyi.thirdparty.wechat.entity.TianXinOrder; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TianXinMapper { + void insertTianXinOrder(TianXinOrder tianXinOrder); + + TianXinOrder selectTianXinOrderByOrderId(String sdorderno); + + void updateTianXinOrder(TianXinOrder tianXinOrder); + + void deleteDataTianXinOrder(); + + void deleteDataTtCoinRechargeRecord(); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TtCoinRechargeRecordMapper.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TtCoinRechargeRecordMapper.java new file mode 100644 index 0000000..742e0da --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TtCoinRechargeRecordMapper.java @@ -0,0 +1,38 @@ +package com.ruoyi.thirdparty.wechat.mapper; + +import com.ruoyi.thirdparty.wechat.entity.TtCoinRechargeRecord; + +/** + * 充值记录Mapper接口 + * + * @author ruoyi + * @date 2023-07-06 + */ +public interface TtCoinRechargeRecordMapper +{ + + /** + * 新增充值记录 + * + * @param ttCoinRechargeRecord 充值记录 + * @return 结果 + */ + public int insertTtCoinRechargeRecord(TtCoinRechargeRecord ttCoinRechargeRecord); + + /** + * 修改充值记录 + * + * @param ttCoinRechargeRecord 充值记录 + * @return 结果 + */ + public int updateTtCoinRechargeRecord(TtCoinRechargeRecord ttCoinRechargeRecord); + + /** + * 根据订单号查询数据 + * + * @param orderNo + * @return + */ + TtCoinRechargeRecord selectTtCoinRechargeRecordByOrder(String orderNo); + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TtCoinRecordMapper.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TtCoinRecordMapper.java new file mode 100644 index 0000000..5599e5d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TtCoinRecordMapper.java @@ -0,0 +1,23 @@ +package com.ruoyi.thirdparty.wechat.mapper; + +import com.ruoyi.thirdparty.wechat.entity.TtCoinRecord; + +/** + * 金币变动记录Mapper接口 + * + * @author ruoyi + * @date 2023-07-06 + */ +public interface TtCoinRecordMapper +{ + + /** + * 新增金币变动记录 + * + * @param ttCoinRecord 金币变动记录 + * @return 结果 + */ + public int insertTtCoinRecord(TtCoinRecord ttCoinRecord); + + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TtUserLsjlMapper.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TtUserLsjlMapper.java new file mode 100644 index 0000000..5723539 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/mapper/TtUserLsjlMapper.java @@ -0,0 +1,22 @@ +package com.ruoyi.thirdparty.wechat.mapper; + +import com.ruoyi.thirdparty.wechat.entity.TtUserLsjl; + +/** + * 流水记录Mapper接口 + * + * @author junhai + * @date 2023-08-19 + */ +public interface TtUserLsjlMapper +{ + + /** + * 新增流水记录 + * + * @param ttUserLsjl 流水记录 + * @return 结果 + */ + public int insertTtUserLsjl(TtUserLsjl ttUserLsjl); + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ApiTokenService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ApiTokenService.java new file mode 100644 index 0000000..d431322 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ApiTokenService.java @@ -0,0 +1,259 @@ +package com.ruoyi.thirdparty.wechat.service; + +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.AddressUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.thirdparty.wechat.domain.ApiLoginUser; +import com.ruoyi.thirdparty.wechat.domain.PtLoginUser; +import eu.bitwalker.useragentutils.UserAgent; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * token验证处理 + * + * @author ruoyi + */ +@Component +public class ApiTokenService +{ + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + // 令牌秘钥 + @Value("${token.secret}") + private String secret; + + // 令牌有效期(30天,玩家端专用) + @Value("${token.expireTime:43200}") + private int expireTime; + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + // 剩余不剗1天自动续期(玩家每天登录都自动续30天) + private static final Long MILLIS_MINUTE_TEN = 24 * 60 * 60 * 1000L; + + @Autowired + private RedisCache redisCache; + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public PtLoginUser getLoginUser(HttpServletRequest request) + { + // 获取请求携带的令牌 + String token = getToken(request); + if (StringUtils.isNotEmpty(token)) + { + try + { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + PtLoginUser user = redisCache.getCacheObject(userKey); + return user; + } + catch (Exception e) + { + } + } + return null; + } + + + + /** + * @author 作者:Jason E-mail: Jason_Lee_lizhen@outlook.com + * @copyright All Rights Reserved @version v1.0 + * @version 创建时间:2023年6月13日16:41:51 + * @description方法说明 : + * @arg 入参类型:*****;出参类型:**** + */ + public ApiLoginUser getLoginUser(String token) + { + // 获取请求携带的令牌 + if (StringUtils.isNotEmpty(token)) + { + try + { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + ApiLoginUser user = redisCache.getCacheObject(userKey); + return user; + } + catch (Exception e) + { + } + } + return null; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(PtLoginUser loginUser) + { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) + { + refreshToken(loginUser); + } + } + + /** + * 删除用户身份信息 + */ + public void delLoginUser(String token) + { + if (StringUtils.isNotEmpty(token)) + { + String userKey = getTokenKey(token); + redisCache.deleteObject(userKey); + } + } + + /** + * 创建令牌 + * + * @param loginUser 用户信息 + * @return 令牌 + */ + public String createToken(PtLoginUser loginUser) + { + String token = IdUtils.fastUUID(); + loginUser.setToken(token); + setUserAgent(loginUser); + refreshToken(loginUser); + + Map claims = new HashMap<>(); + claims.put(Constants.LOGIN_USER_KEY, token); + return createToken(claims); + } + + /** + * 验证令牌有效期,相差不足20分钟,自动刷新缓存 + * + * @param loginUser + * @return 令牌 + */ + public void verifyToken(PtLoginUser loginUser) + { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= MILLIS_MINUTE_TEN) + { + refreshToken(loginUser); + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(PtLoginUser loginUser) + { + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + } + + /** + * 设置用户代理信息 + * + * @param loginUser 登录信息 + */ + public void setUserAgent(PtLoginUser loginUser) + { + UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + String ip = IpUtils.getIpAddr(); + loginUser.setIpaddr(ip); + loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); + loginUser.setBrowser(userAgent.getBrowser().getName()); + loginUser.setOs(userAgent.getOperatingSystem().getName()); + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + private String createToken(Map claims) + { + String token = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS512, secret).compact(); + return token; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private Claims parseToken(String token) + { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) + { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + /** + * 获取请求token + * + * @param request + * @return token + */ + private String getToken(HttpServletRequest request) + { + String token = request.getHeader(header); + if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) + { + token = token.replace(Constants.TOKEN_PREFIX, ""); + } + return token; + } + + private String getTokenKey(String uuid) + { + return CacheConstants.LOGIN_TOKEN_KEY + uuid; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ITtCoinRechargeRecordService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ITtCoinRechargeRecordService.java new file mode 100644 index 0000000..e0bdadd --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ITtCoinRechargeRecordService.java @@ -0,0 +1,38 @@ +package com.ruoyi.thirdparty.wechat.service; + +import com.ruoyi.thirdparty.wechat.entity.TtCoinRechargeRecord; + +/** + * 充值记录Service接口 + * + * @author ruoyi + * @date 2023-07-06 + */ +public interface ITtCoinRechargeRecordService { + + /** + * 新增充值记录 + * + * @param ttCoinRechargeRecord 充值记录 + * @return 结果 + */ + public int insertTtCoinRechargeRecord(TtCoinRechargeRecord ttCoinRechargeRecord); + + /** + * 修改充值记录 + * + * @param ttCoinRechargeRecord 充值记录 + * @return 结果 + */ + public int updateTtCoinRechargeRecord(TtCoinRechargeRecord ttCoinRechargeRecord); + + + /** + * 根据订单号查询 + * + * @param tradeNo + * @return + */ + TtCoinRechargeRecord selectTtCoinRechargeRecordByOrder(String tradeNo); + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ITtCoinRecordService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ITtCoinRecordService.java new file mode 100644 index 0000000..3c895f0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ITtCoinRecordService.java @@ -0,0 +1,21 @@ +package com.ruoyi.thirdparty.wechat.service; + +import com.ruoyi.thirdparty.wechat.entity.TtCoinRecord; + +/** + * 金币变动记录Service接口 + * + * @author ruoyi + * @date 2023-07-06 + */ +public interface ITtCoinRecordService +{ + /** + * 新增金币变动记录 + * + * @param ttCoinRecord 金币变动记录 + * @return 结果 + */ + public int insertTtCoinRecord(TtCoinRecord ttCoinRecord); + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ITtUserLsjlService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ITtUserLsjlService.java new file mode 100644 index 0000000..306b4f6 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/ITtUserLsjlService.java @@ -0,0 +1,21 @@ +package com.ruoyi.thirdparty.wechat.service; + +import com.ruoyi.thirdparty.wechat.entity.TtUserLsjl; + +/** + * 流水记录Service接口 + * + * @author junhai + * @date 2023-08-19 + */ +public interface ITtUserLsjlService +{ + + /** + * 新增流水记录 + * + * @param ttUserLsjl 流水记录 + * @return 结果 + */ + public int insertTtUserLsjl(TtUserLsjl ttUserLsjl); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/TianXinService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/TianXinService.java new file mode 100644 index 0000000..f998fba --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/TianXinService.java @@ -0,0 +1,15 @@ +package com.ruoyi.thirdparty.wechat.service; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.wechat.domain.PayOrderParam; + +import jakarta.servlet.http.HttpServletRequest; + +public interface TianXinService { + AjaxResult createOrder(PayOrderParam param, TtUser user, String ip); + + String callBack(HttpServletRequest request); + + AjaxResult queryOrderStatus(String sdorderno); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TianXinServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TianXinServiceImpl.java new file mode 100644 index 0000000..13f5d55 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TianXinServiceImpl.java @@ -0,0 +1,265 @@ +package com.ruoyi.thirdparty.wechat.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtRechargeProdService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.wechat.config.TianXinProperties; +import com.ruoyi.thirdparty.wechat.domain.PayOrderParam; +import com.ruoyi.thirdparty.wechat.entity.TianXinOrder; +import com.ruoyi.thirdparty.wechat.entity.TtCoinRechargeRecord; +import com.ruoyi.thirdparty.wechat.entity.TtCoinRecord; +import com.ruoyi.thirdparty.wechat.entity.TtUserLsjl; +import com.ruoyi.thirdparty.wechat.mapper.TianXinMapper; +import com.ruoyi.thirdparty.wechat.service.ITtCoinRechargeRecordService; +import com.ruoyi.thirdparty.wechat.service.ITtCoinRecordService; +import com.ruoyi.thirdparty.wechat.service.ITtUserLsjlService; +import com.ruoyi.thirdparty.wechat.service.TianXinService; +import com.ruoyi.thirdparty.wechat.utils.DemoUtils; +import com.ruoyi.thirdparty.wechat.utils.GenerateUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import jakarta.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +@Service +@EnableConfigurationProperties(value = TianXinProperties.class) +@Slf4j +public class TianXinServiceImpl implements TianXinService { + + private final TianXinProperties tianXinProperties; + private final TtRechargeProdService coinItemService; + private final TtUserMapper userMapper; + private final TianXinMapper tianXinMapper; + private final ITtCoinRechargeRecordService ttCoinRechargeRecordService; + private final ITtCoinRecordService recordService; + private final ITtUserLsjlService userLsjlService; + private final TtOrderMapper orderMapper; + + public TianXinServiceImpl(TianXinProperties tianXinProperties, + TtRechargeProdService coinItemService, + TtUserMapper userMapper, + TianXinMapper tianXinMapper, + ITtCoinRechargeRecordService ttCoinRechargeRecordService, + ITtCoinRecordService recordService, + ITtUserLsjlService userLsjlService, + TtOrderMapper orderMapper) { + this.tianXinProperties = tianXinProperties; + this.coinItemService = coinItemService; + this.userMapper = userMapper; + this.tianXinMapper = tianXinMapper; + this.ttCoinRechargeRecordService = ttCoinRechargeRecordService; + this.recordService = recordService; + this.userLsjlService = userLsjlService; + this.orderMapper = orderMapper; + } + + @Override + public AjaxResult createOrder(PayOrderParam param, TtUser ttUser, String ip) { + + long itemId = param.getCoinItemId(); + Integer itemNum = param.getCoinItemNum(); + BigDecimal payAmount = param.getCoinItemAmount(); + TtRechargeProd item = coinItemService.getById(itemId); + + BigDecimal realPayAmout = item.getPrice().multiply(new BigDecimal(itemNum)); + if (realPayAmout.compareTo(payAmount) != 0) { + return AjaxResult.error("支付金额不正确!"); + } + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("version", tianXinProperties.getVersion()); // 版本号 + map.add("customerid", tianXinProperties.getCustomerid()); // 商户编号 + map.add("sdorderno", GenerateUtils.getGuid("TX")); //商户请求的唯一标识,推荐为uuid,必填 IdUtil hutool工具 + map.add("total_fee", realPayAmout); // 订单金额 + map.add("paytype", tianXinProperties.getPaytype()); // 支付编号 + map.add("bankcode", ""); // 银行编号 + map.add("notifyurl", tianXinProperties.getNotifyurl()); // 异步通知URL + map.add("clientip", ip); // 用户ip + map.add("returnurl", tianXinProperties.getReturnurl()); // 同步通知URL + map.add("remark", "会员充值"); // 订单备注 + map.add("access_type", tianXinProperties.getAccess_type()); // 接入方式 + String signStr = DemoUtils.sortMapByValues(map.toSingleValueMap()); + String sign = DemoUtils.encryptToMD5(signStr + "&" + tianXinProperties.getUserkey()); + map.add("sign", sign); + map.add("is_jump", false); + + Map resMap = DemoUtils.postFormData(map, tianXinProperties.getApiurl() + "/apisubmit"); + + if (tianXinProperties.getAccess_type().equals("API")) { + if (resMap.get("status").equals("OK")) { + // tianxin_order表添加数据 +// TianXinOrder tianXinOrder = TianXinOrder.builder().build(); +// tianXinOrder.setOrderId(resMap.get("sdorderno")); // 系统订单号(唯一) +// tianXinOrder.setPayType("5"); // 支付方式:5微信扫码 +// tianXinOrder.setGoodsId(String.valueOf(itemId)); // 商户商品ID +// tianXinOrder.setGoodsPrice(payAmount); // 商户商品价格 +// tianXinOrder.setGoodsNum(itemNum); // 商户商品数量 +// tianXinOrder.setTotalAmount(realPayAmout); // 付款总价 +// tianXinOrder.setUserIp(ip); // 用户IP +// tianXinOrder.setPayStatus(0); // 支付状态: 0待支付,1已支付,2,取消支付 +// tianXinOrder.setCreateTime(DateUtils.getNowDate()); // 创建时间 +// tianXinOrder.setUserId(Long.valueOf(ttUser.getUserId())); // 订单发起用户ID +// tianXinOrder.setUserName(ttUser.getUserName()); // 订单发起用户名 +// tianXinOrder.setRemark("待付款!"); // 备注 +// tianXinOrder.setCallBackOrderId(resMap.get("orderid")); // 回传单号 +// tianXinMapper.insertTianXinOrder(tianXinOrder); + + TtOrder ttOrder = TtOrder.builder().build(); + ttOrder.setUserId(ttUser.getUserId()); + ttOrder.setThirdParty("0"); + ttOrder.setType("2"); + ttOrder.setGoodsId((int)itemId); + ttOrder.setGoodsPrice(payAmount); + ttOrder.setGoodsNum(itemNum); + ttOrder.setTotalAmount(realPayAmout); + ttOrder.setOrderId(resMap.get("sdorderno")); + ttOrder.setSign(sign); + ttOrder.setStatus("0"); + ttOrder.setOutTradeNo(resMap.get("orderid")); + ttOrder.setPayUrl(resMap.get("url")); + ttOrder.setCreateTime(DateUtils.getNowDate()); + int isSuccess = orderMapper.insert(ttOrder); + + //充值记录的新增 +// TtCoinRechargeRecord coinRechargeRecord = new TtCoinRechargeRecord(); +// coinRechargeRecord.setCoin(param.getCoinItemAmount()); +// coinRechargeRecord.setPayStatus("0"); // 订单状态:0未付款,1已付款,2已取消' +// coinRechargeRecord.setUid(Long.valueOf(ttUser.getUserId())); +// coinRechargeRecord.setUname(ttUser.getUserName()); +// coinRechargeRecord.setCreateTime(DateUtils.getNowDate()); +// coinRechargeRecord.setOrderNo(tianXinOrder.getOrderId()); +// ttCoinRechargeRecordService.insertTtCoinRechargeRecord(coinRechargeRecord); + + return AjaxResult.success("订单创建成功!", resMap); + } else { + return AjaxResult.error("订单创建失败!"); + } + } + return AjaxResult.error("接入方式选择错误,请联系管理员!"); + } + + + @Override + public String callBack(HttpServletRequest request) { + String sign_str = "customerid=" + request.getParameter("customerid") + + "&status=" + request.getParameter("status") + + "&sdpayno=" + request.getParameter("sdpayno") + + "&sdorderno=" + request.getParameter("sdorderno") + + "&total_fee=" + request.getParameter("total_fee") + + "&realmoney=" + request.getParameter("realmoney") + + "&paytype=" + request.getParameter("paytype") + + "&" + tianXinProperties.getUserkey(); + String sign = DemoUtils.encryptToMD5(sign_str); + + // 订单ID + String sdorderno = request.getParameter("sdorderno"); +// String sdorderno = "20231111160000000002TX"; + + // 获取tianxin订单数据 + TianXinOrder tianXinOrder = tianXinMapper.selectTianXinOrderByOrderId(sdorderno); + // 获取充值记录 + TtCoinRechargeRecord ttCoinRechargeRecord = ttCoinRechargeRecordService.selectTtCoinRechargeRecordByOrder(sdorderno); + + if ("1".equals(request.getParameter("status")) && sign.equals(request.getParameter("sign"))) { + Enumeration parameterNames = request.getParameterNames(); + Map parameterMap = new HashMap<>(); + while (parameterNames.hasMoreElements()) { + String paraName = parameterNames.nextElement(); + parameterMap.put(paraName, request.getParameter(paraName)); + } + + try { + tianXinOrder.setPayStatus(1); + tianXinOrder.setCallBackMsg("支付成功!"); // 回传信息 + tianXinOrder.setCallBackStatus(request.getParameter("status")); // 回传状态 1:成功,其他失败 + tianXinOrder.setSign(request.getParameter("sign")); // sign + tianXinOrder.setSubject("FireSkins平台" + request.getParameter("realmoney") + "充值!"); // 商品名 + tianXinOrder.setUpdateTime(DateUtils.getNowDate()); + tianXinOrder.setRemark("付款成功!"); // 备注 + tianXinMapper.updateTianXinOrder(tianXinOrder); + + ttCoinRechargeRecord.setPayStatus("1"); + ttCoinRechargeRecord.setCallbackNo(request.getParameter("sdpayno")); + ttCoinRechargeRecord.setUpdateTime(DateUtils.getNowDate()); + ttCoinRechargeRecord.setCallbackMsg(JSONObject.toJSONString(parameterMap)); + ttCoinRechargeRecordService.updateTtCoinRechargeRecord(ttCoinRechargeRecord); + + //记录金币数 + TtCoinRecord record = new TtCoinRecord(); + record.setMoney(tianXinOrder.getTotalAmount()); + record.setOperType("3"); + record.setType("0"); + record.setUid(tianXinOrder.getUserId()); + record.setUname(tianXinOrder.getUserName()); + record.setCreateTime(new Date()); + record.setStatus(1l); + recordService.insertTtCoinRecord(record); + + //记录流水的变更 + TtUser ttUser = userMapper.selectTtUserById(tianXinOrder.getUserId()); + TtUserLsjl userLsjl = new TtUserLsjl(); +// userLsjl.setTtSsUserId(ttUser.getSuperiorId()); + userLsjl.setType("充值"); +// userLsjl.setvCoinBefore(ttUser.getBean()); + userLsjl.setvCoinChange(record.getMoney()); +// userLsjl.setvCoinAfter(ttUser.getBean().add(record.getMoney())); + //设置变动积分情况 +// userLsjl.setdCoinBefore(ttUser.getStoreBean()); +// userLsjl.setdCoinAfter(ttUser.getStoreBean()); + userLsjl.setdCoinChange(BigDecimal.valueOf(0)); + userLsjl.setCreateTime(DateUtils.getNowDate()); + userLsjlService.insertTtUserLsjl(userLsjl); + + userMapper.updateTtUserCoin(tianXinOrder.getUserId(), record.getMoney()); + + // 删除10分钟之前的无用数据 + tianXinMapper.deleteDataTianXinOrder(); + tianXinMapper.deleteDataTtCoinRechargeRecord(); + return "success"; + } catch (Exception e) { + return "error"; + } + + } else { + try { + tianXinOrder.setPayStatus(2); + tianXinOrder.setUpdateTime(new Date()); + tianXinMapper.updateTianXinOrder(tianXinOrder); + ttCoinRechargeRecord.setPayStatus("2"); + ttCoinRechargeRecord.setUpdateTime(DateUtils.getNowDate()); + ttCoinRechargeRecordService.updateTtCoinRechargeRecord(ttCoinRechargeRecord); + } catch (Exception e) { + return "error"; + } + return "error"; + } + } + + @Override + public AjaxResult queryOrderStatus(String sdorderno) { + MultiValueMap map = new LinkedMultiValueMap<>(); + String reqtime = String.valueOf(System.currentTimeMillis() / 1000); + String signStr = "customerid=" + tianXinProperties.getCustomerid() + "&sdorderno=" + sdorderno + "&reqtime=" + reqtime; + String sign = DemoUtils.encryptToMD5(signStr + "&" + tianXinProperties.getUserkey()); + map.add("customerid", tianXinProperties.getCustomerid()); + map.add("sdorderno", sdorderno); + map.add("reqtime", reqtime); + map.add("sign", sign); + Map resMap = DemoUtils.postFormData(map, tianXinProperties.getApiurl() + "/apiorderquery"); + return AjaxResult.success(resMap); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TtCoinRechargeRecordServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TtCoinRechargeRecordServiceImpl.java new file mode 100644 index 0000000..291ee19 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TtCoinRechargeRecordServiceImpl.java @@ -0,0 +1,57 @@ +package com.ruoyi.thirdparty.wechat.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.thirdparty.wechat.entity.TtCoinRechargeRecord; +import com.ruoyi.thirdparty.wechat.mapper.TtCoinRechargeRecordMapper; +import com.ruoyi.thirdparty.wechat.service.ITtCoinRechargeRecordService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 充值记录Service业务层处理 + * + * @author ruoyi + * @date 2023-07-06 + */ +@Service +public class TtCoinRechargeRecordServiceImpl implements ITtCoinRechargeRecordService +{ + @Autowired + private TtCoinRechargeRecordMapper ttCoinRechargeRecordMapper; + + + + /** + * 新增充值记录 + * + * @param ttCoinRechargeRecord 充值记录 + * @return 结果 + */ + @Override + public int insertTtCoinRechargeRecord(TtCoinRechargeRecord ttCoinRechargeRecord) + { + ttCoinRechargeRecord.setCreateTime(DateUtils.getNowDate()); + return ttCoinRechargeRecordMapper.insertTtCoinRechargeRecord(ttCoinRechargeRecord); + } + + /** + * 修改充值记录 + * + * @param ttCoinRechargeRecord 充值记录 + * @return 结果 + */ + @Override + public int updateTtCoinRechargeRecord(TtCoinRechargeRecord ttCoinRechargeRecord) + { + ttCoinRechargeRecord.setUpdateTime(DateUtils.getNowDate()); + return ttCoinRechargeRecordMapper.updateTtCoinRechargeRecord(ttCoinRechargeRecord); + } + + + @Override + public TtCoinRechargeRecord selectTtCoinRechargeRecordByOrder(String tradeNo) { + return ttCoinRechargeRecordMapper.selectTtCoinRechargeRecordByOrder(tradeNo); + } + + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TtCoinRecordServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TtCoinRecordServiceImpl.java new file mode 100644 index 0000000..a9dde42 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TtCoinRecordServiceImpl.java @@ -0,0 +1,36 @@ +package com.ruoyi.thirdparty.wechat.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.thirdparty.wechat.entity.TtCoinRecord; +import com.ruoyi.thirdparty.wechat.mapper.TtCoinRecordMapper; +import com.ruoyi.thirdparty.wechat.service.ITtCoinRecordService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 金币变动记录Service业务层处理 + * + * @author ruoyi + * @date 2023-07-06 + */ +@Service +public class TtCoinRecordServiceImpl implements ITtCoinRecordService +{ + @Autowired + private TtCoinRecordMapper ttCoinRecordMapper; + + /** + * 新增金币变动记录 + * + * @param ttCoinRecord 金币变动记录 + * @return 结果 + */ + @Override + public int insertTtCoinRecord(TtCoinRecord ttCoinRecord) + { + ttCoinRecord.setCreateTime(DateUtils.getNowDate()); + return ttCoinRecordMapper.insertTtCoinRecord(ttCoinRecord); + } + + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TtUserLsjlServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TtUserLsjlServiceImpl.java new file mode 100644 index 0000000..5681033 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/service/impl/TtUserLsjlServiceImpl.java @@ -0,0 +1,35 @@ +package com.ruoyi.thirdparty.wechat.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.thirdparty.wechat.entity.TtUserLsjl; +import com.ruoyi.thirdparty.wechat.mapper.TtUserLsjlMapper; +import com.ruoyi.thirdparty.wechat.service.ITtUserLsjlService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 流水记录Service业务层处理 + * + * @author junhai + * @date 2023-08-19 + */ +@Service +public class TtUserLsjlServiceImpl implements ITtUserLsjlService +{ + @Autowired + private TtUserLsjlMapper ttUserLsjlMapper; + + /** + * 新增流水记录 + * + * @param ttUserLsjl 流水记录 + * @return 结果 + */ + @Override + public int insertTtUserLsjl(TtUserLsjl ttUserLsjl) + { + ttUserLsjl.setCreateTime(DateUtils.getNowDate()); + return ttUserLsjlMapper.insertTtUserLsjl(ttUserLsjl); + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/DateUtils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/DateUtils.java new file mode 100644 index 0000000..d3b67b4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/DateUtils.java @@ -0,0 +1,305 @@ +package com.ruoyi.thirdparty.wechat.utils; + +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +public class DateUtils { + + /** + * 获取当前时间 + * "yyyy-MM-dd" + * @return + */ + public static String queryCurrentDate(){ + Date d = new Date(); + System.out.println(d); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.format(d); + } + + /** + * 获取当前时间 + * "yyyy-MM-dd" + * @return + */ + public static String queryCurrentDay(){ + Date d = new Date(); + System.out.println(d); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.format(d); + } + + public static String queryCurrentHour(){ + Date d = new Date(); + System.out.println(d); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.format(d); + } + + public static String queryCurrentHourToEnd(){ + Date d = new Date(); + Date dend = new Date(d.getTime()+300000); + System.out.println(d); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String eString=sdf.format(dend); + + return eString; + } + + public static String longToString(long time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String date = sdf.format(new Date(time*1000)); + return date; + } + + public static String longToStringMonth(long time){ + SimpleDateFormat sdf = new SimpleDateFormat("MM-dd"); + String date = sdf.format(new Date(time)); + return date; + } + + public static String longToStringQuan(long time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String date = sdf.format(new Date(time*1000)); + return date; + } + + public static String longToStringAll(long time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String date = sdf.format(new Date(time)); + return date; + } + + public static String dateStrclean(String dateStr){ + int index = dateStr.indexOf("."); + dateStr = dateStr.substring(0, index); + return dateStr; + } + + public static String dateToStringShort(Date time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String date = sdf.format(time); + return date; + } + + public static String dateToStringLong(Date time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String date = sdf.format(time); + return date; + } + + public static String dateToStringShortFormate(Date time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); + String date = sdf.format(time); + return date; + } + + public static String dateToDes(Date time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHH"); + String date = sdf.format(time); + return date; + } + + public static int dateTimestamp(){ + return Integer.parseInt((new Date().getTime() + "").substring( + 0, 10)); + } + + public static Long stringToLong(String time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = null; + try { + // Fri Feb 24 00:00:00 CST 2012 + date = sdf.parse(time); + } catch (Exception e) { + e.printStackTrace(); + } + // 2012-02-24 + + return date.getTime(); + } + + public static Date stringToDateLong(String time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = null; + try { + // Fri Feb 24 00:00:00 CST 2012 + date = sdf.parse(time); + } catch (Exception e) { + e.printStackTrace(); + } + // 2012-02-24 + + return date; + } + + public static Timestamp getTimestamp(){ + + try { + // Fri Feb 24 00:00:00 CST 2012 + Timestamp date = Timestamp.valueOf(dateToStringLong(new Date())); + return date; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + // 2012-02-24 + + + } + + public static long geTimestamp(){ + + try { + // Fri Feb 24 00:00:00 CST 2012 + long date = System.currentTimeMillis(); + return date; + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + // 2012-02-24 + + + } + + public static Date stringToDateShort(String time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date date = null; + try { + // Fri Feb 24 00:00:00 CST 2012 + date = sdf.parse(time); + } catch (Exception e) { + e.printStackTrace(); + } + // 2012-02-24 + + return date; + } + + public static Date StringToDate(String time){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date date = null; + try { + // Fri Feb 24 00:00:00 CST 2012 + date = sdf.parse(time); + } catch (Exception e) { + e.printStackTrace(); + } + // 2012-02-24 + + return date; + } + + public static String getWeekOfDate(Date dt) { + String[] weekDays = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"}; + Calendar cal = Calendar.getInstance(); + cal.setTime(dt); + + int w = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (w < 0) + w = 0; + + return weekDays[w]; + } + + public static int getWeekIndexOfDate(Date dt) { + Calendar cal = Calendar.getInstance(); + cal.setTime(dt); + + int w = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (w < 0) + w = 0; + + return w; + } + + public static int getMonthOfSystem(){ + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis( System.currentTimeMillis()); + int month = cal.get(Calendar.MONTH) + 1; + return month; + } + + public static int getYearOfSystem(){ + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis( System.currentTimeMillis()); + int year = cal.get(Calendar.YEAR); + return year; + } + + public static int getHourOfSystem(){ + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis( System.currentTimeMillis()); + int hour = cal.get(Calendar.HOUR_OF_DAY); + return hour; + } + + public static int getDayOfDate(Date date){ + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(date.getTime()); + int day = cal.get(Calendar.DAY_OF_MONTH); + return day; + } + + public static String getDayJiaByDay(int day){ + SimpleDateFormat dft = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date beginDate = new Date(); + Calendar date = Calendar.getInstance(); + date.setTime(beginDate); + date.set(Calendar.DATE, date.get(Calendar.DATE) + day); + String endDate = dft.format(date.getTime()); + return endDate; + } + + public static String getDayJiaByDay(Date date1 ,int day){ + SimpleDateFormat dft = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date beginDate = date1; + Calendar date = Calendar.getInstance(); + date.setTime(beginDate); + date.set(Calendar.DATE, date.get(Calendar.DATE) + day); + String endDate = dft.format(date.getTime()); + return endDate; + } + + + public static String getDayShortJiaByDay(Date date1 ,int day){ + SimpleDateFormat dft = new SimpleDateFormat("yyyy-MM-dd"); + Date beginDate = date1; + Calendar date = Calendar.getInstance(); + date.setTime(beginDate); + date.set(Calendar.DATE, date.get(Calendar.DATE) + day); + String endDate = dft.format(date.getTime()); + return endDate; + } + + public static boolean belongCalendar(Date time, Date from, Date to) { + Calendar date = Calendar.getInstance(); + date.setTime(time); + + Calendar after = Calendar.getInstance(); + after.setTime(from); + + Calendar before = Calendar.getInstance(); + before.setTime(to); + + if (date.after(after) && date.before(before)) { + return true; + } else { + return false; + } + } + +// public static void main(String[] args) throws Exception { +// +// Date now = DateUtils.stringToDateShort("2017-10-18"); +// Date from = DateUtils.stringToDateShort("2017-10-18"); +// Date to = DateUtils.stringToDateShort("2017-10-18"); +// +// System.out.println(DateUtils.belongCalendar(now, from, to)); +// +// } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/DemoUtils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/DemoUtils.java new file mode 100644 index 0000000..f940aef --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/DemoUtils.java @@ -0,0 +1,104 @@ +package com.ruoyi.thirdparty.wechat.utils; + +import com.alibaba.fastjson2.JSON; +import org.apache.commons.codec.digest.DigestUtils; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.*; + +public class DemoUtils { + public static String getRandom620(Integer length) { + String result = ""; + Random rand = new Random(); + int n = 20; + if (null != length && length > 0) { + n = length; + } + int randInt = 0; + for (int i = 0; i < n; i++) { + randInt = rand.nextInt(10); + + result += randInt; + } + return result; + } + + public static String sortMapByValues(Map map){ + String[] sortedKeys = map.keySet().toArray(new String[]{}); + Arrays.sort(sortedKeys);// 排序请求参数 + StringBuilder s2 = new StringBuilder(); + for (String key : sortedKeys) { + //if str(v).strip() is not None and k != 'paytype' and k != 'remark' and k != 'bankcode' and k != 'sign' and k != 'userkey' and k != 'apiurl': + if(!Objects.equals(key, "paytype") && !Objects.equals(key, "remark") && !Objects.equals(key, "bankcode") && !Objects.equals(key, "sign")){ + s2.append(key).append("=").append(map.get(key)).append("&"); + } + } + s2.deleteCharAt(s2.length() - 1); + return String.valueOf(s2); + } + + /** + * RestTemplate发送POST请求之formData形式 + * @return + */ + public static Map postFormData(MultiValueMap map, String url){ + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + //头部类型 + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + //构造实体对象 + HttpEntity> param = new HttpEntity<>(map, headers); + //发起请求,服务地址,请求参数,返回消息体的数据类型 + ResponseEntity response = restTemplate.postForEntity(url, param, String.class); + //body + String body = response.getBody(); + if(body ==null){ + String location = Objects.requireNonNull(response.getHeaders().getLocation()).toASCIIString(); + Map m = new HashMap<>(); + m.put("url",location); + return m; + }else{ + Map result = JSON.parseObject(body, Map.class); + System.out.println("2"+result); + return result; + } + } + + /** + * MD5加密之方法一 + * @explain 借助apache工具类DigestUtils实现 + * @param str + * 待加密字符串 + * @return 16进制加密字符串 + */ + public static String encryptToMD5(String str) { + return DigestUtils.md5Hex(str); + } + + + + // 把json格式的字符串写到文件 + public static boolean writeFile(String filePath, String sets) { + FileWriter fw; + try { + fw = new FileWriter(filePath); + PrintWriter out = new PrintWriter(fw); + out.write(sets); + out.println(); + fw.close(); + out.close(); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/GenerateUtils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/GenerateUtils.java new file mode 100644 index 0000000..73cfc47 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/GenerateUtils.java @@ -0,0 +1,228 @@ +package com.ruoyi.thirdparty.wechat.utils; + +import java.text.SimpleDateFormat; +import java.util.*; + +public class GenerateUtils { + public static String getOrderIdByUUId() { + + SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmm"); + String first = df.format(new Date()); + System.out.println(first); + int hashCodeV = UUID.randomUUID().toString().hashCode(); + if (hashCodeV < 0) {//有可能是负数 + hashCodeV = -hashCodeV; + } + // 0 代表前面补充0 + // 4 代表长度为4 + // d 代表参数为正数型 + return first + String.format("%015d", hashCodeV); + } + + /** + * @author 作者:Jason E-mail: Jason_Lee_lizhen@outlook.com + * @copyright All Rights Reserved @version v1.0 + * @version 创建时间:2019年7月1日 上午9:56:16 + * @description方法说明 :获取UUID + * @arg 入参类型:*****;出参类型:String + */ + public static String generateUUid() { + String uuid = UUID.randomUUID().toString().replaceAll("-", ""); + + + return uuid; + } + + /** + * @author 作者:Jason E-mail: Jason_Lee_lizhen@outlook.com + * @copyright All Rights Reserved @version v1.0 + * @version 创建时间:2019年7月1日 上午9:58:40 + * @description方法说明 :生成商户号 + * @arg 入参类型:*****;出参类型:**** + */ + public static String generateStoreId() { + +// 注册年月日+CHAMBROAD+四位随机数 + //加上三位随机数 + String dateString = DateUtils.queryCurrentDay().replaceAll("-", ""); + Random random = new Random(); + int end4 = random.nextInt(9999); + String storeId = "store-" + dateString + "-LEE-" + end4; + + + return storeId; + } + + /** + * @author 作者:Jason E-mail: Jason_Lee_lizhen@outlook.com + * @copyright All Rights Reserved @version v1.0 + * @version 创建时间:2019年8月9日 上午10:34:08 + * @description方法说明 : 生成用户名 + * @arg 入参类型:*****;出参类型:**** + */ + public static String getStringRandom(int length) { + + String val = ""; + Random random = new Random(); + + //参数length,表示生成几位随机数 + for (int i = 0; i < length; i++) { + + String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; + //输出字母还是数字 + if ("char".equalsIgnoreCase(charOrNum)) { + //输出是大写字母还是小写字母 + int temp = random.nextInt(2) % 2 == 0 ? 65 : 97; + val += (char) (random.nextInt(26) + temp); + } else if ("num".equalsIgnoreCase(charOrNum)) { + val += String.valueOf(random.nextInt(10)); + } + } + return val; + } + + public static int Guid = 1; + + public static String guidDate = "2021070900"; + + public static int guidNew() { + SimpleDateFormat simpleDateFormatDay = new SimpleDateFormat("yyyyMMddHH"); + String outTradeDate = simpleDateFormatDay.format(new Date()); + if (!outTradeDate.equals(GenerateUtils.guidDate)) { + GenerateUtils.guidDate = outTradeDate; + GenerateUtils.Guid = 1; + return 1; + } + return GenerateUtils.Guid; + + + } + + /** + * @author 作者:Jason E-mail: Jason_Lee_lizhen@outlook.com + * @copyright All Rights Reserved @version v1.0 + * @version 创建时间:2021年7月12日 下午2:48:15 + * @description方法说明 : + * @arg 入参类型:*****workId String 分布式系统中传入机器码;出参类型:****String 生成的以yyyyMMddHH开头的有序单号 + */ + + public static String getGuid(String workId) { + + GenerateUtils.Guid += 1; + + int ran = 0; + + ran = GenerateUtils.guidNew(); + String guidIntStr = addZeroForNum(ran + "", 10); + + + return GenerateUtils.guidDate + guidIntStr + workId; + } + + public static String addZeroForNum(String str, int strLength) { + int strLen = str.length(); + if (strLen < strLength) { + while (strLen < strLength) { + StringBuffer sb = new StringBuffer(); + sb.append("0").append(str);//左补0 + str = sb.toString(); + strLen = str.length(); + } + } + return str; + } + + + public static List GuidList = new ArrayList(); + public static int GuidIndex = 0; + + + /** + * @author 作者:Jason E-mail: Jason_Lee_lizhen@outlook.com + * @copyright All Rights Reserved @version v1.0 + * @version 创建时间:2021年7月28日 上午10:54:24 + * @description方法说明 :获取最新下标 + * @arg 入参类型:*****;出参类型:**** + */ + public static int getGuidIndex() { + + SimpleDateFormat simpleDateFormatDay = new SimpleDateFormat("yyyyMMddHH"); + String outTradeDate = simpleDateFormatDay.format(new Date()); + if (!outTradeDate.equals(GenerateUtils.guidDate)) { + GenerateUtils.guidDate = outTradeDate; + Collections.shuffle(GuidList); + GenerateUtils.GuidIndex = 0; + } + return GenerateUtils.GuidIndex; + + } + + /** + * @author 作者:Jason E-mail: Jason_Lee_lizhen@outlook.com + * @copyright All Rights Reserved @version v1.0 + * @version 创建时间:2021年7月28日 上午10:54:50 + * @description方法说明 :获取无序单号 + * @arg 入参类型:*****;出参类型:****String 生成的以yyyyMMddHH开头的无序单号 + */ + public static String getGuidRand(String workId) { + + GenerateUtils.GuidIndex += 1; + + long now = System.currentTimeMillis(); + String info = now + ""; + int ran = 0; + + ran = GenerateUtils.getGuidIndex(); + String guidIntStr = GuidList.get(ran); + + + return GenerateUtils.guidDate + guidIntStr + workId; + } + + +// public static void main (String args[]) { +//// for(int i=0;i<999999;i++) { +//// GuidList.add(addZeroForNum(i+"",6)); +//// System.out.println(GuidList.get(i)); +//// } +// for(int i=0;i<999999;i++) { +// System.out.println(getGuidRand("")); +// try { +// Thread.sleep(5 * 1000); +// } catch (InterruptedException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// } +// +// +// } + + // 生成6位随机验证码 + public static String generateCode() { + Random randObj = new Random(); + return Integer.toString(100000 + randObj.nextInt(900000)); + } + + // 生成4位随机验证码 + public static String generateCode4() { + Random randObj = new Random(); + return Integer.toString(1000 + randObj.nextInt(9000)); + } + + + public static void main(String args[]) { + for (int i = 0; i < 100000000; i++) { + System.out.println(getGuid("")); + try { + Thread.sleep(5 * 1000); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + } + + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/GetIpUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/GetIpUtil.java new file mode 100644 index 0000000..18acbfc --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/wechat/utils/GetIpUtil.java @@ -0,0 +1,47 @@ +package com.ruoyi.thirdparty.wechat.utils; + +import com.ruoyi.common.utils.StringUtils; + +import jakarta.servlet.http.HttpServletRequest; + +public class GetIpUtil { + public static String getIpAddress(HttpServletRequest request) { + // 首先, 获取 X-Forwarded-For 中的 IP 地址,它在 HTTP 扩展协议中能表示真实的客户端 IP + String ipAddress = request.getHeader("X-Forwarded-For"); + if (StringUtils.isNotBlank(ipAddress) && !"unknown".equalsIgnoreCase(ipAddress)) { + // 多次反向代理后会有多个 ip 值,第一个 ip 才是真实 ip, 例: X-Forwarded-For: client, proxy1, proxy2,proxy… + int index = ipAddress.indexOf(","); + if (index != -1) { + return ipAddress.substring(0, index); + } + + return ipAddress; + } + + // 如果 X-Forwarded-For 获取不到, 就去获取 X-Real-IP + ipAddress = request.getHeader("X-Real-IP"); + if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) { + // 如果 X-Real-IP 获取不到, 就去获取 Proxy-Client-IP + ipAddress = request.getHeader("Proxy-Client-IP"); + } + if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) { + // 如果 Proxy-Client-IP 获取不到, 就去获取 WL-Proxy-Client-IP + ipAddress = request.getHeader("WL-Proxy-Client-IP"); + } + if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) { + // 如果 WL-Proxy-Client-IP 获取不到, 就去获取 HTTP_CLIENT_IP + ipAddress = request.getHeader("HTTP_CLIENT_IP"); + } + if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) { + // 如果 HTTP_CLIENT_IP 获取不到, 就去获取 HTTP_X_FORWARDED_FOR + ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) { + // 都获取不到, 最后才通过 request.getRemoteAddr() 获取IP + ipAddress = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ipAddress) ? "127.0.0.1" : ipAddress; + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/config/WinicConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/config/WinicConfig.java new file mode 100644 index 0000000..a9a7f73 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/config/WinicConfig.java @@ -0,0 +1,17 @@ +package com.ruoyi.thirdparty.winic.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "winic") +public class WinicConfig { + + private String httpUrl; + + private String username; + + private String password; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/controller/WinicController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/controller/WinicController.java new file mode 100644 index 0000000..6a3c160 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/controller/WinicController.java @@ -0,0 +1,22 @@ +package com.ruoyi.thirdparty.winic.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.thirdparty.winic.service.WinicService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin/winic") +public class WinicController extends BaseController { + + @Autowired + private WinicService winicService; + + @GetMapping("/test") + public AjaxResult test(String mobile, String content) { + return success(winicService.sendSms(mobile, content)); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/service/WinicService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/service/WinicService.java new file mode 100644 index 0000000..95c2783 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/service/WinicService.java @@ -0,0 +1,6 @@ +package com.ruoyi.thirdparty.winic.service; + +public interface WinicService { + + public String sendSms(String mobile, String content); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/service/impl/WinicServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/service/impl/WinicServiceImpl.java new file mode 100644 index 0000000..a30045d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/service/impl/WinicServiceImpl.java @@ -0,0 +1,26 @@ +package com.ruoyi.thirdparty.winic.service.impl; + +import com.ruoyi.thirdparty.winic.config.WinicConfig; +import com.ruoyi.thirdparty.winic.service.WinicService; +import com.ruoyi.thirdparty.winic.util.WinicUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class WinicServiceImpl implements WinicService { + + @Autowired + private WinicConfig winicConfig; + + @Override + public String sendSms(String mobile, String content) { + StringBuffer httpArg = new StringBuffer(); + httpArg.append("id=").append(winicConfig.getUsername()).append("&"); + httpArg.append("pwd=").append(winicConfig.getPassword()).append("&"); + httpArg.append("to=").append(mobile).append("&"); + httpArg.append("content=").append(WinicUtil.encodeUrlString(content, "GB2312")); + + String result = WinicUtil.request(winicConfig.getHttpUrl(), httpArg.toString()); + return result.split("/", 2)[0]; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/util/WinicUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/util/WinicUtil.java new file mode 100644 index 0000000..02c1fa4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/winic/util/WinicUtil.java @@ -0,0 +1,54 @@ +package com.ruoyi.thirdparty.winic.util; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class WinicUtil { + + public static String encodeUrlString(String str, String charset) { + String strret = null; + if (str == null) + return str; + try { + strret = java.net.URLEncoder.encode(str, charset); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return strret; + } + + public static String request(String httpUrl, String httpArg) { + BufferedReader reader = null; + String result = null; + StringBuffer sbf = new StringBuffer(); + httpUrl = httpUrl + "?" + httpArg; + + try { + URL url = new URL(httpUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + InputStream is = connection.getInputStream(); + reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); + String strRead = reader.readLine(); + if (strRead != null) { + sbf.append(strRead); + while ((strRead = reader.readLine()) != null) { + sbf.append("\n"); + sbf.append(strRead); + } + } + reader.close(); + result = sbf.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/config/XinghuoPayConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/config/XinghuoPayConfig.java new file mode 100644 index 0000000..96c035b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/config/XinghuoPayConfig.java @@ -0,0 +1,36 @@ +package com.ruoyi.thirdparty.xinghuopay.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "xinghuopay") +public class XinghuoPayConfig { + + /** + * 服务器URL + */ + private String serverUrl; + + /** + * 商户ID + */ + private String pid; + + /** + * 签名 + */ + private String sign; + + /** + * 通知URL + */ + private String notifyUrl; + + /** + * 返回URL + */ + private String returnUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/controller/XinghuoPayController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/controller/XinghuoPayController.java new file mode 100644 index 0000000..a84bed4 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/controller/XinghuoPayController.java @@ -0,0 +1,51 @@ +package com.ruoyi.thirdparty.xinghuopay.controller; + +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.UserPermission; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.thirdparty.xinghuopay.service.XinghuoPayService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Api(tags = "XinghuoPay") +@Slf4j +@RestController +@RequestMapping("api/xinghuopay") +public class XinghuoPayController extends BaseController { + + @Autowired + private TtUserService ttUserService; + + @Autowired + private XinghuoPayService xinghuoService; + + @ApiOperation("支付") + @UserPermission + @PostMapping(value = "/pay") + public R pay(@RequestBody @Validated CreateOrderParam param) { + Long userId = SecurityUtils.getUserId(); + TtUser ttUser = ttUserService.getById(userId); + // 是否实名认证(0未认证 1已认证) + if ("0".equals(ttUser.getIsRealCheck())) { + return R.realNameRequired(); + } + String ip = IpUtils.getIpAddr(); + return xinghuoService.pay(param, ttUser, ip); + } + + // @GetMapping("/notify") + public String notify(@RequestParam Map params) { + return xinghuoService.notify(params); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/domain/XinghuoPayResponse.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/domain/XinghuoPayResponse.java new file mode 100644 index 0000000..4257e5c --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/domain/XinghuoPayResponse.java @@ -0,0 +1,19 @@ +package com.ruoyi.thirdparty.xinghuopay.domain; + +import lombok.Data; + +@Data +public class XinghuoPayResponse { + + private String code; + + private String msg; + + private String tradeNo; + + private String payurl; + + private String qrcode; + + private String urlscheme; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/service/XinghuoPayService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/service/XinghuoPayService.java new file mode 100644 index 0000000..1cde8cb --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/service/XinghuoPayService.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.xinghuopay.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; + +import java.util.Map; + +public interface XinghuoPayService { + + R pay(CreateOrderParam param, TtUser ttUser, String ip); + + String notify(Map params); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/service/impl/XinghuoPayServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/service/impl/XinghuoPayServiceImpl.java new file mode 100644 index 0000000..bf5df9c --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/service/impl/XinghuoPayServiceImpl.java @@ -0,0 +1,371 @@ +package com.ruoyi.thirdparty.xinghuopay.service.impl; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtPromotionLevel; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import com.ruoyi.thirdparty.xinghuopay.config.XinghuoPayConfig; +import com.ruoyi.thirdparty.xinghuopay.domain.XinghuoPayResponse; +import com.ruoyi.thirdparty.xinghuopay.service.XinghuoPayService; +import com.ruoyi.thirdparty.xinghuopay.util.XinghuoPayUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; + +@Slf4j +@Service +public class XinghuoPayServiceImpl implements XinghuoPayService { + + @Autowired + private TtRechargeProdMapper rechargeProdMapper; + + @Autowired + private XinghuoPayConfig xinghuoPayConfig; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtRechargeProdMapper rechargeListMapper; + + @Autowired + private TtBonusService bonusService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private TtPromotionLevelMapper ttPromotionLevelMapper; + + @Autowired + private TtFirstRechargeMapper ttFirstRechargeMapper; + + @Override + public R pay(CreateOrderParam param, TtUser ttUser, String ip) { + // 1.查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper) + .eq(TtRechargeProd::getId, param.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + return R.fail("不存在的商品"); + } + if (goods.getPrice().compareTo(param.getGoodsPrice()) != 0) { + return R.fail("商品价格不一致"); + } + + // 计算总价值 + BigDecimal totalAmount = param.getGoodsPrice().multiply(new BigDecimal(param.getGoodsNum())); + + // 2.构建支付参数 + Map map = new HashMap<>(); + map.put("pid", xinghuoPayConfig.getPid()); + map.put("type", "alipay"); + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String orderId = String.valueOf(snowflake.nextId()); + map.put("out_trade_no", orderId); + map.put("notify_url", xinghuoPayConfig.getNotifyUrl()); + map.put("return_url", xinghuoPayConfig.getReturnUrl()); + map.put("name", goods.getName()); + map.put("money", String.valueOf(totalAmount)); + map.put("clientip", ip); + String key = xinghuoPayConfig.getSign(); + XinghuoPayUtil signUtil = new XinghuoPayUtil(key); + String sign = signUtil.getSign(map); + map.put("sign", sign); + map.put("sign_type", "MD5"); + + // 3.发送请求 + HttpRequest post = HttpUtil.createPost(xinghuoPayConfig.getServerUrl()); + post.header("Content-Type","application/x-www-form-urlencoded"); + post.formStr(map); + HttpResponse res = post.execute(); + + // 4.解析响应 + XinghuoPayResponse resBody = JSONUtil.toBean(res.body(), XinghuoPayResponse.class); + if (!resBody.getCode().equals("1")) { + return R.fail(resBody.getMsg()); + } + + // 5.创建订单 + TtOrder order = new TtOrder(); + order.setOrderId(orderId); + order.setOutTradeNo(resBody.getTradeNo()); + order.setUserId(ttUser.getUserId()); + order.setType(PayType.XIN_HUO_ZFB.getCode()); + order.setGoodsId(param.getGoodsId()); + order.setGoodsPrice(param.getGoodsPrice()); + order.setGoodsNum(param.getGoodsNum()); + order.setTotalAmount(totalAmount); + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setCreateTime(new Date()); + orderMapper.insert(order); + + return R.ok(resBody); + } + + @Override + public String notify(Map params) { + log.info("支付成功通知"); + String result = "failure"; + + // 校验 + String tradeStatus = params.get("trade_status"); + if (!"TRADE_SUCCESS".equals(tradeStatus)) { + log.error("交易失败"); + return result; + } + + // 1.查询订单信息 + String outTradeNo = params.get("trade_no"); + TtOrder order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOutTradeNo, outTradeNo) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .one(); + // 防止重复通知 + if (!PayOrderStatus.NO_PAY.getCode().equals(order.getStatus())) { + return result; + } + + // 2.查询用户信息 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + + // 3.查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeListMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + + // 4.账户结算 + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + + try { + // 首充赠送 + firstChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广等级充值赠送 + promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广充值返佣 + promotionChargeCommission(user, goods, order.getGoodsNum()); + } catch (Exception e) { + e.printStackTrace(); + } + + // 5.更新订单 + boolean update = new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .update(); + if (update) { + result = "success"; + } + + // 6.发送充值通知 + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(order.getUserId().toString(), order.getGoodsPrice()); + + return result; + } + + // 账户结算 + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + + // 加钱(充值不加积分:productA + productC 全部计入 account_amount) + // BigDecimal totalAmount = order.getTotalAmount(); + BigDecimal productAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal productGift = goods.getProductC().multiply(new BigDecimal(goodsNumber)); + BigDecimal totalAmount = productAmount.add(productGift); + BigDecimal totalCredits = BigDecimal.ZERO; + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount.toString()); + userService.update(userUpdate); + + user = userService.getById(user.getUserId()); + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + + .amount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? totalAmount : null) + .finalAmount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountAmount().add(totalAmount) : null) + + .credits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? totalCredits : null) + .finalCredits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountCredits().add(totalCredits) : null) + + .total(totalAmount.add(totalCredits)) // 收支合计 + + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + userBlendErcashMapper.insert(blendErcash); + + // 充值记录 + TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build(); + ttRechargeRecord.setUserId(order.getUserId()); + ttRechargeRecord.setParentId(user.getParentId()); + ttRechargeRecord.setArrivalAmount(totalAmount); + ttRechargeRecord.setAmountActuallyPaid(totalAmount); + ttRechargeRecord.setFinallyPrice(user.getAccountAmount()); + ttRechargeRecord.setOrderId(order.getOrderId()); + ttRechargeRecord.setOutTradeNo(order.getOutTradeNo()); + ttRechargeRecord.setStatus("0"); + ttRechargeRecord.setChannelType("1"); + ttRechargeRecord.setCreateTime(DateUtils.getNowDate()); + ttRechargeRecordMapper.insert(ttRechargeRecord); + + // 更新VIP等级 + userService.updateUserVIPLevel(user.getUserId()); + } + + /** + * 首充赠送 + */ + private void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.判断是否为首充 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtUserBlendErcash::getSource, 1) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()); + List ttUserBlendErcashes = userBlendErcashMapper.selectList(wrapper); + if (ttUserBlendErcashes.size() > 0) { + return; + } + + // 2.加钱 + // 充值金额 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + // 查询赠送比例 + BigDecimal firstChargeAmountRatio = ttFirstRechargeMapper.selectRatioByMinAmount(totalAmount); + if (Objects.isNull(firstChargeAmountRatio)) { + return; + } + // 计算赠送金额 + BigDecimal giftAmount = totalAmount.multiply(firstChargeAmountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.FIRST_CHARGE.getCode()) + .remark(TtAccountRecordSource.FIRST_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广等级充值赠送 + */ + private void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.查询用户推广等级对应的赠送比例 + TtPromotionLevel ttPromotionLevel = ttPromotionLevelMapper.selectById(ttUser.getPromotionLevel()); + if (Objects.isNull(ttPromotionLevel)) + return; + + // 2.加钱 + BigDecimal amountRatio = ttPromotionLevel.getRechargeGiftRatio(); + if (Objects.isNull(amountRatio)) + return; + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal giftAmount = totalAmount.multiply(amountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getCode()) + .remark(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广充值返佣 + */ + private void promotionChargeCommission(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + if (Objects.isNull(ttUser.getParentId())) { + return; + } + // 充值返佣统一走 TtBonusService:按各级上级 commissionRate 逐级向上实时返佣、招商周结, + // 写 source=13,与卡密充值(ApiRechargeServiceImpl→bonus)口径一致,避免使用推广等级费率造成口径不符。 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + bonusService.distributeRechargeCommission(ttUser.getUserId(), totalAmount); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/util/XinghuoPayUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/util/XinghuoPayUtil.java new file mode 100644 index 0000000..89ab5ae --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/xinghuopay/util/XinghuoPayUtil.java @@ -0,0 +1,76 @@ +package com.ruoyi.thirdparty.xinghuopay.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.TreeMap; + +public class XinghuoPayUtil { + + private String key; + + public XinghuoPayUtil(String key) { + this.key = key; + } + + /** + * 生成MD5签名 + * + * @param params 参数列表 + * @return 生成的签名 + */ + public String getSign(Map params) { + // 排序参数 + Map sortedParams = new TreeMap<>(params); + sortedParams.remove("sign"); + sortedParams.remove("sign_type"); + + // 构建签名字符串 + StringBuilder signStr = new StringBuilder(); + for (Map.Entry entry : sortedParams.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + signStr.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); + } + } + + if (signStr.length() > 0) { + signStr.setLength(signStr.length() - 1); // 移除最后一个& + } + + // 添加商户密钥 + signStr.append(this.key); + + // 计算MD5签名 + return md5(signStr.toString()).toLowerCase(); + } + + /** + * 计算MD5哈希值 + * + * @param input 输入字符串 + * @return MD5哈希值 + */ + private static String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(input.getBytes()); + return bytesToHex(messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 algorithm not found", e); + } + } + + /** + * 将字节数组转换为十六进制字符串 + * + * @param bytes 字节数组 + * @return 十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/config/YYConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/config/YYConfig.java new file mode 100644 index 0000000..ad0a1ef --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/config/YYConfig.java @@ -0,0 +1,70 @@ +package com.ruoyi.thirdparty.yyyouping.config; + +import com.ruoyi.thirdparty.yyyouping.utils.YYClient; +import com.ruoyi.thirdparty.yyyouping.utils.common.RSAUtils; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "yy-youping") +public class YYConfig { + + private String appKey; + private String publicKey; + private String privateKey; + + public static String BaseUrl = "https://gw-openapi.youpin898.com"; + /** + * 用户相关接口 + */ + public static final String ApiGetAssetsInfo = "/open/v1/api/getAssetsInfo"; + public static final String ApiCheckTradeUrl = "/open/v1/api/checkTradeUrl"; + public static final String ApiDetailDataQueryAplly = "/open/v1/api/detailDataQueryAplly"; + public static final String ApiDetailDataQueryResult = "/open/v1/api/detailDataQueryResult"; + + /** + * 市场查询接口 + */ + public static final String ApiTemplateQuery = "/open/v1/api/templateQuery"; + public static final String ApiGoodsQuery = "/open/v1/api/goodsQuery"; + public static final String ApiBatchGetOnSaleCommodityInfo = "/open/v1/api/batchGetOnSaleCommodityInfo"; + public static final String ApiQueryTemplateSaleByCategory = "/open/v1/api/queryTemplateSaleByCategory"; + public static final String ApiQueryViewChart = "/open/v1/api/queryViewChart"; + + /** + * 购买接口 + */ + public static final String ApiByTemplateCreateOrder = "/open/v1/api/byTemplateCreateOrder"; + public static final String ApiByGoodsIdCreateOrder = "/open/v1/api/byGoodsIdCreateOrder"; + public static final String ApiByTemplateAsyncCreateOrder = "/open/v1/api/byTemplateAsyncCreateOrder"; + public static final String ApiByGoodsIdAsyncCreateOrder = "/open/v1/api/byGoodsIdAsyncCreateOrder"; + + /** + * 买家订单接口 + */ + public static final String ApiOrderCancel = "/open/v1/api/orderCancel"; + public static final String ApiOrderStatus = "/open/v1/api/orderStatus"; + public static final String ApiOrderInfo = "/open/v1/api/orderInfo"; + + /** + * 出售接口 + */ + public static final String ApiGetUserSteamInventoryData = "/open/v1/api/getUserSteamInventoryData"; + public static final String ApiGetUserOnSaleCommodityData = "/open/v1/api/getUserOnSaleCommodityData"; + + /** + * 卖家订单接口 + */ + public static final String ApiSellerQueryOrderList = "/open/v1/api/sellerQueryOrderList"; + public static final String ApiSellerOrderStatus = "/open/v1/api/sellerOrderStatus"; + public static final String ApiSellerOrderDetail = "/open/v1/api/sellerOrderDetail"; + + @Bean + public RSAUtils yyClient(){ + return new RSAUtils(appKey,publicKey,privateKey); + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/controller/yyyoupingController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/controller/yyyoupingController.java new file mode 100644 index 0000000..7df3339 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/controller/yyyoupingController.java @@ -0,0 +1,117 @@ +package com.ruoyi.thirdparty.yyyouping.controller; + + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.TtOrnamentYYService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.TtOrnamentsYY; +import com.ruoyi.thirdparty.yyyouping.service.yyyoupingService; +import com.ruoyi.thirdparty.yyyouping.utils.YYClient; +import com.ruoyi.thirdparty.yyyouping.utils.common.YYResult; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Api(tags = "YYYouPing") +@RestController +@RequestMapping("api/yyyouping") +public class yyyoupingController { + + @Autowired + private yyyoupingService yyyoupingService; + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static class Param { + private Integer templateId; + private String templateHashName; + } + + @Autowired + private TtOrnamentYYService ttOrnamentYYService; + + // 加载平台的饰品数据 + @ApiOperation("加载平台的饰品数据") + @GetMapping("loadData") + public R loadData(){ + // 读json + File file = new File("C:\\Users\\Administrator\\Desktop\\response.json"); + JSONArray array = JSONUtil.readJSONArray(file, StandardCharsets.UTF_8); + List list = JSONUtil.toList(array, TtOrnamentsYY.class); + // 写db + ttOrnamentYYService.saveBatch(list); + return R.ok(); + } + + // 通过此接口可获取悠悠有品所有商品对应的模板ID的下载链接。 + @ApiOperation("查询模板ID下载链接") + @GetMapping("yyApiTemplateQuery") + public R yyApiTemplateQuery(){ + YYResult yyResult = yyyoupingService.yyApiTemplateQuery(new HashMap<>()); + if (!yyResult.getCode().equals(0)) return R.fail(yyResult); + return R.ok(yyResult); + } + + @ApiOperation("查询商品列表") + @PostMapping("yyApiGoodsQuery") + public R yyApiGoodsQuery(@RequestBody Map map){ + yyyoupingService.yyApiGoodsQuery(map); + return R.ok(); + } + + @ApiOperation("余额查询") + @PostMapping("yyApiGetAssetsInfo") + public R yyApiGetAssetsInfo(){ + YYResult yyResult = yyyoupingService.yyApiGetAssetsInfo(); + if (!yyResult.getCode().equals(0)) return R.fail(yyResult); + return R.ok(yyResult); + } + + @ApiOperation("批量查询模板饰品的在售列表") + @PostMapping("/yyApiBatchGetOnSaleCommodityInfo") + public R balance(@RequestBody List ids) { + ArrayList list = new ArrayList<>(); + HashMap param = new HashMap<>(); + for (String id:ids){ + HashMap map = new HashMap<>(); + map.put("templateId",id); + list.add(map); + } + param.put("requestList",list); + YYResult yyResult = yyyoupingService.yyApiBatchGetOnSaleCommodityInfo(param); + return R.ok(yyResult); + } + + @ApiOperation("按类别查询模板销售") + @GetMapping("/yyApiQueryTemplateSaleByCategory") + public R yyApiQueryTemplateSaleByCategory() throws Exception { + HashMap map = new HashMap<>(); + + // map.put("typeHashName","FAMAS | Commemoration (Factory New)"); + map.put("weaponHashName","FAMAS | Commemoration (Factory New)"); + map.put("page",1); + map.put("size",10); + + // return yyClient.yyApiQueryTemplateSaleByCategory(map); + + return R.ok(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/service/Impl/yyyoupingServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/service/Impl/yyyoupingServiceImpl.java new file mode 100644 index 0000000..644ec62 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/service/Impl/yyyoupingServiceImpl.java @@ -0,0 +1,176 @@ +package com.ruoyi.thirdparty.yyyouping.service.Impl; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson2.JSON; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.thirdparty.yyyouping.config.YYConfig; +import com.ruoyi.thirdparty.yyyouping.service.yyyoupingService; +import com.ruoyi.thirdparty.yyyouping.utils.common.RSAUtils; +import com.ruoyi.thirdparty.yyyouping.utils.common.YYResult; +import io.swagger.annotations.Api; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.PostMapping; + +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; + +import static com.ruoyi.thirdparty.yyyouping.config.YYConfig.*; + + +@Slf4j +@Service +public class yyyoupingServiceImpl implements yyyoupingService { + + @Autowired + private YYConfig yyConfig; + + @Autowired + private RSAUtils rsaUtils; + + // 添加基本请求参数 + private Map createParam(Map param) { + try { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + param.put("appKey", yyConfig.getAppKey()); + param.put("timestamp", dateFormat.format(new Timestamp(System.currentTimeMillis()))); + param.put("sign", rsaUtils.getSign(param)); + }catch (Exception e){ + log.warn("参数构造异常"); + } + return param; + } + + + public YYResult yyApiBatchGetOnSaleCommodityInfo(Map param) { + + Map p = createParam(param); + HttpRequest post = HttpUtil.createPost(BaseUrl + ApiBatchGetOnSaleCommodityInfo); + post.body(JSONUtil.toJsonStr(p)); + HttpResponse res = post.execute(); + String body = res.body(); + + return JSONUtil.toBean(body, YYResult.class); + + } + + // 通过此接口可获取悠悠有品所有商品对应的模板ID的下载链接。 + public YYResult yyApiTemplateQuery(Map param) { + + try { + + Map p = createParam(param); + + HttpRequest post = HttpUtil.createPost(BaseUrl + ApiTemplateQuery); + post.body(JSON.toJSONString(p), "json"); + String body = post.execute().body(); + + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(body, YYResult.class); + + } catch (Exception e) { + log.warn("yyApiTemplateQuery"); + return YYResult.builder().code(005).msg("yyApiTemplateQuery").build(); + } + } + + + public YYResult yyApiQueryTemplateSaleByCategory(Map param) { + + Map p = createParam(param); + String res = HttpUtil.post(BaseUrl + ApiQueryTemplateSaleByCategory, p); + ObjectMapper objectMapper = new ObjectMapper(); + + try { + return objectMapper.readValue(res, YYResult.class); + }catch (Exception e){ + log.warn("响应结果解析异常"); + return YYResult.builder().code(005).msg("响应结果解析异常").build(); + } + } + + public YYResult yyApiGetAssetsInfo() { + + try { + + Map p = createParam(new HashMap()); + + HttpRequest post = HttpUtil.createPost(BaseUrl + ApiGetAssetsInfo); + post.body(JSON.toJSONString(p),"json"); + String body = post.execute().body(); + + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(body, YYResult.class); + + } catch (Exception e) { + log.warn("yyApiGetAssetsInfo"); + return YYResult.builder().code(005).msg("yyApiGetAssetsInfo").build(); + } + } + + public YYResult yyApiGoodsQuery(Map param) { + + try { + + Map p = createParam(param); + String res = HttpUtil.post(BaseUrl + ApiGoodsQuery, p); + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(res, YYResult.class); + + } catch (Exception e) { + log.warn("参数构造异常"); + return YYResult.builder().code(005).msg("参数构造异常").build(); + } + + } + + public YYResult yyApiByGoodsIdCreateOrder(Map param) { + try { + + Map p = createParam(param); + String res = HttpUtil.post(BaseUrl + ApiByGoodsIdCreateOrder, p); + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(res, YYResult.class); + + } catch (Exception e) { + log.warn("参数构造异常"); + return YYResult.builder().code(005).msg("参数构造异常").build(); + } + } + + public YYResult yyApiOrderInfo(Map param) { + try { + + Map p = createParam(param); + String res = HttpUtil.post(BaseUrl + ApiOrderInfo, p); + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(res, YYResult.class); + + } catch (Exception e) { + log.warn("参数构造异常"); + return YYResult.builder().code(005).msg("参数构造异常").build(); + } + } + + public YYResult yyApiGetUserSteamInventoryData(Map param) { + try { + + Map p = createParam(param); + String res = HttpUtil.post(BaseUrl + ApiGetUserSteamInventoryData, p); + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(res, YYResult.class); + + } catch (Exception e) { + log.warn("参数构造异常"); + return YYResult.builder().code(005).msg("参数构造异常").build(); + } + } + + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/service/yyyoupingService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/service/yyyoupingService.java new file mode 100644 index 0000000..f9cc36f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/service/yyyoupingService.java @@ -0,0 +1,23 @@ +package com.ruoyi.thirdparty.yyyouping.service; + +import com.ruoyi.thirdparty.yyyouping.config.YYConfig; +import com.ruoyi.thirdparty.yyyouping.utils.common.YYResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Service; + +import java.util.Map; + +public interface yyyoupingService { + + YYResult yyApiBatchGetOnSaleCommodityInfo(Map param); + YYResult yyApiTemplateQuery(Map param); + YYResult yyApiQueryTemplateSaleByCategory(Map param); + YYResult yyApiGetAssetsInfo(); + YYResult yyApiGoodsQuery(Map param); + YYResult yyApiByGoodsIdCreateOrder(Map param); + YYResult yyApiOrderInfo(Map param); + YYResult yyApiGetUserSteamInventoryData(Map param); + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/YYClient.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/YYClient.java new file mode 100644 index 0000000..5157347 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/YYClient.java @@ -0,0 +1,256 @@ +package com.ruoyi.thirdparty.yyyouping.utils; + +import com.alibaba.fastjson2.JSON; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.thirdparty.yyyouping.utils.common.RSAUtils; +import com.ruoyi.thirdparty.yyyouping.utils.common.YYResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + + +@Slf4j +public class YYClient { + + private RSAUtils rsaUtils; + + private static String BaseUrl = "https://gw-openapi.youpin898.com"; + + /** + * 用户相关接口 + */ + private static String ApiGetAssetsInfo = "/open/v1/api/getAssetsInfo"; + private static String ApiCheckTradeUrl = "/open/v1/api/checkTradeUrl"; + private static String ApiDetailDataQueryAplly = "/open/v1/api/detailDataQueryAplly"; + private static String ApiDetailDataQueryResult = "/open/v1/api/detailDataQueryResult"; + + /** + * 市场查询接口 + */ + private static String ApiTemplateQuery = "/open/v1/api/templateQuery"; + private static String ApiGoodsQuery = "/open/v1/api/goodsQuery"; + private static String ApiBatchGetOnSaleCommodityInfo = "/open/v1/api/batchGetOnSaleCommodityInfo"; + private static String ApiQueryTemplateSaleByCategory = "/open/v1/api/queryTemplateSaleByCategory"; + private static String ApiQueryViewChart = "/open/v1/api/queryViewChart"; + + /** + * 购买接口 + */ + private static String ApiByTemplateCreateOrder = "/open/v1/api/byTemplateCreateOrder"; + private static String ApiByGoodsIdCreateOrder = "/open/v1/api/byGoodsIdCreateOrder"; + private static String ApiByTemplateAsyncCreateOrder = "/open/v1/api/byTemplateAsyncCreateOrder"; + private static String ApiByGoodsIdAsyncCreateOrder = "/open/v1/api/byGoodsIdAsyncCreateOrder"; + + /** + * 买家订单接口 + */ + private static String ApiOrderCancel = "/open/v1/api/orderCancel"; + private static String ApiOrderStatus = "/open/v1/api/orderStatus"; + private static String ApiOrderInfo = "/open/v1/api/orderInfo"; + + /** + * 出售接口 + */ + private static String ApiGetUserSteamInventoryData = "/open/v1/api/getUserSteamInventoryData"; + private static String ApiGetUserOnSaleCommodityData = "/open/v1/api/getUserOnSaleCommodityData"; + + /** + * 卖家订单接口 + */ + private static String ApiSellerQueryOrderList = "/open/v1/api/sellerQueryOrderList"; + private static String ApiSellerOrderStatus = "/open/v1/api/sellerOrderStatus"; + private static String ApiSellerOrderDetail = "/open/v1/api/sellerOrderDetail"; + + + public YYClient(String appKey,String publicKey,String privateKey){ + this.rsaUtils = new RSAUtils(appKey,publicKey,privateKey); + } + + public String yyApiTemplateQuery(Map param) { + Map p = null; + try { + p = createParam(param); + }catch (Exception e){ + log.warn("YYClient 参数构造异常"); + return ""; + } + + return doPostToJson(BaseUrl+ApiTemplateQuery, JSON.toJSONString(p)); + } + + //ApiBatchGetOnSaleCommodityInfo + public YYResult yyApiBatchGetOnSaleCommodityInfo(Map param) { + + try { + Map p = createParam(param); + + String s = doPostToJson(BaseUrl + ApiBatchGetOnSaleCommodityInfo, JSON.toJSONString(p)); + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(s, YYResult.class); + + }catch (Exception e){ + log.warn("YYClient 参数构造异常"); + return YYResult.builder().code(005).msg("YYClient 参数构造异常").build(); + } + } + + public String yyApiQueryTemplateSaleByCategory(Map param) { + Map p = null; + try { + p = createParam(param); + }catch (Exception e){ + log.warn("YYClient 参数构造异常"); + return ""; + } + + return doPostToJson(BaseUrl+ApiQueryTemplateSaleByCategory, JSON.toJSONString(p)); + } + + public String yyApiGetAssetsInfo(Map param) throws Exception { + Map p = createParam(param); + return doPostToJson(BaseUrl+ApiGetAssetsInfo, JSON.toJSONString(p)); + } + + public String yyApiGoodsQuery(Map param) { + + Map p = null; + try { + p = createParam(param); + }catch (Exception e){ + log.warn("参数构建异常"); + return ""; + } + + return doPostToJson(BaseUrl+ApiGoodsQuery,JSON.toJSONString(p)); + } + + public String yyApiByGoodsIdCreateOrder(Map param) throws Exception { + Map p = createParam(param); + return doPostToJson(BaseUrl+ApiByGoodsIdCreateOrder,JSON.toJSONString(p)); + } + + public String yyApiOrderInfo(Map param) throws Exception { + Map p = createParam(param); + return doPostToJson(BaseUrl+ApiOrderInfo,JSON.toJSONString(p)); + } + + public String yyApiGetUserSteamInventoryData(Map param) throws Exception { + Map p = createParam(param); + return doPostToJson(BaseUrl+ApiGetUserSteamInventoryData,JSON.toJSONString(p)); + } + + private Map createParam(Map param) throws Exception { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + param.put("appKey",rsaUtils.getAppKey()); + param.put("timestamp",dateFormat.format(new Timestamp(System.currentTimeMillis()))); + param.put("sign",rsaUtils.getSign(param)); + return param; + } + + // 基础post请求 + public static String doPostToJson(String urlPath, String Json) { + + String result = ""; + BufferedReader reader = null; + HttpURLConnection conn = null; + try { + trustAllHosts(); + URL url = new URL(urlPath); + if (url.getProtocol().toLowerCase().equals("https")) { + HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection(); + httpsConn.setHostnameVerifier(DO_NOT_VERIFY); + conn = httpsConn; + } else { + conn = (HttpURLConnection) url.openConnection(); + } + + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestProperty("Connection", "Keep-Alive"); + conn.setRequestProperty("Charset", "UTF-8"); + conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + // conn.setRequestProperty("accept","*/*"); + conn.setRequestProperty("accept", "application/json"); + if (Json != null) { + byte[] writebytes = Json.getBytes(); + conn.setRequestProperty("Content-Length", String.valueOf(writebytes.length)); + OutputStream outwritestream = conn.getOutputStream(); + outwritestream.write(Json.getBytes()); + outwritestream.flush(); + outwritestream.close(); + } + + if (conn.getResponseCode() == 200) { + reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); + result = reader.readLine(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return result; + } + + final static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() { + public boolean verify(String arg0, SSLSession arg1) { + return true; + } + }; + + public static void trustAllHosts() { + TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[]{}; + } + + public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { + + } + + public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { + + } + } + + }; + + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, trustAllCerts, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } catch (Exception e) { + e.printStackTrace(); + } + + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/BaseException.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/BaseException.java new file mode 100644 index 0000000..1c91149 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/BaseException.java @@ -0,0 +1,150 @@ +package com.ruoyi.thirdparty.yyyouping.utils.common; + +public class BaseException extends RuntimeException { + private Status status; + private String message; + private Throwable e; + + public BaseException(Status status) { + super(status.getMsg()); + this.status = status; + this.message = status.getMsg(); + } + + public BaseException(Status status, String message) { + super(message); + this.status = status; + this.message = message; + } + + public BaseException(Status status, Throwable cause) { + super(status.getMsg(), cause); + this.status = status; + this.e = cause; + this.message = status.getMsg(); + } + + public BaseException(Status status, String message, Throwable cause) { + super(message, cause); + this.status = status; + this.message = message; + this.e = cause; + } + + public BaseException() { + } + + public static BaseException of(Status status) { + return new BaseException(status); + } + + public static BaseException of(Status status, String message) { + return new BaseException(status, message); + } + + public static BaseException of(Status status, Throwable cause) { + return new BaseException(status, cause); + } + + public static BaseException of(Status status, String message, Throwable cause) { + return new BaseException(status, message, cause); + } + + public static BaseException of(Status status, Throwable cause, String msg) { + return new BaseException(status, msg, cause); + } + + public Status getStatus() { + return this.status; + } + + public String getMessage() { + return this.message; + } + + public Throwable getE() { + return this.e; + } + + public void setStatus(Status status) { + this.status = status; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setE(Throwable e) { + this.e = e; + } + + public boolean equals(Object o) { + if (o == this) { + return true; + } else if (!(o instanceof BaseException)) { + return false; + } else { + BaseException other = (BaseException)o; + if (!other.canEqual(this)) { + return false; + } else { + label47: { + Object this$status = this.getStatus(); + Object other$status = other.getStatus(); + if (this$status == null) { + if (other$status == null) { + break label47; + } + } else if (this$status.equals(other$status)) { + break label47; + } + + return false; + } + + Object this$message = this.getMessage(); + Object other$message = other.getMessage(); + if (this$message == null) { + if (other$message != null) { + return false; + } + } else if (!this$message.equals(other$message)) { + return false; + } + + Object this$e = this.getE(); + Object other$e = other.getE(); + if (this$e == null) { + if (other$e != null) { + return false; + } + } else if (!this$e.equals(other$e)) { + return false; + } + + return true; + } + } + } + + protected boolean canEqual(Object other) { + return other instanceof BaseException; + } + + public int hashCode() { + boolean PRIME = true; + int result = 1; + Object $status = this.getStatus(); + result = result * 59 + ($status == null ? 43 : $status.hashCode()); + Object $message = this.getMessage(); + result = result * 59 + ($message == null ? 43 : $message.hashCode()); + Object $e = this.getE(); + result = result * 59 + ($e == null ? 43 : $e.hashCode()); + return result; + } + + public String toString() { + Status var10000 = this.getStatus(); + return "BaseException(status=" + var10000 + ", message=" + this.getMessage() + ", e=" + this.getE() + ")"; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/JacksonUtils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/JacksonUtils.java new file mode 100644 index 0000000..954f0ee --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/JacksonUtils.java @@ -0,0 +1,545 @@ +package com.ruoyi.thirdparty.yyyouping.utils.common; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.core.type.ResolvedType; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Date; + +import org.springframework.stereotype.Component; + +@Component +public class JacksonUtils { + private static ObjectMapper objectMapper = null; + + public JacksonUtils() { + } + + private static ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(Include.NON_NULL); + objectMapper.configure(SerializationFeature.INDENT_OUTPUT, Boolean.FALSE); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime.class, new JsonSerializer() { + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(value.toInstant(ZoneOffset.of("+8")).toEpochMilli()); + } + }); + javaTimeModule.addDeserializer(LocalDateTime.class, new JsonDeserializer() { + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return (new Date(Long.parseLong(p.getValueAsString()))).toInstant().atOffset(ZoneOffset.of("+8")).toLocalDateTime(); + } + }); + objectMapper.registerModule(javaTimeModule); + return objectMapper; + } + + public static ObjectMapper getObjectMapper() { + return objectMapper; + } + + public static T readValue(JsonParser p, Class valueType) { + try { + return objectMapper.readValue(p, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(JsonParser p, TypeReference valueTypeRef) { + try { + return objectMapper.readValue(p, valueTypeRef); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(JsonParser p, ResolvedType valueType) { + try { + return objectMapper.readValue(p, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(JsonParser p, JavaType valueType) { + try { + return objectMapper.readValue(p, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readTree(JsonParser p) { + try { + return objectMapper.readTree(p); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + + public static MappingIterator readValues(JsonParser p, ResolvedType valueType) { + try { + return objectMapper.readValues(p, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static MappingIterator readValues(JsonParser p, JavaType valueType) { + try { + return objectMapper.readValues(p, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static MappingIterator readValues(JsonParser p, Class valueType) { + try { + return objectMapper.readValues(p, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static MappingIterator readValues(JsonParser p, TypeReference valueTypeRef) { + try { + return objectMapper.readValues(p, valueTypeRef); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static JsonNode readTree(InputStream in) { + try { + return objectMapper.readTree(in); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + + public static JsonNode readTree(Reader r) { + try { + return objectMapper.readTree(r); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + + public static JsonNode readTree(String content) { + try { + return objectMapper.readTree(content); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + + public static JsonNode readTree(byte[] content) { + try { + return objectMapper.readTree(content); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + + public static JsonNode readTree(File file) { + try { + return objectMapper.readTree(file); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + + public static JsonNode readTree(URL source) { + try { + return objectMapper.readTree(source); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + + public static void writeValue(JsonGenerator g, Object value) { + try { + objectMapper.writeValue(g, value); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static void writeTree(JsonGenerator g, TreeNode rootNode) { + try { + objectMapper.writeTree(g, rootNode); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static void writeTree(JsonGenerator g, JsonNode rootNode) { + try { + objectMapper.writeTree(g, rootNode); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T treeToValue(TreeNode n, Class valueType) { + try { + return objectMapper.treeToValue(n, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T valueToTree(Object fromValue) { + try { + return objectMapper.valueToTree(fromValue); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + + public static T readValue(File src, Class valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(File src, TypeReference valueTypeRef) { + try { + return objectMapper.readValue(src, valueTypeRef); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(File src, JavaType valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(URL src, Class valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(URL src, TypeReference valueTypeRef) { + try { + return objectMapper.readValue(src, valueTypeRef); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(URL src, JavaType valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(String content, Class valueType) { + try { + return objectMapper.readValue(content, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(String content, TypeReference valueTypeRef) { + try { + return objectMapper.readValue(content, valueTypeRef); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(String content, JavaType valueType) { + try { + return objectMapper.readValue(content, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(Reader src, Class valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(Reader src, TypeReference valueTypeRef) { + try { + return objectMapper.readValue(src, valueTypeRef); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(Reader src, JavaType valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(InputStream src, Class valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(InputStream src, TypeReference valueTypeRef) { + try { + return objectMapper.readValue(src, valueTypeRef); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(InputStream src, JavaType valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(byte[] src, Class valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(byte[] src, int offset, int len, Class valueType) { + try { + return objectMapper.readValue(src, offset, len, valueType); + } catch (Exception var5) { + var5.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var5); + } + } + + public static T readValue(byte[] src, TypeReference valueTypeRef) { + try { + return objectMapper.readValue(src, valueTypeRef); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(byte[] src, int offset, int len, TypeReference valueTypeRef) { + try { + return objectMapper.readValue(src, offset, len, valueTypeRef); + } catch (Exception var5) { + var5.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var5); + } + } + + public static T readValue(byte[] src, JavaType valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(byte[] src, int offset, int len, JavaType valueType) { + try { + return objectMapper.readValue(src, offset, len, valueType); + } catch (Exception var5) { + var5.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var5); + } + } + + public static T readValue(DataInput src, Class valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T readValue(DataInput src, JavaType valueType) { + try { + return objectMapper.readValue(src, valueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static void writeValue(File resultFile, Object value) { + try { + objectMapper.writeValue(resultFile, value); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static void writeValue(OutputStream out, Object value) { + try { + objectMapper.writeValue(out, value); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static void writeValue(DataOutput out, Object value) { + try { + objectMapper.writeValue(out, value); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static void writeValue(Writer w, Object value) { + try { + objectMapper.writeValue(w, value); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static String writeValueAsString(Object value) { + if (null == value) { + return null; + } else { + try { + return objectMapper.writeValueAsString(value); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + } + + public static byte[] writeValueAsBytes(Object value) { + try { + return objectMapper.writeValueAsBytes(value); + } catch (Exception var2) { + var2.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var2); + } + } + + public static T convertValue(Object fromValue, Class toValueType) { + try { + return objectMapper.convertValue(fromValue, toValueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T convertValue(Object fromValue, TypeReference toValueTypeRef) { + try { + return objectMapper.convertValue(fromValue, toValueTypeRef); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + public static T convertValue(Object fromValue, JavaType toValueType) { + try { + return objectMapper.convertValue(fromValue, toValueType); + } catch (Exception var3) { + var3.printStackTrace(); + throw BaseException.of(Status.JACKSON_EXCEPTION, var3); + } + } + + static { + objectMapper = objectMapper(); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/RSAUtils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/RSAUtils.java new file mode 100644 index 0000000..b145a04 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/RSAUtils.java @@ -0,0 +1,549 @@ +package com.ruoyi.thirdparty.yyyouping.utils.common; + +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import org.slf4j.Logger; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.LoggerFactory; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * SHA256withRSA + * + * @author zhumiao + * + */ +@Data +public class RSAUtils { + + private static final Logger logger = LoggerFactory.getLogger(RSAUtils.class); + + // MAX_DECRYPT_BLOCK应等于密钥长度/8(1byte=8bit),所以当密钥位数为2048时,最大解密长度应为256. + // 128 对应 1024,256对应2048 + private static final int KEYSIZE = 2048; + + // RSA最大加密明文大小 + private static final int MAX_ENCRYPT_BLOCK = 117; + + // RSA最大解密密文大小 + private static final int MAX_DECRYPT_BLOCK = 256; + + // 不仅可以使用DSA算法,同样也可以使用RSA算法做数字签名 + private static final String KEY_ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; + + + // 默认种子 请根据自己的需要定义 + public static final String DEFAULT_SEED = "xxxxxxxxxxxxxxxxxx"; + + /** + * 目前固定公钥、私钥,有需求再改动 + */ + public String publicKey; + + public String privateKey; + public String appKey; + + public static final String PUBLIC_KEY = "PublicKey"; + public static final String PRIVATE_KEY = "PrivateKey"; + + public RSAUtils(String appKey,String publicKey,String privateKey){ + this.appKey = appKey; + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + /** + * + * 生成密钥 + * + * @param seed 种子 + * + * @return 密钥对象 + * @throws Exception + * + */ + public static Map initKey(String seed) throws Exception { + logger.debug("生成密钥"); + KeyPairGenerator keygen = KeyPairGenerator.getInstance(KEY_ALGORITHM); + SecureRandom secureRandom = new SecureRandom(); + // 如果指定seed,那么secureRandom结果是一样的,所以生成的公私钥也永远不会变 + secureRandom.setSeed(seed.getBytes()); + // Modulus size must range from 512 to 1024 and be a multiple of 64 + keygen.initialize(KEYSIZE, secureRandom); + KeyPair keys = keygen.genKeyPair(); + PrivateKey privateKey = keys.getPrivate(); + PublicKey publicKey = keys.getPublic(); + Map map = new HashMap<>(2); + map.put(PUBLIC_KEY, publicKey); + map.put(PRIVATE_KEY, privateKey); + return map; + } + + /** + * + * 生成默认密钥 + * + * + * @return 密钥对象 + * @throws Exception + * + */ + + public static Map initKey() throws Exception { + return initKey(DEFAULT_SEED); + } + + /** + * + * 取得私钥 + * + * @param keyMap + * + * @return + * @throws Exception + * + */ + public static String getPrivateKey(Map keyMap) throws Exception { + Key key = (Key) keyMap.get(PRIVATE_KEY); + // return encryptBASE64(key.getEncoded()); // base64加密私钥 + return base64ToStr(key.getEncoded()); // base64加密私钥 + } + + private static String base64ToStr(byte[] encoded) { + return java.util.Base64.getEncoder().encodeToString(encoded); + } + + /** + * + * 取得公钥 + * + * @param keyMap + * + * @return + * @throws Exception + * + */ + public static String getPublicKey(Map keyMap) throws Exception { + Key key = (Key) keyMap.get(PUBLIC_KEY); + // return encryptBASE64(key.getEncoded()); // base64加密公钥 + return base64ToStr(key.getEncoded()); // base64加密公钥 + } + + /** + * + * 用私钥对信息进行数字签名 + * + * @param data 加密数据 + * + * @param privateKey 私钥-base64加密的 + * + * @return + * + * @throws Exception + * + */ + public static String signByPrivateKey(byte[] data, String privateKey) throws Exception { + byte[] keyBytes = decryptBASE64(privateKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM); + PrivateKey priKey = factory.generatePrivate(keySpec);// 生成私钥 + // 用私钥对信息进行数字签名 + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initSign(priKey); + signature.update(data); + return encryptBASE64(signature.sign()); + } + + /** + * 获取签名 + */ + public String getSign(Map params) throws Exception { + // 第一步:检查参数是否已经排序 + String[] keys = params.keySet().toArray(new String[0]); + Arrays.sort(keys); + // 第二步:把所有参数名和参数值串在一起 + StringBuilder stringBuilder = new StringBuilder(); + for (String key : keys) { + Object value = params.get(key); + // if (!StringUtils.isEmpty(value)) { + if (!ObjectUtil.isEmpty(value)) { + stringBuilder + .append(key) + .append(JacksonUtils.writeValueAsString(value)); + } + } + //采用私钥签名 + return signByPrivateKey(stringBuilder.toString().getBytes(), privateKey); + } + + /** + * + * BASE64Encoder 加密 + * + * @param data 要加密的数据 + * + * @return 加密后的字符串 + * + */ + private static String encryptBASE64(byte[] data) { + // BASE64Encoder encoder = new BASE64Encoder(); + // String encode = encoder.encode(data); + // return encode; + return new String(Base64.encodeBase64(data)); + } + + private static byte[] decryptBASE64(String data) { + // BASE64Decoder 每76个字符换行 + // BASE64Decoder decoder = new BASE64Decoder(); + // byte[] buffer = decoder.decodeBuffer(data); + // return buffer; + // codec 的 Base64 不换行 + return Base64.decodeBase64(data); + } + + // TODO rsa 2 对比是否更优 + public static boolean verifyByPublicKey(byte[] data, String publicKey, String sign) throws Exception { + byte[] keyBytes = decryptBASE64(publicKey); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + PublicKey pubKey = keyFactory.generatePublic(keySpec); + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initVerify(pubKey); + signature.update(data); + return signature.verify(decryptBASE64(sign)); // 验证签名 + } + + /** + * RSA公钥加密 + * + * @param str 加密字符串 + * @param publicKey 公钥 + * @return 密文 + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws InvalidKeyException + * @throws UnsupportedEncodingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws Exception 加密过程中的异常信息 + */ + public static String encryptByPublicKey(String str, String publicKey) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException { + // base64编码的公钥 + byte[] keyBytes = decryptBASE64(publicKey); + RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance(KEY_ALGORITHM) + .generatePublic(new X509EncodedKeySpec(keyBytes)); + // RSA加密 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, pubKey); + logger.debug(publicKey); + logger.debug("provider: {}", cipher.getProvider().getClass().getName()); + byte[] data = str.getBytes("UTF-8"); + // 加密时超过117字节就报错。为此采用分段加密的办法来加密 + byte[] enBytes = null; + for (int i = 0; i < data.length; i += MAX_ENCRYPT_BLOCK) { + // 注意要使用2的倍数,否则会出现加密后的内容再解密时为乱码 + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_ENCRYPT_BLOCK)); + enBytes = ArrayUtils.addAll(enBytes, doFinal); + } + logger.debug(enBytes.length + ""); + String outStr = encryptBASE64(enBytes); + return outStr; + } + + /** + * RSA私钥加密 + * + * @param str 加密字符串 + * @param privateKey 公钥 + * @return 密文 + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws InvalidKeyException + * @throws UnsupportedEncodingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws Exception 加密过程中的异常信息 + */ + public static String encryptByPrivateKey(String str, String privateKey) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException { + // base64编码的公钥 + byte[] keyBytes = decryptBASE64(privateKey); + RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(KEY_ALGORITHM) + .generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + // RSA加密 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, priKey); + + byte[] data = str.getBytes("UTF-8"); + // 加密时超过117字节就报错。为此采用分段加密的办法来加密 + byte[] enBytes = null; + for (int i = 0; i < data.length; i += MAX_ENCRYPT_BLOCK) { + // 注意要使用2的倍数,否则会出现加密后的内容再解密时为乱码 + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_ENCRYPT_BLOCK)); + enBytes = ArrayUtils.addAll(enBytes, doFinal); + } + String outStr = encryptBASE64(enBytes); + return outStr; + } + + /** + * 读取公钥 + * + * @param publicKeyPath + * @return + */ + public static PublicKey readPublic(String publicKeyPath) { + if (publicKeyPath != null) { + try (FileInputStream bais = new FileInputStream(publicKeyPath)) { + CertificateFactory certificatefactory = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) certificatefactory.generateCertificate(bais); + return cert.getPublicKey(); + } catch (CertificateException e) { + logger.error(e.getMessage(), e); + } catch (FileNotFoundException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + return null; + } + + /** + * 读取私钥 + * + * @param + * @return + */ + public static PrivateKey readPrivate(String privateKeyPath, String privateKeyPwd) { + if (privateKeyPath == null || privateKeyPwd == null) { + return null; + } + try (InputStream stream = new FileInputStream(new File(privateKeyPath));) { + // 获取JKS 服务器私有证书的私钥,取得标准的JKS的 KeyStore实例 + KeyStore store = KeyStore.getInstance("JKS");// JKS,二进制格式,同时包含证书和私钥,一般有密码保护;PKCS12,二进制格式,同时包含证书和私钥,一般有密码保护。 + // jks文件密码,根据实际情况修改 + store.load(stream, privateKeyPwd.toCharArray()); + // 获取jks证书别名 + Enumeration en = store.aliases(); + String pName = null; + while (en.hasMoreElements()) { + String n = (String) en.nextElement(); + if (store.isKeyEntry(n)) { + pName = n; + } + } + // 获取证书的私钥 + PrivateKey key = (PrivateKey) store.getKey(pName, privateKeyPwd.toCharArray()); + return key; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return null; + } + + /** + * RSA私钥解密 + * + * @param encryStr 加密字符串 + * @param privateKey 私钥 + * @return 铭文 + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws InvalidKeyException + * @throws Exception 解密过程中的异常信息 + */ + public static String decryptByPrivateKey(String encryStr, String privateKey) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, + BadPaddingException, InvalidKeyException, UnsupportedEncodingException { + // base64编码的私钥 + byte[] decoded = decryptBASE64(privateKey); + RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(KEY_ALGORITHM) + .generatePrivate(new PKCS8EncodedKeySpec(decoded)); + // RSA解密 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, priKey); + logger.debug(privateKey); + logger.debug("provider: {}" , cipher.getProvider().getClass().getName()); + // 64位解码加密后的字符串 + byte[] data = decryptBASE64(encryStr); + logger.debug(data.length + ""); + // 解密时超过128字节报错。为此采用分段解密的办法来解密 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < data.length; i += MAX_DECRYPT_BLOCK) { + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_DECRYPT_BLOCK)); + sb.append(new String(doFinal)); + } + return sb.toString(); + } + + /** + * RSA公钥解密 + * + * @param encryStr 加密字符串 + * @param + * @return 铭文 + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws InvalidKeyException + * @throws Exception 解密过程中的异常信息 + */ + public static String decryptByPublicKey(String encryStr, String publicKey) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, + BadPaddingException, InvalidKeyException { + // base64编码的私钥 + byte[] decoded = decryptBASE64(publicKey); + RSAPublicKey priKey = (RSAPublicKey) KeyFactory.getInstance(KEY_ALGORITHM) + .generatePublic(new X509EncodedKeySpec(decoded)); + // RSA解密 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, priKey); + + // 64位解码加密后的字符串 + byte[] data = decryptBASE64(encryStr); + // 解密时超过128字节报错。为此采用分段解密的办法来解密 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < data.length; i += MAX_DECRYPT_BLOCK) { + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_DECRYPT_BLOCK)); + sb.append(new String(doFinal)); + } + + return sb.toString(); + } + + /** + * 加密 + * @param key + * @param data + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws NoSuchPaddingException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws InvalidKeyException + * @throws IOException + */ + public static String testEncrypt(String key,String data) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException{ + byte[] decode = Base64.decodeBase64(key); + PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(decode); + KeyFactory kf = KeyFactory.getInstance(KEY_ALGORITHM); + PrivateKey generatePrivate = kf.generatePrivate(pkcs8EncodedKeySpec); + Cipher ci = Cipher.getInstance(KEY_ALGORITHM); + ci.init(Cipher.ENCRYPT_MODE, generatePrivate); + + byte[] bytes = data.getBytes(); + int inputLen = bytes.length; + int offLen = 0;//偏移量 + int i = 0; + ByteArrayOutputStream bops = new ByteArrayOutputStream(); + while(inputLen - offLen > 0){ + byte [] cache; + if(inputLen - offLen > 117){ + cache = ci.doFinal(bytes, offLen,117); + }else{ + cache = ci.doFinal(bytes, offLen,inputLen - offLen); + } + bops.write(cache); + i++; + offLen = 117 * i; + } + bops.close(); + byte[] encryptedData = bops.toByteArray(); + String encodeToString = Base64.encodeBase64String(encryptedData); + return encodeToString; + } + + + + + /** + * 解密 + * @param key + * @param data + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeyException + * @throws NoSuchPaddingException + * @throws InvalidKeySpecException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws IOException + */ + public static String testDecrypt(String key,String data) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, InvalidKeySpecException, IllegalBlockSizeException, BadPaddingException, IOException{ + byte[] decode = Base64.decodeBase64(key); + // PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(decode); //java底层 RSA公钥只支持X509EncodedKeySpec这种格式 + X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(decode); + KeyFactory kf = KeyFactory.getInstance(KEY_ALGORITHM); + PublicKey generatePublic = kf.generatePublic(x509EncodedKeySpec); + Cipher ci = Cipher.getInstance(KEY_ALGORITHM); + ci.init(Cipher.DECRYPT_MODE,generatePublic); + + int inputLen = data.getBytes().length; + byte[] bytes = data.getBytes(); + int offLen = 0; + int i = 0; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + while(inputLen - offLen > 0){ + byte[] cache; + if(inputLen - offLen > 128){ + cache = ci.doFinal(bytes,offLen,128); + }else{ + cache = ci.doFinal(bytes,offLen,inputLen - offLen); + } + byteArrayOutputStream.write(cache); + i++; + offLen = 128 * i; + + } + byteArrayOutputStream.close(); + byte[] byteArray = byteArrayOutputStream.toByteArray(); + return new String(byteArray); + } +} \ No newline at end of file diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/Status.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/Status.java new file mode 100644 index 0000000..82e5972 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/Status.java @@ -0,0 +1,79 @@ +package com.ruoyi.thirdparty.yyyouping.utils.common; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + + +public enum Status { + JACKSON_EXCEPTION(5104, "Jackson异常"), + UNKNOWN(100, "Code异常"); + + private final Integer code; + private final String msg; + private static final int SUCCESS = 2000; + public static final int SUCCESS_ZERO = 0; + private static final int WEIGHT = 80000; + private static final int DONET_WEIGHT = 100000; + private static final String DONET_STATUS_PREFIX = "DONET_"; + private static MessageSource messageSource; + + private Status(Integer code, String msg) { + this.code = code; + this.msg = msg; + } + + public String getMsg() { + String msg = null; + + try { + msg = messageSource.getMessage("status." + this.code, (Object[])null, LocaleContextHolder.getLocale()); + } catch (Exception var3) { + msg = this.msg; + } + + return msg; + } + + public Integer getCode() { + if (this.code == 2000) { + return 0; + } else { + return this.name().startsWith("DONET_") ? Math.subtractExact(this.code, 100000) : Math.addExact(this.code, 80000); + } + } + + public Integer getOriginCode() { + return this.code; + } + + public static Status getByCode(Integer code) { + if (null == code) { + return UNKNOWN; + } else { + Status[] var1 = values(); + int var2 = var1.length; + + for(int var3 = 0; var3 < var2; ++var3) { + Status status = var1[var3]; + if (status.getCode().equals(code)) { + return status; + } + } + + return UNKNOWN; + } + } + + @Component + public static class Builder { + public Builder() { + } + + @Autowired + public void setMessageResource(MessageSource messageSource) { + Status.messageSource = messageSource; + } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/YYResult.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/YYResult.java new file mode 100644 index 0000000..f069d03 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/yyyouping/utils/common/YYResult.java @@ -0,0 +1,26 @@ +package com.ruoyi.thirdparty.yyyouping.utils.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class YYResult { + + private Integer code; + private String msg; + private Long timestamp; + private Object data; + + public boolean isSuccess(){ + if (this.code.equals(0)){ + return true; + } + return false; + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/config/ZBTProperties.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/config/ZBTProperties.java new file mode 100644 index 0000000..5e6969a --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/config/ZBTProperties.java @@ -0,0 +1,43 @@ +package com.ruoyi.thirdparty.zbt.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "zbt") +public class ZBTProperties { + private String appKey; + private String language; + private String appId; + private String baseUrl; + private String callbackUrl; + private String userBalance = "/open/user/v1/t-coin/balance"; + private String userCreate = "/open/user/steam-check/create"; + private String userSteamInfo = "/open/user/steam-info"; + private String developmentSave = "/open/development/callback-url/save"; + private String developmentInfo = "/open/development/info"; + private String userSteamAccounts = "/open/user/steam-accounts"; + private String productSearch = "/open/product/v2/search"; + private String productInfo = "/open/product/price/info"; + private String productFilters = "/open/product/v1/filters"; + private String productList = "/open/product/v1/sell/list"; + private String product = "/open/product/v1/sell/product"; + private String productV1Info = "/open/product/v1/info"; + private String tradeBuy = "/open/trade/v2/buy"; + private String tradeQuickBuy = "/open/trade/v2/quick-buy"; + private String orderBuyerList = "/open/order/buyer/list"; + private String orderSellerList = "/open/order/seller/list"; + private String orderBuyDetail = "/open/order/v2/buy/detail"; + private String orderSellerDetail = "/open/order/v2/seller/detail"; + private String orderBuyerCancel = "/open/order/buyer-cancel"; + private String orderSellerCancel = "/open/order/seller-cancel"; + private String offerStatus = "/open/offer/v1/status"; + private String offerSendAssets = "/open/offer/v1/send-assets"; + private String v1Inventory = "/open/v1/inventory/{steamId}/{appId}/"; + private String v1Bind = "/open/v1/seller/steam/bind/"; + private String v1Items = "/open/v1/sale/items"; + private String v1Deliver = "/open/v1/sale/deliver"; + private String v1SellList = "/open/v1/sell-list"; + private String v1ModifyPrice = "/open/v1/modify-price"; + private String v1OffSale = "/open/v1/off-sale"; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/controller/ZBTController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/controller/ZBTController.java new file mode 100644 index 0000000..6c48e68 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/controller/ZBTController.java @@ -0,0 +1,49 @@ +package com.ruoyi.thirdparty.zbt.controller; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.thirdparty.zbt.config.ZBTProperties; +import com.ruoyi.thirdparty.zbt.result.ResultZbt; +import com.ruoyi.thirdparty.zbt.result.user.BusinessData; +import com.ruoyi.thirdparty.zbt.result.user.UserAccountModel; +import com.ruoyi.thirdparty.zbt.service.ZBTService; +import com.ruoyi.thirdparty.zbt.util.ZBTUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Api(tags = "扎比特") +@RestController +@RequestMapping +public class ZBTController { + + private final ZBTService zbtService; + + public ZBTController(ZBTService zbtService) { + this.zbtService = zbtService; + } + + @ApiOperation("管理端查询用户余额") + @GetMapping("/admin/zbt/balance") + public R balance() { + ResultZbt balance = zbtService.balance(); + if (balance.getSuccess()) return R.ok(balance.getData()); + return R.fail("用户余额查询失败"); + } + + @ApiOperation("管理端获取开发者信息") + @GetMapping("/admin/zbt/developmentInfo") + public R developmentInfo() { + ResultZbt developmentInfo = zbtService.developmentInfo(); + if (developmentInfo.getSuccess()) return R.ok(developmentInfo.getData()); + return R.fail("获取用户开发者账号相关信息失败"); + } + + // @PostMapping("/admin/zbt/callback") + public String callback(@RequestBody Map payload) { + return zbtService.callback(payload); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/NormalBuyParamV2DTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/NormalBuyParamV2DTO.java new file mode 100644 index 0000000..9878de0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/NormalBuyParamV2DTO.java @@ -0,0 +1,17 @@ +package com.ruoyi.thirdparty.zbt.param; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class NormalBuyParamV2DTO { + + private BigDecimal buyPrice; + private BigDecimal maxPrice; + private String outTradeNo; + + // 平台的物品在售记录id + private String productId; + private String tradeUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/OrderBuyDetailParams.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/OrderBuyDetailParams.java new file mode 100644 index 0000000..a607477 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/OrderBuyDetailParams.java @@ -0,0 +1,12 @@ +package com.ruoyi.thirdparty.zbt.param; + +import lombok.Data; + +@Data +public class OrderBuyDetailParams { + + private String appKey; + private String language; + private String orderId; + private String outTradeNo; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/ProductListParams.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/ProductListParams.java new file mode 100644 index 0000000..8626a83 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/ProductListParams.java @@ -0,0 +1,42 @@ +package com.ruoyi.thirdparty.zbt.param; + +import com.ruoyi.common.annotation.Excel; +import lombok.Builder; +import lombok.Data; +// zbt平台接口查询参数 +@Data +@Builder +public class ProductListParams { + + // // 第三方平台 + // private Integer partyType; + // + // // 饰品在该平台上的id + // private String id; + + private String itemId; + private String appKey; + private String language; + private String containSticker; + private String delivery; + private String endWear; + private String gemEnName; + private String gemId; + private String gemIds; + private String levelIds; + private String levelName; + private String limit; + private String orderBy; + private String page; + private String slotStickerIds; + private String sort; + private String startWear; + private String stickerCapsule; + private String stickerIds; + private String styleId; + private String hasFraudwarning; + private String standardPrice; + private String withHistory; + @Excel(name = "饰品唯一名称英文") + private String marketHashName; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/QuickBuyParamV2DTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/QuickBuyParamV2DTO.java new file mode 100644 index 0000000..83a9495 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/QuickBuyParamV2DTO.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.zbt.param; + +import lombok.Data; + +@Data +public class QuickBuyParamV2DTO { + + private String appId; + private String delivery; + private String itemId; + private String lowPrice; + private String marketHashName; + private String maxPrice; + private String outTradeNo; + private String tradeUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/SaveCallbackUrlDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/SaveCallbackUrlDTO.java new file mode 100644 index 0000000..d912853 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/SaveCallbackUrlDTO.java @@ -0,0 +1,8 @@ +package com.ruoyi.thirdparty.zbt.param; + +import lombok.Data; + +@Data +public class SaveCallbackUrlDTO { + private String callbackUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/SearchParams.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/SearchParams.java new file mode 100644 index 0000000..9ef561a --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/SearchParams.java @@ -0,0 +1,34 @@ +package com.ruoyi.thirdparty.zbt.param; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchParams { + private String appId; + private String appKey; + private String category; + private String exterior; + private String hero; + private List itemIds; + private String itemSet; + private String keyword; + private String language; + private String limit; + private String maxPrice; + private String minPrice; + private String orderBy; + private String page; + private String quality; + private String rarity; + private String slot; + private String type; + private String weapon; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/UserSteamInfoParams.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/UserSteamInfoParams.java new file mode 100644 index 0000000..23ef36b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/param/UserSteamInfoParams.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.zbt.param; + +import lombok.Data; + +@Data +public class UserSteamInfoParams { + + private String appId; + private String type; + private String appKey; + private String language; + private String steamId; + private String tradeUrl; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/ResultZbt.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/ResultZbt.java new file mode 100644 index 0000000..36b0ad5 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/ResultZbt.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.zbt.result; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ResultZbt { + private T data; + private Integer errorCode; + private Object errorData; + private String errorMsg; + private Boolean success; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/buy/OpenBuyResultDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/buy/OpenBuyResultDTO.java new file mode 100644 index 0000000..c0a1405 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/buy/OpenBuyResultDTO.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.zbt.result.buy; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class OpenBuyResultDTO { + + private BigDecimal buyPrice; + private Integer delivery; + private String offerId; + private String orderId; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/order/OfferInfoDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/order/OfferInfoDTO.java new file mode 100644 index 0000000..c1527ed --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/order/OfferInfoDTO.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.zbt.result.order; + +import lombok.Data; + +@Data +public class OfferInfoDTO { + + private String appId; + private String imageUrl; + private String itemId; + private String marketHashName; + private String name; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/order/OpenItemInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/order/OpenItemInfo.java new file mode 100644 index 0000000..3aaa286 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/order/OpenItemInfo.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.zbt.result.order; + +import lombok.Data; + +@Data +public class OpenItemInfo { + + private String tradeOfferId; + private String transferId; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/order/OrderBuyDetailDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/order/OrderBuyDetailDTO.java new file mode 100644 index 0000000..47c324f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/order/OrderBuyDetailDTO.java @@ -0,0 +1,27 @@ +package com.ruoyi.thirdparty.zbt.result.order; + +import com.ruoyi.domain.common.constant.DeliveryOrderStatus; +import com.ruoyi.thirdparty.zbt.result.product.AssetInfo; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class OrderBuyDetailDTO { + + private AssetInfo assetInfo; + private String createTime; + private String deliverType; + private String failedDesc; + private OfferInfoDTO offerInfoDTO; + private OpenItemInfo openItemInfo; + private String orderId; + private BigDecimal price; + private String productId; + private String receiveSteamId; + + //订单状态 + private DeliveryOrderStatus status; + private String statusName; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/AssetInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/AssetInfo.java new file mode 100644 index 0000000..f3f3c6f --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/AssetInfo.java @@ -0,0 +1,33 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +import java.util.List; + +@Data +public class AssetInfo { + + private String assetId; + private String classId; + private String classInfoId; + private String ext; + private String fadeColor; + private String fraudwarning; + private List gems; + private String gradient; + private String inspectImageUrl; + + private String instanceId; + + private String lastStyle; + private String levelColor; + private String levelName; + private Integer paintIndex; + private Integer paintSeed; + private List stickers; + private String styleId; + private String styleProgress; + private List styles; + private String wear; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/AvailableMarket.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/AvailableMarket.java new file mode 100644 index 0000000..0233e25 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/AvailableMarket.java @@ -0,0 +1,40 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class AvailableMarket { + + private Integer acceptBargain; + private Integer appId; + private String appName; + private AssetInfo assetInfo; + private String classInfoId; + private BigDecimal cnyPrice; + private Integer compensateType; + private Integer delivery; + private String description; + private String id; + private String imageUrl; + private String inspect3dUrl; + private Integer inspect3dViewable; + private String inspectImageUrl; + private String inspectUrl; + private Integer inspectViewable; + private Integer inspectable; + + private Integer isCollection; + + private String itemId; + private ItemInfo itemInfo; + private String itemName; + private String marketHashName; + private BigDecimal price; + private SellerUserInfoVO sellerInfo; + private BigDecimal sellerPrice; + private Integer systemTime; + + private String token; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/AvailableMarketList.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/AvailableMarketList.java new file mode 100644 index 0000000..552617d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/AvailableMarketList.java @@ -0,0 +1,17 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +import java.util.List; + +@Data +public class AvailableMarketList { + + private Integer limit; + private List list; + private String offsetToken; + private Integer page; + private Integer pages; + private String scrollId; + private Long total; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/DeliveryStatsInfoDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/DeliveryStatsInfoDTO.java new file mode 100644 index 0000000..c418872 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/DeliveryStatsInfoDTO.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +@Data +public class DeliveryStatsInfoDTO { + + private DeliveryStatsItem day15; + private DeliveryStatsItem day7; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/DeliveryStatsItem.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/DeliveryStatsItem.java new file mode 100644 index 0000000..a88a99a --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/DeliveryStatsItem.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +@Data +public class DeliveryStatsItem { + private String deliveryAvgTime; + private Integer deliveryNoneNum; + private String deliverySuccessRate; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/FilterKeyList.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/FilterKeyList.java new file mode 100644 index 0000000..fde20b6 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/FilterKeyList.java @@ -0,0 +1,19 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FilterKeyList { + + private String alias; + private String color; + private String image; + private String key; + private String name; + private String searchKey; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/FilterList.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/FilterList.java new file mode 100644 index 0000000..966ef0e --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/FilterList.java @@ -0,0 +1,20 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FilterList { + + private String alias; + private String key; + private List list; + private String name; + private String searchKey; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/Gems.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/Gems.java new file mode 100644 index 0000000..230607e --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/Gems.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +@Data +public class Gems { + + private String border; + private String gemEnType; + private String gemType; + private String id; + private String image; + private String name; + private String type; + private String value; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ItemAssetClassInfoStyleDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ItemAssetClassInfoStyleDTO.java new file mode 100644 index 0000000..f218136 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ItemAssetClassInfoStyleDTO.java @@ -0,0 +1,13 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +@Data +public class ItemAssetClassInfoStyleDTO { + + private String color; + private String locked; + private String styleEnName; + private String styleId; + private String styleName; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ItemAssetStickerDTO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ItemAssetStickerDTO.java new file mode 100644 index 0000000..a210765 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ItemAssetStickerDTO.java @@ -0,0 +1,19 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ItemAssetStickerDTO { + + private String id; + private String image; + private String itemId; + private String name; + private BigDecimal price; + private String slot; + private String stickerId; + private String type; + private String wear; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ItemInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ItemInfo.java new file mode 100644 index 0000000..7d6dbab --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ItemInfo.java @@ -0,0 +1,31 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +@Data +public class ItemInfo { + + private String category; + private String categoryName; + private String exterior; + private String exteriorColor; + private String exteriorName; + private String hero; + private String heroAvatar; + private String heroName; + private String itemSet; + private String itemSetName; + private String quality; + private String qualityColor; + private String qualityName; + private String rarity; + private String rarityColor; + private String rarityName; + private String slot; + private String slotName; + private String type; + private String typeName; + private String weapon; + private String weaponName; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/PriceInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/PriceInfo.java new file mode 100644 index 0000000..a6d8ffa --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/PriceInfo.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class PriceInfo { + + private BigDecimal autoDeliverPrice; + private Integer autoDeliverQuantity; + private BigDecimal manualDeliverPrice; + private Integer manualQuantity; + private BigDecimal price; + private Integer quantity; + private Integer userId; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ProductFilters.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ProductFilters.java new file mode 100644 index 0000000..6d74928 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/ProductFilters.java @@ -0,0 +1,20 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductFilters { + + private Integer appId; + private String appName; + private String icon; + private String iconLarge; + private List list; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/SearchData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/SearchData.java new file mode 100644 index 0000000..6c9177a --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/SearchData.java @@ -0,0 +1,18 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import com.ruoyi.domain.dto.zbt.OrnamentZBT; +import lombok.Data; + +import java.util.Set; + +@Data +public class SearchData { + private Integer limit; + private Set list; + private String offsetToken; + private Integer page; + private Integer pages; + private String scrollId; + private Integer total; +} + diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/SearchList.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/SearchList.java new file mode 100644 index 0000000..7bcc6c7 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/SearchList.java @@ -0,0 +1,26 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +import java.util.List; + +@Data +public class SearchList { + private Integer appId; + private String exterior; + private String exteriorName; + private String imageUrl; + private String itemId; + private String itemName; + private String marketHashName; + private List priceInfo; + private String quality; + private String qualityColor; + private String qualityName; + private String rarity; + private String rarityColor; + private String rarityName; + private String shortName; + private String type; + private String typeName; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/SellerUserInfoVO.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/SellerUserInfoVO.java new file mode 100644 index 0000000..66713b6 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/product/SellerUserInfoVO.java @@ -0,0 +1,16 @@ +package com.ruoyi.thirdparty.zbt.result.product; + +import lombok.Data; + +@Data +public class SellerUserInfoVO { + + private String avatar; + private DeliveryStatsInfoDTO deliveryInfo; + private String lastActive; + private String nickname; + private Integer platformId; + private String thirdUserId; + private String userId; + private String verified; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/BusinessData.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/BusinessData.java new file mode 100644 index 0000000..f9aa01b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/BusinessData.java @@ -0,0 +1,27 @@ +package com.ruoyi.thirdparty.zbt.result.user; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class BusinessData { + + private String appKey; + private String appSecret; + private String applyNote; + private String area; + private BigDecimal balance; + private BigDecimal balanceLack; + private String callbackUrl; + private String email; + private String grantIpList; + private String rejectReason; + private String remark; + private String status; + private Integer successRate24; + private String telephone; + private Long total24Access; + private Long totalAccess; + private String weixin; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/CheckStatusInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/CheckStatusInfo.java new file mode 100644 index 0000000..3375880 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/CheckStatusInfo.java @@ -0,0 +1,10 @@ +package com.ruoyi.thirdparty.zbt.result.user; + +import lombok.Data; + +@Data +public class CheckStatusInfo { + + private String statusKey; + private Object statusValue; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/GenericCheckSteamOutPut.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/GenericCheckSteamOutPut.java new file mode 100644 index 0000000..6107072 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/GenericCheckSteamOutPut.java @@ -0,0 +1,14 @@ +package com.ruoyi.thirdparty.zbt.result.user; + +import lombok.Data; + +import java.util.List; + +@Data +public class GenericCheckSteamOutPut { + + private Integer appId; + private Integer checkStatus; + private List statusList; + private UserSteamInfo steamInfo; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/UserAccountModel.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/UserAccountModel.java new file mode 100644 index 0000000..a393cc3 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/UserAccountModel.java @@ -0,0 +1,11 @@ +package com.ruoyi.thirdparty.zbt.result.user; + +import lombok.Data; + +@Data +public class UserAccountModel { + + private String data; + private String name; + private String userId; +} \ No newline at end of file diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/UserSteamInfo.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/UserSteamInfo.java new file mode 100644 index 0000000..5a90f28 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/result/user/UserSteamInfo.java @@ -0,0 +1,12 @@ +package com.ruoyi.thirdparty.zbt.result.user; + +import lombok.Data; + +@Data +public class UserSteamInfo { + + private String avatar; + private String nickName; + private String steamId; + private String tradeToken; +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/service/ZBTService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/service/ZBTService.java new file mode 100644 index 0000000..ca64108 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/service/ZBTService.java @@ -0,0 +1,39 @@ +package com.ruoyi.thirdparty.zbt.service; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.thirdparty.zbt.param.*; +import com.ruoyi.thirdparty.zbt.result.ResultZbt; +import com.ruoyi.thirdparty.zbt.result.buy.OpenBuyResultDTO; +import com.ruoyi.thirdparty.zbt.result.order.OrderBuyDetailDTO; +import com.ruoyi.thirdparty.zbt.result.product.AvailableMarketList; +import com.ruoyi.thirdparty.zbt.result.product.ProductFilters; +import com.ruoyi.thirdparty.zbt.result.product.SearchData; +import com.ruoyi.thirdparty.zbt.result.user.BusinessData; +import com.ruoyi.thirdparty.zbt.result.user.GenericCheckSteamOutPut; +import com.ruoyi.thirdparty.zbt.result.user.UserAccountModel; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.Map; + +public interface ZBTService { + + ResultZbt balance(); + + ResultZbt userSteamInfo(UserSteamInfoParams userSteamInfoParams); + + ResultZbt developmentInfo(); + + ResultZbt search(SearchParams params); + + ResultZbt productFilters(); + + ResultZbt productList(ProductListParams productListParams); + + ResultZbt tradeBuy(NormalBuyParamV2DTO normalBuyParamV2DTO); + + ResultZbt tradeQuickBuy(QuickBuyParamV2DTO quickBuyParamV2DTO); + + ResultZbt orderBuyDetail(OrderBuyDetailParams orderBuyDetailParams); + + String callback(Map payload); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/service/impl/ZBTServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/service/impl/ZBTServiceImpl.java new file mode 100644 index 0000000..e3833fa --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/service/impl/ZBTServiceImpl.java @@ -0,0 +1,244 @@ +package com.ruoyi.thirdparty.zbt.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.mapper.TtDeliveryRecordMapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpUtils; +import com.ruoyi.domain.common.constant.DeliveryOrderStatus; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.thirdparty.zbt.config.ZBTProperties; +import com.ruoyi.thirdparty.zbt.param.*; +import com.ruoyi.thirdparty.zbt.result.ResultZbt; +import com.ruoyi.thirdparty.zbt.result.buy.OpenBuyResultDTO; +import com.ruoyi.thirdparty.zbt.result.order.OrderBuyDetailDTO; +import com.ruoyi.thirdparty.zbt.result.product.AvailableMarketList; +import com.ruoyi.thirdparty.zbt.result.product.ProductFilters; +import com.ruoyi.thirdparty.zbt.result.product.SearchData; +import com.ruoyi.thirdparty.zbt.result.user.BusinessData; +import com.ruoyi.thirdparty.zbt.result.user.GenericCheckSteamOutPut; +import com.ruoyi.thirdparty.zbt.result.user.UserAccountModel; +import com.ruoyi.thirdparty.zbt.service.ZBTService; +import com.ruoyi.thirdparty.zbt.util.ZBTUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Service +@Slf4j +@EnableConfigurationProperties(value = ZBTProperties.class) +public class ZBTServiceImpl implements ZBTService { + + private final ZBTProperties zbtProperties; + + public ZBTServiceImpl(ZBTProperties zbtProperties) { + this.zbtProperties = zbtProperties; + } + + @Autowired + private TtDeliveryRecordMapper deliveryRecordMapper; + + @Autowired + private TtBoxRecordsMapper boxRecordsMapper; + + @Override + public ResultZbt balance() { + String result = getMethod(zbtProperties.getUserBalance(), null); + if (StringUtils.isBlank(result)) return null; + return JSONObject.parseObject(result, new TypeReference>() { + }); + } + + @Override + public ResultZbt userSteamInfo(UserSteamInfoParams params) { + try { + params.setTradeUrl(URLEncoder.encode(params.getTradeUrl(), "UTF-8")); + } catch (UnsupportedEncodingException e) { + return null; + } + Map paramMap = ZBTUtils.objectToMap(params); + String result = getMethod(zbtProperties.getUserSteamInfo(), paramMap); + if (StringUtils.isBlank(result)) return null; + return JSONObject.parseObject(result, new TypeReference>() { + }); + } + + @Override + public ResultZbt developmentInfo() { + String result = getMethod(zbtProperties.getDevelopmentInfo(), null); + if (StringUtils.isBlank(result)) return null; + return JSONObject.parseObject(result, new TypeReference>() { + }); + } + + @Override + public ResultZbt search(SearchParams params) { + Map paramMap = ZBTUtils.objectToMap(params); + String result = getMethod(zbtProperties.getProductSearch(), paramMap); + if (StringUtils.isBlank(result)) return null; + return JSONObject.parseObject(result, new TypeReference>() { + }); + } + + @Override + public ResultZbt productFilters() { + String result = getMethod(zbtProperties.getProductFilters(), null); + if (StringUtils.isBlank(result)) return null; + return JSONObject.parseObject(result, new TypeReference>() { + }); + } + + @Override + public ResultZbt productList(ProductListParams params) { + Map paramMap = ZBTUtils.objectToMap(params); + String result = getMethod(zbtProperties.getProductList(), paramMap); + if (StringUtils.isBlank(result)) { + return null; + } + return JSONObject.parseObject(result, new TypeReference>() { + }); + } + + @Override + public ResultZbt tradeBuy(NormalBuyParamV2DTO normalBuyParamV2DTO) { + // 设置回调地址 + SaveCallbackUrlDTO saveCallbackUrlDTO = new SaveCallbackUrlDTO(); + saveCallbackUrlDTO.setCallbackUrl(zbtProperties.getCallbackUrl()); + postMethod(zbtProperties.getDevelopmentSave(), JSON.toJSONString(saveCallbackUrlDTO)); + + String result = postMethod(zbtProperties.getTradeBuy(), JSON.toJSONString(normalBuyParamV2DTO)); + System.err.println(result); + if (StringUtils.isBlank(result)) { + return null; + } + return JSONObject.parseObject(result, new TypeReference>() { + }); + } + + @Override + public ResultZbt tradeQuickBuy(QuickBuyParamV2DTO quickBuyParamV2DTO) { + String result = postMethod(zbtProperties.getTradeQuickBuy(), JSON.toJSONString(quickBuyParamV2DTO)); + if (StringUtils.isBlank(result)) return null; + return JSONObject.parseObject(result, new TypeReference>() { + }); + } + + @Override + public ResultZbt orderBuyDetail(OrderBuyDetailParams params) { + Map paramMap = ZBTUtils.objectToMap(params); + String result = getMethod(zbtProperties.getOrderBuyDetail(), paramMap); + if (StringUtils.isBlank(result)) return null; + return JSONObject.parseObject(result, new TypeReference>() { + }); + } + + @Override + public String callback(Map payload) { + ResultZbt businessDataResultZbt = developmentInfo(); + String appSecret = businessDataResultZbt.getData().getAppSecret(); + + if (!ZBTUtils.verifySign(payload, appSecret)) { + log.error("扎比特回调通知验签未通过"); + return ""; + } + + Integer status = (Integer) payload.get("status"); + String outTradeNo = (String) payload.get("outTradeNo"); + // 获取提货订单数据 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TtDeliveryRecord::getOutTradeNo, outTradeNo); + TtDeliveryRecord ttDeliveryRecord = deliveryRecordMapper.selectOne(queryWrapper); + if (status == 1) { + ttDeliveryRecord.setStatus(DeliveryOrderStatus.DELIVERY_BEFORE.getCode()); + ttDeliveryRecord.setMessage(DeliveryOrderStatus.DELIVERY_BEFORE.getMsg()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + deliveryRecordMapper.updateById(ttDeliveryRecord); + } else if (status == 3) { + ttDeliveryRecord.setStatus(DeliveryOrderStatus.DELIVERY_AFTER.getCode()); + ttDeliveryRecord.setMessage(DeliveryOrderStatus.DELIVERY_AFTER.getMsg()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + deliveryRecordMapper.updateById(ttDeliveryRecord); + } else if (status == 10) { + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_COMPLETE.getCode()); + ttDeliveryRecord.setMessage(DeliveryOrderStatus.ORDER_COMPLETE.getMsg()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + deliveryRecordMapper.updateById(ttDeliveryRecord); + // 联动:订单完成后将对应 box_records 标为「已经提货」,不再在背包 / 提货背包展示,不可分解 / 转赠 + markBoxAsDelivered(ttDeliveryRecord.getBoxRecordsId()); + return "success"; + } else if (status == 11) { + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_CANCEL.getCode()); + ttDeliveryRecord.setMessage(DeliveryOrderStatus.ORDER_CANCEL.getMsg()); + ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate()); + deliveryRecordMapper.updateById(ttDeliveryRecord); + } + + return ""; + } + + /** + * 将提货记录对应的 box_records 状态从 APPLY_DELIVERY(2) 走到 DELIVERY_YET(3)。 + * 仅在当前状态确为「申请提货」时才写入,避免覆盖其他状态。 + */ + private void markBoxAsDelivered(Long boxRecordsId) { + if (boxRecordsId == null) { + return; + } + TtBoxRecords box = boxRecordsMapper.selectById(boxRecordsId); + if (box != null && TtboxRecordStatus.APPLY_DELIVERY.getCode().equals(box.getStatus())) { + box.setStatus(TtboxRecordStatus.DELIVERY_YET.getCode()); + box.setUpdateTime(DateUtils.getNowDate()); + boxRecordsMapper.updateById(box); + } + } + + private String postMethod(String url, String JSONString) { + Map param = new HashMap<>(); + param.put("app-key", zbtProperties.getAppKey()); + param.put("language", zbtProperties.getLanguage()); + String substring = GeneratedStr(param); + url = zbtProperties.getBaseUrl() + url + "?" + substring; + return HttpUtils.sendPostJSONString(url, JSONString); + } + + private String getMethod(String url, Map param) { + + if (param == null) { + param = new HashMap<>(); + } + param.put("appId", zbtProperties.getAppId()); + param.put("app-key", zbtProperties.getAppKey()); + param.put("language", zbtProperties.getLanguage()); + String substring = GeneratedStr(param); + return HttpUtils.sendGet(zbtProperties.getBaseUrl() + url, substring); + } + + private String GeneratedStr(Map param){ + StringBuilder params = new StringBuilder(); + for (Map.Entry entry : param.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + params.append(key); + params.append("="); + params.append(value); + params.append("&"); + } + params.trimToSize(); + return params.substring(0, params.length() - 1); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/task/OrnamentsPullTask.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/task/OrnamentsPullTask.java new file mode 100644 index 0000000..71afadb --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/task/OrnamentsPullTask.java @@ -0,0 +1,253 @@ +package com.ruoyi.thirdparty.zbt.task; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.domain.dto.zbt.OrnamentZBT; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.zbt.param.SearchParams; +import com.ruoyi.thirdparty.zbt.result.ResultZbt; +import com.ruoyi.thirdparty.zbt.result.product.SearchData; +import com.ruoyi.thirdparty.zbt.result.product.SearchList; +import com.ruoyi.thirdparty.zbt.service.ZBTService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Component("OrnamentsPullTask") +public class OrnamentsPullTask { + + private Set removeDuplicates = new HashSet<>(); + private final ZBTService zbtService; + private final SysDictDataMapper dictDataMapper; + private final ISysConfigService configService; + private final TtOrnamentService ornamentsService; + + public OrnamentsPullTask(ZBTService zbtService, + SysDictDataMapper dictDataMapper, + ISysConfigService configService, + TtOrnamentService ornamentsService) { + this.zbtService = zbtService; + this.dictDataMapper = dictDataMapper; + this.configService = configService; + this.ornamentsService = ornamentsService; + } + + public void getOrnamentsData() { + log.info("拉取最新饰品数据任务执行开始..."); + long startTime = System.currentTimeMillis(); + List itemIdList = ornamentsService.selectOrnamentsMarketHashNameList(); + if (!itemIdList.isEmpty()) removeDuplicates = new HashSet<>(itemIdList); + log.info("数据库总条数:" + removeDuplicates.size()); + List type = dictDataMapper.selectDictDataByType("ornaments_type"); + + if (ObjectUtils.isEmpty(type)) return; + Map typeMap = objectToMap(type); + + Set tempRemoveDuplicates = new HashSet<>(); + List ZBTOrnamentsDataList = new ArrayList<>(); + + for (Map.Entry entry : typeMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + SearchParams searchParams = new SearchParams(); + searchParams.setLimit("200"); + searchParams.setPage("1"); + searchParams.setCategory(key); + ResultZbt searchResult = zbtService.search(searchParams); + if (!searchResult.getSuccess()) return; + List list = searchResult.getData().getList().stream() + .filter(searchList -> !tempRemoveDuplicates.contains(searchList.getMarketHashName())) + .peek(searchList -> tempRemoveDuplicates.add(searchList.getMarketHashName())) + .collect(Collectors.toList()); + ZBTOrnamentsDataList.addAll(list); + List ZBTOrnamentsDataListByType = searchResult.getData().getList().stream() + .filter(searchList -> !removeDuplicates.contains(searchList.getMarketHashName())) + .peek(searchList -> removeDuplicates.add(searchList.getMarketHashName())) + .collect(Collectors.toList()); + Integer total = searchResult.getData().getTotal(); + int pageSize = 200; + int pageNumCount = (total % pageSize == 0) ? total / pageSize : total / pageSize + 1; + for (int i = 2; i <= pageNumCount; i++) { + searchParams.setPage(String.valueOf(i)); + if (i == pageNumCount) searchParams.setLimit(String.valueOf(total % pageSize)); + searchResult = zbtService.search(searchParams); + if (!searchResult.getSuccess()) return; + list = searchResult.getData().getList().stream().filter(searchList -> !tempRemoveDuplicates.contains(searchList.getMarketHashName())) + .peek(searchList -> tempRemoveDuplicates.add(searchList.getMarketHashName())).collect(Collectors.toList()); + ZBTOrnamentsDataList.addAll(list); + List typeList = searchResult.getData().getList().stream().filter(searchList -> !removeDuplicates.contains(searchList.getMarketHashName())) + .peek(searchList -> removeDuplicates.add(searchList.getMarketHashName())).collect(Collectors.toList()); + ZBTOrnamentsDataListByType.addAll(typeList); + } + batchInsertOrnaments(ZBTOrnamentsDataListByType, value); + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // wrapper.eq(TtOrnament::getIsProprietaryProperty, "1"); + Map ornamentsMap = + ornamentsService.list(wrapper).stream().collect(Collectors.toMap(TtOrnament::getMarketHashName, ornaments -> ornaments)); + log.info("扎比特数据条数:" + ZBTOrnamentsDataList.size()); + batchUpdateOrnamentsPrice(ZBTOrnamentsDataList, ornamentsMap); + batchSoldOutOrnaments(ZBTOrnamentsDataList, ornamentsMap); + // 获取方法结束时间 + long endTime = System.currentTimeMillis(); + // 计算方法执行时间 + long duration = endTime - startTime; + log.info("方法执行时间(秒):" + duration / 1000.0); + log.info("拉取最新饰品数据任务执行结束..."); + } + + private void batchSoldOutOrnaments(List ZBTOrnamentsDataList, Map ornamentsMap) { + List ornamentsList = ornamentsMap.entrySet().stream() + .filter(entry -> ZBTOrnamentsDataList.stream().noneMatch(searchList -> searchList.getMarketHashName().equals(entry.getKey()))) + .map(Map.Entry::getValue) + .collect(Collectors.toList()); + ornamentsList = ornamentsList.stream().peek(ttOrnaments -> { + ttOrnaments.setUsePrice(BigDecimal.ZERO); + ttOrnaments.setPrice(BigDecimal.ZERO); + ttOrnaments.setUpdateTime(DateUtils.getNowDate()); + ttOrnaments.setRemark("ZBT市场已下架"); + ttOrnaments.setIsPutaway("1"); + }).collect(Collectors.toList()); + log.info("下架条数:" + ornamentsList.size()); + boolean isSuccess = ornamentsService.updateBatchById(ornamentsList, 1); + if (isSuccess) { + log.info("批量下架饰品成功!"); + } else { + log.info("批量下架饰品失败!"); + } + } + + private void batchUpdateOrnamentsPrice(List list, Map ornamentsMap) { + String usePricePremiumRateStr = configService.selectConfigByKey("usePricePremiumRate"); + BigDecimal usePricePremiumRate = new BigDecimal(usePricePremiumRateStr); + String ZBTParitiesStr = configService.selectConfigByKey("ZBTParities"); + BigDecimal ZBTParities = new BigDecimal(ZBTParitiesStr); + List updateList = list.stream().distinct().filter(ornamentZBT -> { + OrnamentZBT.PriceInfo priceInfo = ornamentZBT.getPriceInfo(); + TtOrnament ornament = ornamentsMap.get(ornamentZBT.getMarketHashName()); + BigDecimal priceData; + if (!Objects.isNull(ornament)) { + priceData = ornament.getPrice(); + } else { + priceData = BigDecimal.ZERO; + } + BigDecimal price; + if (!Objects.isNull(priceInfo)) { + price = priceInfo.getPrice(); + } else { + price = BigDecimal.ZERO; + } + Integer quantityData; + if (!Objects.isNull(ornament)) { + quantityData = ornament.getQuantity(); + } else { + quantityData = 0; + } + Integer quantity; + if (!Objects.isNull(priceInfo)) { + quantity = priceInfo.getQuantity(); + } else { + quantity = 0; + } + return (priceData == null || quantityData == null) || + (priceInfo == null) || + (ornament == null) || + (priceData.compareTo(price) != 0 || !Objects.equals(quantityData, quantity)); + }).collect(Collectors.toList()); + List ttOrnamentList = new ArrayList<>(); + updateList.forEach(searchList -> { + String marketHashName = searchList.getMarketHashName(); + OrnamentZBT.PriceInfo priceInfo = searchList.getPriceInfo(); + BigDecimal price = StringUtils.isNull(priceInfo) ? BigDecimal.ZERO : priceInfo.getPrice(); + Integer quantity = StringUtils.isNull(priceInfo) ? 0 : priceInfo.getQuantity(); + TtOrnament ttOrnament = ornamentsMap.get(marketHashName); + if (!Objects.isNull(ttOrnament)) { + ttOrnament.setUsePrice(price.multiply(ZBTParities).multiply(usePricePremiumRate)); + ttOrnament.setPrice(price); + ttOrnament.setQuantity(quantity); + ttOrnament.setUpdateTime(DateUtils.getNowDate()); + ttOrnamentList.add(ttOrnament); + } + }); + log.info("更新条数:" + ttOrnamentList.size()); + boolean isSuccess = ornamentsService.updateBatchById(ttOrnamentList, 1); + if (isSuccess) { + log.info("批量更新饰品价格成功!"); + } else { + log.info("批量更新饰品价格失败!"); + } + } + + // 批量插入饰品 + private void batchInsertOrnaments(List ZBTOrnamentsDataListByType, String ornamentsTypeValue) { + List rarity = dictDataMapper.selectDictDataByType("ornaments_rarity"); + if (ObjectUtils.isEmpty(rarity)) return; + List exterior = dictDataMapper.selectDictDataByType("ornaments_exterior"); + if (ObjectUtils.isEmpty(exterior)) return; + List quality = dictDataMapper.selectDictDataByType("ornaments_quality"); + if (ObjectUtils.isEmpty(quality)) return; + String usePricePremiumRateStr = configService.selectConfigByKey("usePricePremiumRate"); + BigDecimal usePricePremiumRate = new BigDecimal(usePricePremiumRateStr); + String ZBTParitiesStr = configService.selectConfigByKey("ZBTParities"); + BigDecimal ZBTParities = new BigDecimal(ZBTParitiesStr); + Map rarityMap = objectToMap(rarity); + Map exteriorMap = objectToMap(exterior); + Map qualityMap = objectToMap(quality); + List ornamentsList = new ArrayList<>(); + ZBTOrnamentsDataListByType.forEach(searchList -> { + OrnamentZBT.PriceInfo priceInfo = searchList.getPriceInfo(); + BigDecimal price = StringUtils.isNull(priceInfo) ? BigDecimal.ZERO : priceInfo.getPrice(); + Integer quantity = StringUtils.isNull(priceInfo) ? 0 : priceInfo.getQuantity(); + String rarityValue = rarityMap.get(searchList.getRarity()); + String exteriorValue = exteriorMap.get(searchList.getExterior()); + String qualityValue = qualityMap.get(searchList.getQuality()); + TtOrnament ornaments = TtOrnament.builder().build(); + ornaments.setName(searchList.getItemName()); + ornaments.setUsePrice(price.multiply(ZBTParities).multiply(usePricePremiumRate)); + ornaments.setImageUrl(searchList.getImageUrl()); + ornaments.setMarketHashName(searchList.getMarketHashName()); + ornaments.setId(Long.valueOf(searchList.getItemId())); + ornaments.setPrice(price); + ornaments.setQuantity(quantity); + ornaments.setShortName(searchList.getShortName()); + ornaments.setType(ornamentsTypeValue); + ornaments.setTypeName(searchList.getTypeName()); + ornaments.setQuality(qualityValue); + ornaments.setQualityName(searchList.getQualityName()); + ornaments.setQualityColor(searchList.getQualityColor()); + ornaments.setRarity(rarityValue); + ornaments.setRarityName(searchList.getRarityName()); + ornaments.setRarityColor(searchList.getRarityColor()); + ornaments.setExterior(exteriorValue); + ornaments.setExteriorName(searchList.getExteriorName()); + ornaments.setCreateTime(DateUtils.getNowDate()); + ornamentsList.add(ornaments); + }); + log.info("新增条数:" + ornamentsList.size()); + boolean isSuccess = ornamentsService.saveBatch(ornamentsList, 1); + if (isSuccess) { + log.info("批量插入饰品成功!"); + } else { + log.info("批量插入饰品失败!"); + } + } + + private Map objectToMap(List list) { + Map map = new HashMap<>(); + for (SysDictData sysDictData : list) { + map.put(sysDictData.getDictLabel(), sysDictData.getDictValue()); + } + return map; + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/util/MD5Utils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/util/MD5Utils.java new file mode 100644 index 0000000..0499088 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/util/MD5Utils.java @@ -0,0 +1,21 @@ +package com.ruoyi.thirdparty.zbt.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class MD5Utils { + + public static String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(input.getBytes()); + StringBuilder sb = new StringBuilder(); + for (byte b : messageDigest) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 algorithm not found", e); + } + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/util/ZBTUtils.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/util/ZBTUtils.java new file mode 100644 index 0000000..c80d2e0 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zbt/util/ZBTUtils.java @@ -0,0 +1,75 @@ +package com.ruoyi.thirdparty.zbt.util; + +import com.ruoyi.common.utils.sign.Md5Utils; +import com.ruoyi.thirdparty.zbt.config.ZBTProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +public class ZBTUtils { + + private static final Logger log = LoggerFactory.getLogger(ZBTUtils.class); + + public static Map objectToMap(T obj) { + Map map = new HashMap<>(); + if (obj == null) { + return map; + } + + try { + Class clazz = obj.getClass(); + for (Field field : clazz.getDeclaredFields()) { + field.setAccessible(true); + String value = (String) field.get(obj); + if (value != null) { + map.put(field.getName(), value); + } + } + } catch (Exception e) { + log.error("对象转Map时出现异常", e); + } + return map; + } + + /** + * 验签 + */ + public static boolean verifySign(Map requestBody, String appSecret) { + // 提取sign字段 + String receivedSign = (String) requestBody.get("sign"); + + // 删除sign字段 + requestBody.remove("sign"); + + // 生成待签名字符串 + StringBuilder sb = new StringBuilder(); + TreeMap sortedParams = new TreeMap<>(); + requestBody.forEach((key, value) -> { + String stringValue = value == null ? "null" : value.toString(); + try { + String decodedValue = URLDecoder.decode(stringValue, "UTF-8"); + sortedParams.put(key, decodedValue); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("URL decoding error", e); + } + }); + + sortedParams.forEach((key, value) -> sb.append(key).append("=").append(value).append("&")); + + // 添加appSecret + sb.append("sign=").append(appSecret); + + // 计算签名 + String calculatedSign = MD5Utils.md5(sb.toString()).toUpperCase(); + + // 比较签名 + return calculatedSign.equals(receivedSign); + } + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/config/ZYConfig.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/config/ZYConfig.java new file mode 100644 index 0000000..00e9208 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/config/ZYConfig.java @@ -0,0 +1,30 @@ +package com.ruoyi.thirdparty.zyZFB.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "zy-zfb") +public class ZYConfig { + + private String merKey; + private String gateway; + private String tranType; + private String merchantId; + private String notifyBaseUrl; + + // 下单接口 + public static final String ApiAddTrans = "/ctp/view/server/aotori/addTrans"; + + // 查询订单接口 + public static final String ApiQueryTrans = "/ctp/view/server/aotori/queryTrans"; + + // 代付接口 + public static final String ApiPropayTrans = "/ctp/view/server/aotori/propayTrans"; + + // 查询余额接口 + public static final String ApiQueryBalancePHP = "/ctp/view/server/aotori/querybalance.php"; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/controller/ZyController.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/controller/ZyController.java new file mode 100644 index 0000000..0e9ba4d --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/controller/ZyController.java @@ -0,0 +1,119 @@ +package com.ruoyi.thirdparty.zyZFB.controller; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.zyZFB.config.ZYConfig; +import com.ruoyi.thirdparty.zyZFB.service.zyService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; + +@Api(tags = "中云支付宝") +@Slf4j +@RestController +@RequestMapping("api/zyZFB") +public class ZyController extends BaseController { + + @Autowired + private ZYConfig zyConfig; + + @Autowired + private zyService zyService; + + @Autowired + private TtUserService userService; + + // 下单 + @ApiOperation("预下单接口") + @PostMapping("/ApiAddTrans") + @UpdateUserCache + public R ApiAddTrans(HttpServletRequest request, CreateOrderParam param) { + R checkLogin = checkLogin(); + if (!checkLogin.getCode().equals(200)) return checkLogin; + Integer userId = ((Long) checkLogin.getData()).intValue(); + + TtUser user = userService.getById(userId); + + return zyService.ApiAddTrans(param, user, request); + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class PayNotifyData { + // 支付方式 + private String tranType; + private String respCode; + private String respDesc; + // 交易流水号 + private String serverRspNo; + private String amt; + private String merReqNo; + private String msgExt; + private String tranTime; + private String sign; + } + + // 支付回调 + @ApiOperation("支付回调") + // @PostMapping("/addTransNotify") + public String addTransNotify(PayNotifyData data) { + log.info("支付回调 {}", JSONUtil.toJsonStr(data)); + return zyService.payNotify(data); + } + + // 查询订单接口 + @ApiOperation("查询订单") + @PostMapping("/ApiQueryTrans") + public String ApiQueryTrans(HttpServletRequest request, Map param) { + return "success"; + } + + // 代付接口 + @ApiOperation("代付") + @PostMapping("/ApiPropayTrans") + public String ApiPropayTrans(HttpServletRequest request, Map param) { + return "success"; + } + + // 查询余额接口 + @ApiOperation("查询余额") + @PostMapping("/ApiQueryBalancePHP") + public String ApiQueryBalancePHP(HttpServletRequest request, Map param) { + return "success"; + } + + public R checkLogin() { + Long userId; + try { + userId = getUserId(); + if (ObjectUtil.isEmpty(userId)) return R.fail(401,"登录过期,请重新登录。"); + return R.ok(userId); + } catch (Exception e) { + return R.fail("登录过期,请重新登录。"); + } + } + + // 支付回调 + // @PostMapping("/propayTransNotify") + // public String propayTransNotify(Map map) { + // log.info("代付回调 {}", JSONUtil.toJsonStr(map)); + // return "success"; + // } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/service/Impl/zyServiceImpl.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/service/Impl/zyServiceImpl.java new file mode 100644 index 0000000..d882f45 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/service/Impl/zyServiceImpl.java @@ -0,0 +1,409 @@ +package com.ruoyi.thirdparty.zyZFB.service.Impl; + +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtFirstRechargeMapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtPromotionLevelMapper; +import com.ruoyi.admin.mapper.TtRechargeProdMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtBonusService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.common.constant.PayOrderStatus; +import com.ruoyi.domain.common.constant.PayType; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtPromotionLevel; +import com.ruoyi.domain.entity.TtRechargeProd; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService; +import com.ruoyi.thirdparty.zyZFB.config.ZYConfig; +import com.ruoyi.thirdparty.zyZFB.controller.ZyController.PayNotifyData; +import com.ruoyi.thirdparty.zyZFB.service.zyService; +import com.ruoyi.thirdparty.zyZFB.utils.ZYHTTPRes; +import com.ruoyi.thirdparty.zyZFB.utils.signUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; + +@Slf4j +@Service +public class zyServiceImpl implements zyService { + + @Autowired + private ZYConfig zyConfig; + + @Autowired + private TtOrderMapper orderMapper; + + @Autowired + private Executor customThreadPoolExecutor; + + @Autowired + private TtBonusService bonusService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtRechargeProdMapper rechargeListMapper; + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private TtPromotionLevelMapper ttPromotionLevelMapper; + + @Autowired + private TtFirstRechargeMapper ttFirstRechargeMapper; + + @Override + public R ApiAddTrans(@Validated CreateOrderParam param, TtUser user, HttpServletRequest request) { + + // 构建完整参数 + Map map = buildBaseParam(new HashMap<>(), request); + + Snowflake snowflake = IdUtil.getSnowflake(1, 1); + String merReqNo = String.valueOf(snowflake.nextId()); + map.put("merReqNo", merReqNo); + map.put("notifyUrl", zyConfig.getNotifyBaseUrl() + "/api/zyZFB/addTransNotify"); + map.put("returnUrl", zyConfig.getNotifyBaseUrl()); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); + String tranDateTime = dateFormat.format(new Date()); + map.put("tranDateTime", tranDateTime); + + BigDecimal totalAmount = param.getGoodsPrice() + .multiply(new BigDecimal(param.getGoodsNum())) + .multiply(new BigDecimal("100")) + .setScale(0, BigDecimal.ROUND_HALF_EVEN); + map.put("amt", totalAmount.toString()); + map.put("goodsName", param.getGoodsId().toString()); + + String sign = signUtil.getSign(map, zyConfig.getMerKey()); + map.put("sign", sign); + + // 发送请求 + HttpRequest post = HttpUtil.createPost(zyConfig.getGateway() + ZYConfig.ApiAddTrans); + // post.header("Content-Type","application/x-www-form-urlencoded"); + // post.body(param, "application/x-www-form-urlencoded"); + post.body(JSONUtil.toJsonStr(map), "json"); + HttpResponse res = post.execute(); + + // 解析响应 + ZYHTTPRes resBody = JSONUtil.toBean(res.body(), ZYHTTPRes.class); + if (!resBody.getRespCode().equals("0000")) { + return R.fail(resBody); + } + + TtOrder order = new TtOrder(); + order.setUserId(user.getUserId()); + // ttOrder.setThirdParty("0"); + order.setType(PayType.ZY_ZFB.getCode()); + order.setGoodsId(param.getGoodsId()); + order.setGoodsPrice(param.getGoodsPrice()); + order.setGoodsNum(param.getGoodsNum()); + order.setTotalAmount(totalAmount); + order.setOrderId(map.get("merReqNo")); + order.setSign(sign); + order.setStatus(PayOrderStatus.NO_PAY.getCode()); + order.setOutTradeNo(resBody.getMerReqNo()); + order.setPayUrl(resBody.getPayUrl()); + order.setCreateTime(new Date()); + + // System.out.println(order.getPayUrl().length()); + + orderMapper.insert(order); + + // 发送充值成功通知 + rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(user.getUserId().toString(), order.getGoodsPrice()); + + return R.ok(order); + } + + @Override + public String payNotify(PayNotifyData data) { + + // System.out.println("中云 支付回调" + JSONUtil.toJsonStr(data)); + + TtOrder order = null; + // TODO: 2024/4/12 最好再查询一下第三方平台的订单信息 + + try { + + // 查询订单信息 + order = new LambdaQueryChainWrapper<>(orderMapper) + .eq(TtOrder::getOrderId, data.getMerReqNo()) + .eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode()) + .one(); + + if (ObjectUtil.isEmpty(order)) { + log.warn("支付回调异常,不存在的有效订单。"); + return "fail"; + } + + // 查询用户信息 + TtUser user = new LambdaQueryChainWrapper<>(userMapper) + .eq(TtUser::getUserId, order.getUserId()) + .eq(TtUser::getDelFlag, 0) + .one(); + if (ObjectUtil.isEmpty(user)) { + log.warn("支付回调异常,不存在的有效用户。"); + return "fail"; + } + + if (order.getStatus().equals(PayOrderStatus.PAY_COMPLE.getCode()) || order.getStatus().equals(PayOrderStatus.PAY_YET.getCode())) { + log.warn("重复的回调!该订单已完成。"); + return "success"; + } + + // 查询商品信息 + TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeListMapper) + .eq(TtRechargeProd::getId, order.getGoodsId()) + .eq(TtRechargeProd::getStatus, 0) + .one(); + if (ObjectUtil.isEmpty(goods)) { + log.warn("支付回调异常,不存在的商品。"); + return "fail"; + } + + // 账户结算 + if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO); + if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO); + payNotifyAccounting(order, user, goods, order.getGoodsNum()); + + // 首充赠送 + firstChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广等级充值赠送 + promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum()); + + // 推广充值返佣 + promotionChargeCommission(user, goods, order.getGoodsNum()); + + // 更新订单 + new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .update(); + + return "success"; + + } catch (Exception e) { + + e.printStackTrace(); + + log.warn("zy zfb 回调异常"); + + if (ObjectUtil.isNotNull(order)) { + new LambdaUpdateChainWrapper<>(orderMapper) + .eq(TtOrder::getId, order.getId()) + .set(TtOrder::getStatus, PayOrderStatus.CALL_BACK_ERRO.getCode()) + .set(TtOrder::getUpdateTime, new Date()) + .update(); + } + + return "fail"; + + } + + } + + @Override + public R ApiQueryTrans(Map param, TtUser user) { + return null; + } + + @Override + public R ApiPropayTrans(Map param, TtUser user) { + return null; + } + + @Override + public R ApiQueryBalancePHP(Map param, TtUser user) { + return null; + } + + // 构建基本请求信息 + private Map buildBaseParam(Map map, HttpServletRequest request) { + map.put("merchantId", zyConfig.getMerchantId()); + map.put("tranType", zyConfig.getTranType()); + map.put("msgExt", "success"); + // map.put("tranType", "2004"); + // map.put("notifyUrl",zyConfig.getNotifyBaseUrl()+"api/zyZFB/addTransNotify"); + if (ObjectUtil.isNotEmpty(request)) { + map.put("creatIp", request.getRemoteAddr()); + // map.put("creatIp", "110.228.15.148"); + } + return map; + } + + public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) { + + // 加钱(充值不加积分:productA + productC 全部计入 account_amount) + // BigDecimal totalAmount = order.getTotalAmount(); + BigDecimal productAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal productGift = goods.getProductC().multiply(new BigDecimal(goodsNumber)); + BigDecimal totalAmount = productAmount.add(productGift); + BigDecimal totalCredits = BigDecimal.ZERO; + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, user.getUserId()) + .setSql("account_amount = account_amount + " + totalAmount.toString()); + userService.update(userUpdate); + + user = userService.getById(user.getUserId()); + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(user.getUserId()) + + .amount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? totalAmount : null) + .finalAmount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountAmount().add(totalAmount) : null) + + .credits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? totalCredits : null) + .finalCredits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountCredits().add(totalCredits) : null) + + .total(totalAmount.add(totalCredits)) // 收支合计 + + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE.getCode()) + .remark(TtAccountRecordSource.RECHARGE.getMsg()) + + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + + int insert = userBlendErcashMapper.insert(blendErcash); + + } + + /** + * 首充赠送 + */ + private void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.判断是否为首充 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper + .eq(TtUserBlendErcash::getSource, 1) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()); + List ttUserBlendErcashes = userBlendErcashMapper.selectList(wrapper); + if (ttUserBlendErcashes.size() > 0) { + return; + } + + // 2.加钱 + // 充值金额 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + // 查询赠送比例 + BigDecimal firstChargeAmountRatio = ttFirstRechargeMapper.selectRatioByMinAmount(totalAmount); + if (Objects.isNull(firstChargeAmountRatio)) { + return; + } + // 计算赠送金额 + BigDecimal giftAmount = totalAmount.multiply(firstChargeAmountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.FIRST_CHARGE.getCode()) + .remark(TtAccountRecordSource.FIRST_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广等级充值赠送 + */ + private void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + // 1.查询用户推广等级对应的赠送比例 + TtPromotionLevel ttPromotionLevel = ttPromotionLevelMapper.selectById(ttUser.getPromotionLevel()); + + // 2.加钱 + BigDecimal amountRatio = ttPromotionLevel.getRechargeGiftRatio(); + if (Objects.isNull(amountRatio)) + return; + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + BigDecimal giftAmount = totalAmount.multiply(amountRatio); + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, ttUser.getUserId()) + .setSql("account_amount = account_amount + " + giftAmount.toString()); + userService.update(userUpdate); + + // 3.记录 + ttUser = userService.getById(ttUser.getUserId()); + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(ttUser.getUserId()) + .amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null) + .finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null) + .total(giftAmount) + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getCode()) + .remark(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .build(); + userBlendErcashMapper.insert(blendErcash); + } + + /** + * 推广充值返佣 + */ + private void promotionChargeCommission(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) { + if (Objects.isNull(ttUser.getParentId())) { + return; + } + // 充值返佣统一走 TtBonusService:按各级上级 commissionRate 逐级向上实时返佣、招商周结, + // 写 source=13,与卡密充值(ApiRechargeServiceImpl→bonus)口径一致,避免使用推广等级费率造成口径不符。 + BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber)); + bonusService.distributeRechargeCommission(ttUser.getUserId(), totalAmount); + } +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/service/zyService.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/service/zyService.java new file mode 100644 index 0000000..9a13c2e --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/service/zyService.java @@ -0,0 +1,19 @@ +package com.ruoyi.thirdparty.zyZFB.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.other.CreateOrderParam; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.thirdparty.zyZFB.controller.ZyController; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; + +public interface zyService { + + R ApiAddTrans(CreateOrderParam param, TtUser user, HttpServletRequest request); + R ApiQueryTrans(Map param, TtUser user); + R ApiPropayTrans(Map param, TtUser user); + R ApiQueryBalancePHP(Map param, TtUser user); + + String payNotify(ZyController.PayNotifyData data); +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/utils/ZYHTTPRes.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/utils/ZYHTTPRes.java new file mode 100644 index 0000000..b6dce5b --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/utils/ZYHTTPRes.java @@ -0,0 +1,47 @@ +package com.ruoyi.thirdparty.zyZFB.utils; + +// zy返回值 + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ZYHTTPRes { + + public enum resCode{ + + success("0000"), + DOING("0001"); + + private String code; + + resCode(String code){ + this.code = code; + } + public String getCode(){ + return this.code; + } + } + + private String respCode; + //返回码说明 + private String respDesc; + //返回码 + private String payCode; + private String payDesc; + private String amt; + private String tranTime; + private String tranType; + //扩展信息 + private String msgExt; + private String payUrl; + // 交易流水号 + private String merReqNo; + //服务端响应流水号 + private String serverRspNo; + private String sign; + +} diff --git a/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/utils/signUtil.java b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/utils/signUtil.java new file mode 100644 index 0000000..3175a35 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/java/com/ruoyi/thirdparty/zyZFB/utils/signUtil.java @@ -0,0 +1,46 @@ +package com.ruoyi.thirdparty.zyZFB.utils; + +import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; + +import java.security.MessageDigest; +import java.util.*; + +public class signUtil { + + public static String getSign(Map map, String key) { + + // 排序 + List> entryList = new ArrayList<>(map.entrySet()); + Collections.sort(entryList, new Comparator>() { + @Override + public int compare(Map.Entry o1, Map.Entry o2) { + return o1.getKey().compareTo(o2.getKey()); + } + }); + + String str = ""; + for (int i = 0; i < entryList.size(); i++) { + if (i == entryList.size()-1) { + str = str + entryList.get(i).getKey() + "=" + entryList.get(i).getValue(); + continue; + } + str = str + entryList.get(i).getKey() + "=" + entryList.get(i).getValue() + "&"; + } + str = str + key; + + // MD5加密 + String sign = ""; + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + // 转换为MD5码 + byte[] digest = md5.digest(str.getBytes("utf-8")); + sign = ByteUtils.toHexString(digest); + } catch (Exception e) { + System.out.println("md5加密 warn"); + } + + return sign; + + } + +} diff --git a/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TianXinMapper.xml b/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TianXinMapper.xml new file mode 100644 index 0000000..f42a283 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TianXinMapper.xml @@ -0,0 +1,114 @@ + + + + + + insert into tianxin_order + + order_id, + pay_type, + goods_id, + goods_price, + goods_num, + total_amount, + user_ip, + pay_status, + create_time, + update_time, + user_id, + user_name, + remark, + call_back_order_id, + call_back_msg, + call_back_status, + sign, + subject, + + + #{orderId}, + #{payType}, + #{goodsId}, + #{goodsPrice}, + #{goodsNum}, + #{totalAmount}, + #{userIp}, + #{payStatus}, + #{createTime}, + #{updateTime}, + #{userId}, + #{userName}, + #{remark}, + #{callBackOrderId}, + #{callBackMsg}, + #{callBackStatus}, + #{sign}, + #{subject}, + + + + + update tianxin_order + + pay_type = #{payType}, + goods_id = #{goodsId}, + goods_price = #{goodsPrice}, + goods_num = #{goodsNum}, + total_amount = #{totalAmount}, + user_ip = #{userIp}, + pay_status = #{payStatus}, + create_time = #{createTime}, + update_time = #{updateTime}, + user_id = #{userId}, + user_name = #{userName}, + remark = #{remark}, + call_back_order_id = #{callBackOrderId}, + call_back_msg = #{callBackMsg}, + call_back_status = #{callBackStatus}, + sign = #{sign}, + subject = #{subject}, + + where order_id = #{orderId} + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TtCoinRechargeRecordMapper.xml b/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TtCoinRechargeRecordMapper.xml new file mode 100644 index 0000000..503f5e8 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TtCoinRechargeRecordMapper.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + select + id, + coin, + pay_status, + uid, + uname, + create_time, + update_time, + order_no, + callback_no, + callback_msg + from tt_coin_recharge_record + + + + + + + + + + + + + + + + + insert into tt_coin_recharge_record + + id, + coin, + pay_status, + uid, + uname, + create_time, + update_time, + order_no, + callback_no, + callback_msg, + + + #{id}, + #{coin}, + #{payStatus}, + #{uid}, + #{uname}, + #{createTime}, + #{updateTime}, + #{orderNo}, + #{callbackNo}, + #{callbackMsg}, + + + + + update tt_coin_recharge_record + + coin = #{coin}, + pay_status = #{payStatus}, + uid = #{uid}, + uname = #{uname}, + create_time = #{createTime}, + update_time = #{updateTime}, + order_no = #{orderNo}, + callback_no = #{callbackNo}, + callback_msg = #{callbackMsg}, + + where id = #{id} + + + + delete from tt_coin_recharge_record where id = #{id} + + + + delete from tt_coin_recharge_record where id in + + #{id} + + + \ No newline at end of file diff --git a/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TtCoinRecordMapper.xml b/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TtCoinRecordMapper.xml new file mode 100644 index 0000000..2993799 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TtCoinRecordMapper.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + select id, uid, uname, `type`,oper_type, reward_type, money, `time`, status, create_time, update_time from tt_coin_record + + + + + + + + insert into tt_coin_record + + uid, + uname, + `type`, + oper_type, + reward_type, + money, + `time`, + status, + create_time, + update_time, + + + #{uid}, + #{uname}, + #{type}, + #{operType}, + + #{rewardType}, + #{money}, + #{time}, + #{status}, + #{createTime}, + #{updateTime}, + + + + + update tt_coin_record + + uid = #{uid}, + uname = #{uname}, + type = #{type}, + oper_type = #{operType}, + reward_type = #{rewardType}, + money = #{money}, + `time` = #{time}, + status = #{status}, + create_time = #{createTime}, + update_time = #{updateTime}, + + where id = #{id} + + + + delete from tt_coin_record where id = #{id} + + + + delete from tt_coin_record where id in + + #{id} + + + \ No newline at end of file diff --git a/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TtUserLsjlMapper.xml b/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TtUserLsjlMapper.xml new file mode 100644 index 0000000..dfe15f8 --- /dev/null +++ b/skins-service/service-thirdparty/src/main/resources/mapper/skinservice/TtUserLsjlMapper.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, + tt_user_id, + type, + v_coin_before, + v_coin_after, + v_coin_change, + d_coin_before, + d_coin_change, + d_coin_after, + create_time, + update_time + from tt_user_lsjl + + + + + + + + insert into tt_user_lsjl + + tt_user_id, + type, + v_coin_before, + v_coin_after, + v_coin_change, + d_coin_before, + d_coin_change, + d_coin_after, + create_time, + update_time, + + + #{ttUserId}, + #{type}, + #{vCoinBefore}, + #{vCoinAfter}, + #{vCoinChange}, + #{dCoinBefore}, + #{dCoinChange}, + #{dCoinAfter}, + #{createTime}, + #{updateTime}, + + + + + update tt_user_lsjl + + tt_user_id = #{ttUserId}, + type = #{type}, + v_coin_before = #{vCoinBefore}, + v_coin_after = #{vCoinAfter}, + v_coin_change = #{vCoinChange}, + d_coin_before = #{dCoinBefore}, + d_coin_change = #{dCoinChange}, + d_coin_after = #{dCoinAfter}, + create_time = #{createTime}, + update_time = #{updateTime}, + + where id = #{id} + + + + delete + from tt_user_lsjl + where id = #{id} + + + + delete from tt_user_lsjl where id in + + #{id} + + + \ No newline at end of file diff --git a/skins-service/service-user/pom.xml b/skins-service/service-user/pom.xml new file mode 100644 index 0000000..826348b --- /dev/null +++ b/skins-service/service-user/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + com.ruoyi + skins-service + 4.8.2 + + + service-user + + + + com.ruoyi + ruoyi-framework + + + + com.ruoyi + service-admin + 4.8.2 + + + + + com.ruoyi + message-service + 4.8.2 + + + + + cloud.tianai.captcha + tianai-captcha-springboot-starter + 1.5.5 + + + + com.ruoyi + service-thirdparty + 4.8.2 + + + + \ No newline at end of file diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/config/CaptchaResourceConfiguration.java b/skins-service/service-user/src/main/java/com/ruoyi/user/config/CaptchaResourceConfiguration.java new file mode 100644 index 0000000..f7bf847 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/config/CaptchaResourceConfiguration.java @@ -0,0 +1,69 @@ +package com.ruoyi.user.config; + +import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.resource.ResourceStore; +import cloud.tianai.captcha.resource.common.model.dto.Resource; +import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +@Slf4j +@Configuration +public class CaptchaResourceConfiguration { + + @Autowired + private ApplicationContext applicationContext; + + @Bean + public ResourceStore resourceStore() throws IOException { + LocalMemoryResourceStore resourceStore = new LocalMemoryResourceStore(); + + // 动态扫描 classpath:image/ 下的背景图 + // 优先加载 jpg(体积更小、I/O 更快,tianai-captcha 官方推荐), + // 仅在没有 jpg 时才回退加载 png,避免两种格式重复注册 + // 注意:必须用 classpath*: 前缀,否则打成 fat jar 后部署会失败 + // (FileNotFoundException: class path resource [image/] cannot be resolved to URL)。 + // PathMatchingResourcePatternResolver 只有在 classpath*: 下才会展开 jar 内部的 entries。 + int count = registerImages("classpath*:image/*.jpg", resourceStore); + if (count == 0) { + log.info("[滑块验证码] 未找到 JPG,回退加载 PNG"); + count = registerImages("classpath*:image/*.png", resourceStore); + } + + log.info("[滑块验证码] 已加载 {} 张背景图片", count); + if (count == 0) { + log.error("[滑块验证码] 未扫到任何背景图片!请检查:\n" + + " 1) service-user/src/main/resources/image/ 是否存在且含 PNG/JPG\n" + + " 2) 文件名是否为 ASCII(避免中文/空格)\n" + + " 3) fat jar 部署时内嵌 jar 中是否含 image/ 目录"); + } + + return resourceStore; + } + + /** + * 扫描指定模式的图片并注册到 ResourceStore + * + * @param pattern Spring 资源匹配模式,如 classpath*:image/*.jpg + * @param resourceStore 资源存储器 + * @return 注册的图片数量 + */ + private int registerImages(String pattern, LocalMemoryResourceStore resourceStore) throws IOException { + org.springframework.core.io.Resource[] images = applicationContext.getResources(pattern); + int count = 0; + for (org.springframework.core.io.Resource img : images) { + String filename = img.getFilename(); + if (filename != null) { + resourceStore.addResource(CaptchaTypeConstant.SLIDER, + new Resource("classpath", "image/" + filename, "default")); + count++; + } + } + return count; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/consumer/NoticeConsumer.java b/skins-service/service-user/src/main/java/com/ruoyi/user/consumer/NoticeConsumer.java new file mode 100644 index 0000000..975743c --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/consumer/NoticeConsumer.java @@ -0,0 +1,44 @@ +package com.ruoyi.user.consumer; + +import com.rabbitmq.client.Channel; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.other.TtNotice; +import com.ruoyi.user.service.ApiNoticeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Map; + +@Component +@Slf4j +public class NoticeConsumer { + + @Autowired + private ApiNoticeService apiNoticeService; + + @RabbitListener(queues = "notice_queue") + public void insertNotice(Map noticeMap, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) { + String userId = noticeMap.get("userId"); + String title = noticeMap.get("title"); + String content = noticeMap.get("content"); + String createTime = noticeMap.get("createTime"); + TtNotice ttNotice = new TtNotice(); + ttNotice.setUserId(Long.valueOf(userId)); + ttNotice.setTitle(title); + ttNotice.setContent(content); + ttNotice.setCreateTime(DateUtils.parseDate(createTime)); + apiNoticeService.addNotice(ttNotice); + + try { + //手动确认消费 + channel.basicAck(tag, false); + } catch (IOException e) { + log.error(e.getMessage()); + } + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/AmountRankRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/AmountRankRequest.java new file mode 100644 index 0000000..3c70247 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/AmountRankRequest.java @@ -0,0 +1,10 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class AmountRankRequest { + private Integer type; + private Integer page; + private Integer size; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/BaiduCallbackRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/BaiduCallbackRequest.java new file mode 100644 index 0000000..5a7b3fd --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/BaiduCallbackRequest.java @@ -0,0 +1,11 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class BaiduCallbackRequest { + /** 类型 */ + private Integer newType; + /** 百度ID */ + private String bdVid; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/BatchOperationMessageRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/BatchOperationMessageRequest.java new file mode 100644 index 0000000..d6e0660 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/BatchOperationMessageRequest.java @@ -0,0 +1,10 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class BatchOperationMessageRequest { + + private Integer[] ids; + private String status; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/BindInvitationCodeRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/BindInvitationCodeRequest.java new file mode 100644 index 0000000..e4f90d8 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/BindInvitationCodeRequest.java @@ -0,0 +1,11 @@ +package com.ruoyi.user.contract.api.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class BindInvitationCodeRequest { + + @NotBlank(message = "邀请码不能为空") + private String invitationCode; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ContentByTypeRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ContentByTypeRequest.java new file mode 100644 index 0000000..e5f1995 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ContentByTypeRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class ContentByTypeRequest { + + private String alias; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/CreditsRankRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/CreditsRankRequest.java new file mode 100644 index 0000000..6491523 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/CreditsRankRequest.java @@ -0,0 +1,10 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class CreditsRankRequest { + private Integer type; + private Integer page; + private Integer size; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ExchangeOrnamentsId.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ExchangeOrnamentsId.java new file mode 100644 index 0000000..d44192a --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ExchangeOrnamentsId.java @@ -0,0 +1,10 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +import java.util.List; + +@Data +public class ExchangeOrnamentsId { + private List ornamentsIds; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetAdvertisementRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetAdvertisementRequest.java new file mode 100644 index 0000000..04ad7e9 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetAdvertisementRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class GetAdvertisementRequest { + /** 广告ID */ + private Integer id; +} \ No newline at end of file diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetAnnouncementListRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetAnnouncementListRequest.java new file mode 100644 index 0000000..79aa567 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetAnnouncementListRequest.java @@ -0,0 +1,10 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class GetAnnouncementListRequest { + + /** Tab 类型:0 系统 / 1 游戏;为空返回全部 */ + private Integer tabType; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetAnnouncementRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetAnnouncementRequest.java new file mode 100644 index 0000000..70cc243 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetAnnouncementRequest.java @@ -0,0 +1,15 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class GetAnnouncementRequest { + /** + * 消息主键ID,与 /list 返回结构中的 id 字段对齐。 + * 需配合 source 使用:source=1 时为公告ID,source=2 时为通知ID。 + */ + private Integer id; + + /** 消息来源:1 公告 / 2 通知,与 /list 返回结构中的 source 字段对齐 */ + private Integer source; +} \ No newline at end of file diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetMessageListRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetMessageListRequest.java new file mode 100644 index 0000000..6e39338 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetMessageListRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class GetMessageListRequest { + /** 消息类型ID*/ + private Integer id; +} \ No newline at end of file diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetNoticeRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetNoticeRequest.java new file mode 100644 index 0000000..cb4b33f --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetNoticeRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class GetNoticeRequest { + /** 通知ID */ + private Integer noticeId; +} \ No newline at end of file diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetRewardRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetRewardRequest.java new file mode 100644 index 0000000..a973ff9 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetRewardRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class GetRewardRequest { + /** 任务ID */ + private Integer taskId; +} \ No newline at end of file diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetShoppingValueRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetShoppingValueRequest.java new file mode 100644 index 0000000..383128f --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/GetShoppingValueRequest.java @@ -0,0 +1,8 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class GetShoppingValueRequest { + private String value; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/IntegratingConversionRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/IntegratingConversionRequest.java new file mode 100644 index 0000000..c47c4a7 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/IntegratingConversionRequest.java @@ -0,0 +1,10 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class IntegratingConversionRequest { + private BigDecimal credits; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/MarkAsReadRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/MarkAsReadRequest.java new file mode 100644 index 0000000..1621fa3 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/MarkAsReadRequest.java @@ -0,0 +1,26 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +/** + * 批量设置通知为已读请求 + */ +@Data +public class MarkAsReadRequest { + /** + * 分类类型:0-系统通知,1-游戏通知 + */ + private Integer categoryType; + + /** + * 获取分类前缀(根据前端传入的类型进行映射) + * @return 分类前缀字符串 + */ + public String getCategoryPrefix() { + if (categoryType == null) { + return null; + } + // 0: SYS_, 1: GAME_ + return categoryType == 1 ? "GAME_" : "SYS_"; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/MessageRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/MessageRequest.java new file mode 100644 index 0000000..7269586 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/MessageRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class MessageRequest { + + private Integer id; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PWelfareCreditsRecordsRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PWelfareCreditsRecordsRequest.java new file mode 100644 index 0000000..59e2c15 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PWelfareCreditsRecordsRequest.java @@ -0,0 +1,10 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class PWelfareCreditsRecordsRequest { + private Integer type; + private Integer page; + private Integer size; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PWelfareRecordsRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PWelfareRecordsRequest.java new file mode 100644 index 0000000..83ef83f --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PWelfareRecordsRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class PWelfareRecordsRequest { + private Integer page; + private Integer size; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PersonalRecordRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PersonalRecordRequest.java new file mode 100644 index 0000000..51244b7 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PersonalRecordRequest.java @@ -0,0 +1,28 @@ +package com.ruoyi.user.contract.api.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class PersonalRecordRequest { + @NotNull + @Min(value = 1,message = "最小值1") + private Integer page; + @NotNull + @Min(value = 1,message = "最小值1") + @Max(value = 50, message = "最大值20") + private Integer pageSize; + + // 1:今日 2:昨日 3:最近7天 + @NotNull(message = "type不能为空") + private Integer type; + + // 查询的数据源 1:充值 2:消费 3:红包 + @NotNull(message = "source不能为空") + private Integer source; + + // 过滤选项 筛选条件 0:支出 1:收入 + private Integer filteringOptions; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PropRankOfDayRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PropRankOfDayRequest.java new file mode 100644 index 0000000..51f70fc --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/PropRankOfDayRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class PropRankOfDayRequest { + private Integer type; + private Integer number; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/QueryTransferRecordRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/QueryTransferRecordRequest.java new file mode 100644 index 0000000..f421b86 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/QueryTransferRecordRequest.java @@ -0,0 +1,10 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class QueryTransferRecordRequest { + private String kind; + private Integer pageNum; + private Integer pageSize; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ReceiveRedPacketRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ReceiveRedPacketRequest.java new file mode 100644 index 0000000..35cc559 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ReceiveRedPacketRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class ReceiveRedPacketRequest { + /** 红包码 */ + private String code; +} \ No newline at end of file diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/RechargeDetailsRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/RechargeDetailsRequest.java new file mode 100644 index 0000000..9828502 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/RechargeDetailsRequest.java @@ -0,0 +1,11 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class RechargeDetailsRequest { + /** 页码 */ + private Integer page; + /** 每页数量 */ + private Integer size; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/UserCreditsToAmountRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/UserCreditsToAmountRequest.java new file mode 100644 index 0000000..35facce --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/UserCreditsToAmountRequest.java @@ -0,0 +1,11 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class UserCreditsToAmountRequest { + /** 积分数量 */ + private BigDecimal credits; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/UserRechargeBonusRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/UserRechargeBonusRequest.java new file mode 100644 index 0000000..b712b07 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/UserRechargeBonusRequest.java @@ -0,0 +1,11 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class UserRechargeBonusRequest { + /** 充值金额门槛 */ + private Integer recharge; + /** 奖励金额 */ + private Double bonus; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/UserVipLevelRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/UserVipLevelRequest.java new file mode 100644 index 0000000..6d4cf70 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/UserVipLevelRequest.java @@ -0,0 +1,13 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +import java.util.List; + +@Data +public class UserVipLevelRequest { + /** + * 要领取的 VIP 显示等级集合(VIPn 中的 n,支持一次领多个,例如 [1,2,3] 表示 VIP1、VIP2、VIP3) + */ + private List vipLevels; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ViewMessageRequest.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ViewMessageRequest.java new file mode 100644 index 0000000..b27ab8f --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/request/ViewMessageRequest.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.contract.api.request; + +import lombok.Data; + +@Data +public class ViewMessageRequest { + /** 消息ID */ + private Integer id; +} \ No newline at end of file diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/PeakMomentResponse.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/PeakMomentResponse.java new file mode 100644 index 0000000..7ac3bae --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/PeakMomentResponse.java @@ -0,0 +1,48 @@ +package com.ruoyi.user.contract.api.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * 巅峰时刻 - 近7日高品质TOP10记录 + */ +@Data +public class PeakMomentResponse { + + /** 用户ID */ + private Integer userId; + /** 用户昵称 */ + private String nickName; + /** 用户头像 */ + private String avatar; + + /** 近7日内获得的高品质TOP10饰品列表(按价格降序) */ + private List items; + + @Data + public static class PeakItem { + /** 记录ID */ + private Long id; + /** 饰品完整名称(tt_ornament.name,回退 tt_box_records.ornament_name) */ + private String name; + /** 饰品名称 */ + private String ornamentName; + /** 饰品图片 */ + private String imageUrl; + /** 饰品等级图片 */ + private String ornamentLevelImg; + /** 外观名称(如:崭新出厂) */ + private String exteriorName; + /** 饰品价格 */ + private BigDecimal ornamentsPrice; + /** 饰品来源 */ + private Integer source; + /** 获得时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/PersonalRecordResponse.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/PersonalRecordResponse.java new file mode 100644 index 0000000..516d00f --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/PersonalRecordResponse.java @@ -0,0 +1,24 @@ +package com.ruoyi.user.contract.api.response; + +import lombok.Data; + +import java.util.List; + +@Data +public class PersonalRecordResponse { + private long total; + private List rows; + + @Data + public static class RecordVO { + private String amount; + + private String finalAmount; + + private String remark; + + private String datetime; + + private Integer source; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/QueryTransferRecordResponse.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/QueryTransferRecordResponse.java new file mode 100644 index 0000000..fccfbde --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/QueryTransferRecordResponse.java @@ -0,0 +1,12 @@ +package com.ruoyi.user.contract.api.response; + +import com.ruoyi.user.model.vo.ApiTransferRecordVO; +import lombok.Data; + +import java.util.List; + +@Data +public class QueryTransferRecordResponse { + private long total; + private List rows; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/QueryVipRewardResponse.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/QueryVipRewardResponse.java new file mode 100644 index 0000000..ce8fc97 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/QueryVipRewardResponse.java @@ -0,0 +1,41 @@ +package com.ruoyi.user.contract.api.response; + +import com.ruoyi.domain.other.TtVipLevel; +import lombok.Data; + +import java.util.List; + +@Data +public class QueryVipRewardResponse { + /** 今日游戏流水 */ + private String todayAmount; + /** 预计奖励(今日流水 × 当前VIP等级流水返利百分比) */ + private String todayReward; + /** 昨日游戏流水 */ + private String yesterdayAmount; + /** 已领奖励(昨日流水对应的VIP流水返利) */ + private String yesterdayReward; + /** VIP等级列表(含每个等级的达标金额、流水返利百分比等) */ + private List vipLevels; + + // ====== 当前等级与经验进度 ====== + + /** 当前VIP等级 */ + private Integer currentVipLevel; + /** 当前累计经验值 */ + private Long currentExp; + /** 下一级所需经验阈值 */ + private Long nextLevelExp; + /** 距离下一级还需经验值 */ + private Long needExp; + /** 是否已是最高VIP等级 */ + private Boolean maxLevel; + /** 进度文本,形如 "3500/12000" */ + private String progressText; + + /** 当前VIP每日转赠次数上限 */ + private Integer transferCount; + + /** 今日还可转赠次数 */ + private Integer availableTransferCount; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/VipFlowRewardResponse.java b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/VipFlowRewardResponse.java new file mode 100644 index 0000000..6299639 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/contract/api/response/VipFlowRewardResponse.java @@ -0,0 +1,70 @@ +package com.ruoyi.user.contract.api.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * VIP 流水奖励接口响应(精简版,仅包含流水奖励页面所需数据) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VipFlowRewardResponse { + + /** 今日游戏流水 */ + private String todayAmount; + + /** 预计奖励(今日流水 × 当前VIP等级流水返利百分比) */ + private String todayReward; + + /** 昨日游戏流水 */ + private String yesterdayAmount; + + /** 已领奖励(昨日VIP流水返利实际发放金额) */ + private String yesterdayReward; + + /** VIP等级列表(精简:仅名称、达标金额、流水返利百分比) */ + private List vipLevels; + + /** 当前VIP等级 */ + private Integer currentVipLevel; + + /** 当前累计经验值 */ + private Long currentExp; + + /** 下一级所需经验阈值 */ + private Long nextLevelExp; + + /** 距离下一级还需经验值 */ + private Long needExp; + + /** 是否已是最高VIP等级 */ + private Boolean maxLevel; + + /** 进度文本,形如 "3500/12000" */ + private String progressText; + + /** + * VIP等级精简项 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class VipLevelItem { + /** 等级名称(如 VIP0, VIP1 ...) */ + private String name; + /** 等级达标金额(经验阈值) */ + private BigDecimal rechargeThreshold; + /** 流水返利百分比(如 0.2 代表 0.2%) */ + private BigDecimal flowRebatePercent; + /** 当前等级进度文本,形如 "500/500"、"3500/5000" */ + private String progressText; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiAdvertisementController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiAdvertisementController.java new file mode 100644 index 0000000..50a0ab5 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiAdvertisementController.java @@ -0,0 +1,28 @@ +package com.ruoyi.user.controller; + +import com.ruoyi.admin.service.TtAdvertisementService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.other.TtAdvertisement; +import com.ruoyi.user.contract.api.request.GetAdvertisementRequest; +import io.swagger.annotations.Api; +import org.springframework.web.bind.annotation.*; + +@Api(tags = "广告查询") +@RestController +@RequestMapping("/api/advertisement") +public class ApiAdvertisementController { + + private final TtAdvertisementService ttAdvertisementService; + + public ApiAdvertisementController(TtAdvertisementService ttAdvertisementService) { + this.ttAdvertisementService = ttAdvertisementService; + } + + @PostMapping("/getInfo") + public R getInfo(@RequestBody GetAdvertisementRequest getAdvertisementRequest) { + TtAdvertisement ttAdvertisement = ttAdvertisementService.getById(getAdvertisementRequest.getId()); + // ttBanner.setPicture(""); + return R.ok(ttAdvertisement); + } + +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiAnnouncementController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiAnnouncementController.java new file mode 100644 index 0000000..e6c5fad --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiAnnouncementController.java @@ -0,0 +1,171 @@ +package com.ruoyi.user.controller; + + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.other.TtAnnouncement; +import com.ruoyi.user.contract.api.request.GetAnnouncementListRequest; +import com.ruoyi.user.contract.api.request.GetAnnouncementRequest; +import com.ruoyi.user.contract.api.request.MarkAsReadRequest; +import com.ruoyi.user.model.vo.ApiNoticeVO; +import com.ruoyi.user.model.vo.MessageItemVO; +import com.ruoyi.user.service.ApiAnnouncementService; +import com.ruoyi.user.service.ApiNoticeService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@Api(tags = "公告") +@RestController +@RequestMapping("/api/announcement") +public class ApiAnnouncementController extends BaseController { + + @Autowired + private ApiAnnouncementService apiAnnouncementService; + + @Autowired + private ApiNoticeService apiNoticeService; + + @ApiOperation("获取公告列表(含用户通知,统一结构返回)") + @ApiResponse(code = 200, message = "read【阅读状态(0未读 1已读)】") + @PostMapping("/list") + public AjaxResult list(@RequestBody(required = false) GetAnnouncementListRequest request) { + Integer tabType = request == null ? null : request.getTabType(); + Long userId = SecurityUtils.getUserId(); + List result = new ArrayList<>(); + + // tabType=1(游戏 Tab)只返回通知;tabType=0(系统 Tab)或 null 才返回公告 + if (!Integer.valueOf(1).equals(tabType)) { + List announcementList = apiAnnouncementService.getAnnouncementList(userId); + if (announcementList != null) { + for (TtAnnouncement a : announcementList) { + result.add(toItem(a)); + } + } + } + + // 根据 tabType 决定查询哪类通知 + List noticeList; + if (tabType == null) { + noticeList = apiNoticeService.getNoticeList(userId); + } else if (tabType == 1) { + noticeList = apiNoticeService.getNoticeListByCategoryPrefix(userId, "GAME_"); + } else { + noticeList = apiNoticeService.getNoticeListByCategoryPrefix(userId, "SYS_"); + } + if (noticeList != null) { + for (ApiNoticeVO n : noticeList) { + result.add(toItem(n)); + } + } + + // 按创建时间倒序 + result.sort(Comparator.comparing(MessageItemVO::getCreateTime, + Comparator.nullsLast(Comparator.reverseOrder()))); + return AjaxResult.success(result); + } + + @ApiOperation("获取系统公告列表(免登录)") + @PostMapping("/systemList") + public TableDataInfo systemList() { + startPage(); + List announcementList = apiAnnouncementService.getSystemAnnouncementList(); + return getDataTable(announcementList); + } + + @ApiOperation("获取公告/通知详情(统一结构返回)") + @PostMapping("/detail") + public AjaxResult getAnnouncementByAnnouncementId(@RequestBody GetAnnouncementRequest request) { + Long userId = SecurityUtils.getUserId(); + + if (request.getId() == null || request.getSource() == null) { + return AjaxResult.error("请传入 id 与 source"); + } + + // source=1 公告:返回公告详情同时写入 tt_announcement_read + if (Integer.valueOf(1).equals(request.getSource())) { + TtAnnouncement ttAnnouncement = apiAnnouncementService + .getAnnouncementByAnnouncementId(request.getId(), userId); + if (ttAnnouncement == null) { + return AjaxResult.error("公告不存在"); + } + // tt_announcement 表本身没有 read 列,read 仅在 /list 的 SQL 里通过 LEFT JOIN tt_announcement_read 计算; + // 此处按主键直查不会带回 read。但服务方法内部已写入 tt_announcement_read,调用后对当前用户必然为已读, + // 因此显式置为 "1",与 /list 的语义保持一致。 + ttAnnouncement.setRead("1"); + return AjaxResult.success(toItem(ttAnnouncement)); + } + + // source=2 通知:返回通知详情同时自动将 tt_notice.read 置为 1 + if (Integer.valueOf(2).equals(request.getSource())) { + ApiNoticeVO noticeVO = apiNoticeService.getNoticeByNoticeId(userId, request.getId()); + if (noticeVO == null) { + return AjaxResult.error("公告不存在"); + } + return AjaxResult.success(toItem(noticeVO)); + } + + return AjaxResult.error("source 取值必须为 1(公告)或 2(通知)"); + } + + /** 公告 → 统一展示项 */ + private MessageItemVO toItem(TtAnnouncement a) { + MessageItemVO vo = new MessageItemVO(); + vo.setId(a.getAnnouncementId()); + vo.setSource(1); + vo.setTitle(a.getTitle()); + vo.setContent(a.getContent()); + vo.setCategory(a.getCategory()); + vo.setTabType(resolveTabType(a.getCategory())); + vo.setRead(a.getRead()); + vo.setCreateTime(a.getCreateTime()); + return vo; + } + + /** 通知 → 统一展示项 */ + private MessageItemVO toItem(ApiNoticeVO n) { + MessageItemVO vo = new MessageItemVO(); + vo.setId(n.getNoticeId()); + vo.setSource(2); + vo.setTitle(n.getTitle()); + vo.setContent(n.getContent()); + vo.setCategory(n.getCategory()); + vo.setTabType(resolveTabType(n.getCategory())); + vo.setRead(n.getRead()); + vo.setCreateTime(n.getCreateTime()); + return vo; + } + + /** 根据 category 前缀推导 Tab 类型:GAME_ 为游戏(1),其他为系统(0) */ + private Integer resolveTabType(String category) { + if (category != null && category.toUpperCase().startsWith("GAME_")) { + return 1; + } + return 0; + } + + @ApiOperation("获取未读公告数量(含未读通知)") + @PostMapping("/countUnreadAnnouncement") + public AjaxResult countUnreadAnnouncement() { + Long userId = SecurityUtils.getUserId(); + int announcementUnread = apiAnnouncementService.countUnreadAnnouncement(userId); + int noticeUnread = apiNoticeService.countUnreadNotice(userId); + return AjaxResult.success(announcementUnread + noticeUnread); + } + + @ApiOperation("批量设置指定分类的通知为已读(游戏/系统)") + @PostMapping("/markAsReadByCategory") + public AjaxResult markNoticesAsReadByCategory(@RequestBody MarkAsReadRequest request) { + int result = apiNoticeService.markNoticesAsReadByCategory(request.getCategoryPrefix()); + return AjaxResult.success(result); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiBlendErcashController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiBlendErcashController.java new file mode 100644 index 0000000..9b15a7a --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiBlendErcashController.java @@ -0,0 +1,53 @@ +package com.ruoyi.user.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.service.TtUserBlendErcashService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.ChildDetailByBoss; +import com.ruoyi.domain.vo.ChildDetailByBossVO; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.service.ApiShoppingService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Api(tags = "用户收支记录") +@RestController +@RequestMapping("/api/blendErcash") +public class ApiBlendErcashController extends BaseController { + + @Autowired + private TtUserBlendErcashService blendErcashService; + + @Autowired + private ISysConfigService sysConfigService; + + @Autowired + private ApiShoppingService apiShoppingService; + + @Autowired + private TtUserService userService; + + // 下级消费明细 + @ApiOperation("粉丝消费明细") + @PostMapping("/childDetailByBoss") + // @Anonymous + public R> childDetailByBoss(@RequestBody @Validated ChildDetailByBoss param){ + + Long userId = getUserId(); + if (ObjectUtil.isEmpty(userId)) return R.fail(401,"登录过期"); + + param.setBossId(Integer.valueOf(userId.toString())); + return blendErcashService.childDetailByBoss(param); + + } + +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiBonusController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiBonusController.java new file mode 100644 index 0000000..d0f441c --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiBonusController.java @@ -0,0 +1,67 @@ +package com.ruoyi.user.controller; + +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.contract.api.request.ReceiveRedPacketRequest; +import com.ruoyi.user.contract.api.response.QueryVipRewardResponse; +import com.ruoyi.user.contract.api.response.VipFlowRewardResponse; +import com.ruoyi.user.service.ApiBonusService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +@Api(tags = "福利补贴") +@RestController +@RequestMapping("/api/bonus") +public class ApiBonusController extends BaseController { + + private final ISysConfigService sysConfigService; + private final ApiBonusService apiBonusService; + private final TtUserService userService; + + public ApiBonusController(ISysConfigService sysConfigService, + ApiBonusService apiBonusService, + TtUserService userService) { + this.sysConfigService = sysConfigService; + this.apiBonusService = apiBonusService; + this.userService = userService; + } + + @ApiOperation("领取红包") + @UpdateUserCache + @PostMapping("/receiveRedPacket") + public R receiveRedPacket(@RequestBody ReceiveRedPacketRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + return apiBonusService.receiveRedPacket(request.getCode(), ttUser); + } + + @ApiOperation("查询vip流水奖励") + @PostMapping("/vipreward/list") + public R vipRewardList() { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + return apiBonusService.vipRewardList(ttUser); + } + + @ApiOperation("VIP流水奖励(精简版)") + @PostMapping("/vipFlowReward") + public R vipFlowReward() { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + return apiBonusService.vipFlowReward(ttUser); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiCaptchaController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiCaptchaController.java new file mode 100644 index 0000000..2d31e19 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiCaptchaController.java @@ -0,0 +1,99 @@ +package com.ruoyi.user.controller; + +import cloud.tianai.captcha.application.ImageCaptchaApplication; +import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.generator.common.model.dto.GenerateParam; +import cloud.tianai.captcha.common.response.ApiResponse; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.other.ApiCaptchaCheckRequest; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.TimeUnit; + +/** + * 滑块验证码 Controller + * POST /api/captcha/gen - 生成验证码 + * POST /api/captcha/check - 校验验证码,通过后在 Redis 写入标记供后续短信/密码登录使用 + */ +@Api(tags = "滑块验证码") +@Slf4j +@RestController +@RequestMapping("/api") +public class ApiCaptchaController extends BaseController { + + @Autowired + private ImageCaptchaApplication application; + @Autowired + private RedisCache redisCache; + + /** 滑块位置容差,0.3 表示允许 30% 偏差 */ + private static final float SLIDER_TOLERANT = 0.05f; + + /** + * 生成滑块验证码 + */ + @ApiOperation("生成滑块验证码") + @Anonymous + @PostMapping("/captcha/gen") + public AjaxResult genCaptcha() { + long start = System.currentTimeMillis(); + + GenerateParam generateParam = new GenerateParam(); + generateParam.setType(CaptchaTypeConstant.SLIDER); + // 通过官方 API 设置容差,库内部生成和校验都会使用此值 + generateParam.addParam("tolerant", SLIDER_TOLERANT); + + ApiResponse response = application.generateCaptcha(generateParam); + + long cost = System.currentTimeMillis() - start; + if (cost > 500) { + log.warn("[验证码生成] 耗时 {}ms(较慢,请检查 local-cache 是否生效)", cost); + } else { + log.info("[验证码生成] 耗时 {}ms", cost); + } + + if (!response.isSuccess()) { + return AjaxResult.error("验证码生成失败"); + } + return AjaxResult.success(response.getData()); + } + + /** + * 校验滑块验证码 + * 通过后在 Redis 写入 captcha:verified:{phoneNumber} = captchaId,有效期 5 分钟 + */ + @ApiOperation("校验滑块验证码") + @Anonymous + @PostMapping("/captcha/check") + public AjaxResult checkCaptcha(@RequestBody ApiCaptchaCheckRequest request) { + if (StringUtils.isEmpty(request.getId())) { + return AjaxResult.error("验证码ID不能为空"); + } + if (StringUtils.isEmpty(request.getPhoneNumber())) { + return AjaxResult.error("手机号不能为空"); + } + + ApiResponse checkResponse = application.matching(request.getId(), request.getData()); + if (!checkResponse.isSuccess()) { + log.warn("[滑块验证] 验证失败, captchaId={}, phone={}", request.getId(), request.getPhoneNumber()); + return AjaxResult.error("滑块验证失败,请重试"); + } + + // 验证通过,写入 Redis 标记,5 分钟内可用于发短信或密码登录 + String verifyKey = "captcha:verified:" + request.getPhoneNumber(); + redisCache.setCacheObject(verifyKey, request.getId(), 5, TimeUnit.MINUTES); + + return AjaxResult.success("验证通过"); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiLoginController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiLoginController.java new file mode 100644 index 0000000..0f782d3 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiLoginController.java @@ -0,0 +1,216 @@ +package com.ruoyi.user.controller; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.entity.sys.TtPromotionUpdate; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.NewUserInfo; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.entity.UserData; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.ApiLoginBody; +import com.ruoyi.domain.other.ApiVerificationCodeLoginBody; +import com.ruoyi.domain.other.ApiVerificationCodeLoginOrRegisterBody; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.service.ApiLoginService; +import com.ruoyi.user.service.ApiUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * 登录Controller + */ +@Api(tags = "客户端用户认证") +@RestController +@RequestMapping("/api") +public class ApiLoginController extends BaseController { + + private final ISysConfigService sysConfigService; + private final ApiLoginService loginService; + private final TokenService tokenService; + @Autowired + private TtUserMapper userMapper; + public ApiLoginController(ISysConfigService sysConfigService, + ApiLoginService loginService, + TokenService tokenService) { + this.sysConfigService = sysConfigService; + this.loginService = loginService; + this.tokenService = tokenService; + } + + @Autowired + private ApiUserService apiUserService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtPromotionUpdateMapper ttPromotionUpdateMapper; + + @ApiOperation("客户端用户登录") + @Anonymous + @PostMapping("/login") + public AjaxResult login(@RequestBody @Validated ApiLoginBody loginBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return AjaxResult.error("网站维护中......"); + } + try { + var loginUser = getLoginUser(); + if (Objects.nonNull(loginUser)) { + String userName = loginUser.getUsername(); + tokenService.delLoginUser(loginUser.getToken()); + AsyncManager.me().execute(AsyncFactory.recordLogininfor("api_" + userName, Constants.LOGOUT, "退出成功")); + } + + } catch (Exception e) { + + } + + AjaxResult ajax = AjaxResult.success(); + Pair pair = loginService.login(loginBody.getUsername(), loginBody.getPassword()); + if (pair.getValue() != null) { + return pair.getValue(); + } + ajax.put(Constants.TOKEN, pair.getKey()); + return ajax; + } + + @ApiOperation("客户端用户验证码登录") + @Anonymous + @PostMapping("/verificationCodeLogin") + public AjaxResult verificationCodeLogin(@RequestBody @Validated ApiVerificationCodeLoginBody apiVerificationCodeLoginBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return AjaxResult.error("网站维护中......"); + } + return loginService.verificationCodeLogin(apiVerificationCodeLoginBody.getPhoneNumber(), apiVerificationCodeLoginBody.getCode()); + } + + @ApiOperation("客户端用户验证码登录或注册") + @Anonymous + @PostMapping("/verificationCodeLoginOrRegister") + public AjaxResult verificationCodeLoginOrRegister(@RequestBody @Validated ApiVerificationCodeLoginOrRegisterBody req) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return AjaxResult.error("网站维护中......"); + } + return loginService.verificationCodeLoginOrRegister(req.getPhoneNumber(), req.getCode(), req.getParentInvitationCode()); + } + + + @ApiOperation("获取用户信息") + @NewUserInfo + @PostMapping("/getInfo") + public R getInfo() { + UserData userData = getLoginUser().getUserData(); + TtUser ttUser = userService.getById(getUserId()); + apiUserService.authenticationOk(ttUser); + if (!"1".equals(userData.getIsRealCheck())) { + userData.setRealName(""); + } + userData.setAccountAmount(ttUser.getAccountAmount()); + userData.setAccountCredits(ttUser.getAccountCredits()); + userData.setDeliveryAddress(ttUser.getDeliveryAddress()); + // 获取下级总人数 + userData.setSubordinateNumber(apiUserService.getSubordinateNumber(getUserId())); + userData.setInvitationCode(ttUser.getInvitationCode()); + userData.setVipLevel(convertToDisplayVipLevel(ttUser.getVipLevel())); + return R.ok(userData); + } + + @ApiOperation("获取用户余额信息") + @NewUserInfo + @PostMapping("/getAccountAmount") + public R getAccountAmount() { + UserData userData = getLoginUser().getUserData(); + TtUser ttUser = userService.getById(getUserId()); + userData.setAccountAmount(ttUser.getAccountAmount()); + userData.setAccountCredits(ttUser.getAccountCredits()); + userData.setDeliveryAddress(ttUser.getDeliveryAddress()); + userData.setInvitationCode(ttUser.getInvitationCode()); + userData.setVipLevel(convertToDisplayVipLevel(ttUser.getVipLevel())); + userData.setPassword(""); + return R.ok(userData); + } + + private Integer convertToDisplayVipLevel(Integer dbVipLevel) { + return dbVipLevel != null && dbVipLevel > 0 ? dbVipLevel - 1 : 0; + } + + @ApiOperation("获取用户信息") + @NewUserInfo + @PostMapping("/getParentInfo") + public R> getParentInfo() { + TtUser ttUser = userService.getById(getUserId()); + if (ttUser.getParentId() == null) { + return R.ok(); + } + var parent = userService.getById(ttUser.getParentId()); + if (parent == null) { + return R.ok(); + } + + // 推荐时间:优先取 tt_promotion_update.create_time(注册时绑定即落库), + // 没有记录的兜底为当前用户的 createTime(用户的注册时间)。 + Date recommendTime = null; + TtPromotionUpdate promotion = ttPromotionUpdateMapper.selectOne( + Wrappers.lambdaQuery() + .eq(TtPromotionUpdate::getEmployeeId, ttUser.getUserId()) + .eq(TtPromotionUpdate::getBossId, parent.getUserId()) + .orderByAsc(TtPromotionUpdate::getCreateTime) + .last("limit 1")); + if (promotion != null && promotion.getCreateTime() != null) { + Timestamp ts = promotion.getCreateTime(); + recommendTime = new Date(ts.getTime()); + } else if (ttUser.getCreateTime() != null) { + recommendTime = ttUser.getCreateTime(); + } + + Map r = new HashMap<>(); + r.put("userId", parent.getUserId()); + r.put("avatar", parent.getAvatar()); + r.put("nickName", parent.getNickName()); + r.put("invitationCode", parent.getInvitationCode()); + r.put("recommendTime", recommendTime == null + ? null + : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(recommendTime)); + return R.ok(r); + } + + @ApiOperation("客户端用户退出") + @PostMapping("/logout") + public R logout() { + if (StringUtils.isNotNull(getLoginUser())) { + String userName = getUsername(); + tokenService.delLoginUser(getToken()); + AsyncManager.me().execute(AsyncFactory.recordLogininfor("api_" + userName, Constants.LOGOUT, "退出成功")); + } + return R.ok("退出成功!"); + } + +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiMessageController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiMessageController.java new file mode 100644 index 0000000..f2498a2 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiMessageController.java @@ -0,0 +1,67 @@ +package com.ruoyi.user.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.ruoyi.admin.service.TtMessageService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.other.TtMessage; +import com.ruoyi.user.contract.api.request.BatchOperationMessageRequest; +import com.ruoyi.user.contract.api.request.MessageRequest; +import com.ruoyi.user.service.ApiMessageService; +import com.ruoyi.domain.vo.ApiMessageDataVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@Api(tags = "消息") +@RestController +@RequestMapping("/api/message") +public class ApiMessageController extends BaseController { + + private final ApiMessageService apiMessageService; + + public ApiMessageController(ApiMessageService apiMessageService) { + this.apiMessageService = apiMessageService; + } + + @Autowired + private TtMessageService ttMessageService; + + @PostMapping("/list") + public PageDataInfo list() { + startPage(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.orderByDesc("id"); + List list = ttMessageService.list(queryWrapper); + return getPageData(list); + } + + @ApiOperation("获取消息列表") + @PostMapping("/getMessageList") + public PageDataInfo getMessageList(@RequestBody(required = false) MessageRequest request){ + startPage(); + List list = apiMessageService.getMessageList(getUserId(), request.getId()); + return getPageData(list); + } + + @ApiOperation("已读Message") + @PostMapping("/view") + public R view(@RequestBody MessageRequest request){ + ApiMessageDataVO data = apiMessageService.view(getUserId(), request.getId()); + return R.ok(data); + } + + @ApiOperation("分批操作Message") + @PostMapping("/batchOperation") + public R batchOperation(@RequestBody BatchOperationMessageRequest request){ + String msg = apiMessageService.batchOperation(getUserId(), Arrays.asList(request.getIds()), request.getStatus()); + return StringUtils.isEmpty(msg) ? R.ok(true, "操作成功!") : R.fail(false, msg); + } + +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiNoticeController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiNoticeController.java new file mode 100644 index 0000000..a6be0bc --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiNoticeController.java @@ -0,0 +1,57 @@ +package com.ruoyi.user.controller; + + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.other.TtNotice; +import com.ruoyi.user.contract.api.request.GetNoticeRequest; +import com.ruoyi.user.model.vo.ApiNoticeVO; +import com.ruoyi.user.service.ApiNoticeService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Objects; + +@Api(tags = "通知") +@RestController +@RequestMapping("/api/notice") +public class ApiNoticeController extends BaseController { + + @Autowired + private ApiNoticeService apiNoticeService; + + @ApiOperation("获取通知列表") + @ApiResponse(code = 200, message = "read【阅读状态(0未读 1已读)】") + @PostMapping("/list") + public TableDataInfo list() { + startPage(); + Long userId = SecurityUtils.getUserId(); + List apiNoticeVOList = apiNoticeService.getNoticeList(userId); + return getDataTable(apiNoticeVOList); + } + + @ApiOperation("获取通知详情") + @PostMapping("/detail") + public AjaxResult getNoticeByNoticeId(@RequestBody GetNoticeRequest request) { + Long userId = SecurityUtils.getUserId(); + ApiNoticeVO apiNoticeVO = apiNoticeService.getNoticeByNoticeId(userId, request.getNoticeId()); + if (Objects.isNull(apiNoticeVO)) { + return AjaxResult.error("通知不存在"); + } + return AjaxResult.success(apiNoticeVO); + } + + @ApiOperation("获取未读通知数量") + @PostMapping("/countUnreadNotice") + public AjaxResult countUnreadNotice() { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(apiNoticeService.countUnreadNotice(userId)); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiOrderController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiOrderController.java new file mode 100644 index 0000000..49163c6 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiOrderController.java @@ -0,0 +1,30 @@ +package com.ruoyi.user.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.ruoyi.admin.service.TtOrderService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.user.contract.api.request.RechargeDetailsRequest; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + + +@Api(tags = "订单信息") +@RestController +@RequestMapping("/api/order") +public class ApiOrderController extends BaseController { + + @Autowired + private TtOrderService ttOrderService; + + @ApiOperation("充值明细") + @PostMapping("/list") + public R list(@RequestBody RechargeDetailsRequest request) { + Long userId = getUserId(); + if (ObjectUtil.isNull(userId)) return R.fail("登录过期。"); + return ttOrderService.clientList(request.getPage(), request.getSize(), userId.intValue()); + } + +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiPromotionRecordController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiPromotionRecordController.java new file mode 100644 index 0000000..174cbbf --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiPromotionRecordController.java @@ -0,0 +1,30 @@ +package com.ruoyi.user.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.domain.vo.promotion.TtPromotionRecordVo; +import com.ruoyi.user.service.ApiPromotionRecordService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/promotionRecord") +public class ApiPromotionRecordController extends BaseController { + + @Autowired + private ApiPromotionRecordService apiPromotionRecordService; + + @PostMapping("/getPromotionRecord") + public PageDataInfo getPromotionRecord(){ + startPage(); + Long userId = getUserId(); + List list = apiPromotionRecordService.getPromotionRecord(userId.intValue()); + return getPageData(list); + } + +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiRegisterController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiRegisterController.java new file mode 100644 index 0000000..f24e9a1 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiRegisterController.java @@ -0,0 +1,43 @@ +package com.ruoyi.user.controller; + +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.service.ApiRegisterService; +import com.ruoyi.domain.other.ApiRegisterBody; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Api(tags = "用户注册") +@RestController +@RequestMapping("/api/register") +public class ApiRegisterController extends BaseController { + + private final ISysConfigService sysConfigService; + private final ApiRegisterService apiRegisterService; + + public ApiRegisterController(ISysConfigService sysConfigService, + ApiRegisterService apiRegisterService) { + this.sysConfigService = sysConfigService; + this.apiRegisterService = apiRegisterService; + } + + @ApiOperation("用户注册") + @PostMapping + @Anonymous + public R register(@RequestBody @Validated ApiRegisterBody registerBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String msg = apiRegisterService.register(registerBody); + return StringUtils.isEmpty(msg) ? R.ok("注册成功!") : R.fail(msg); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiShareController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiShareController.java new file mode 100644 index 0000000..ccf4225 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiShareController.java @@ -0,0 +1,47 @@ +package com.ruoyi.user.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.share.ShareRecordQueryDTO; +import com.ruoyi.domain.vo.share.ShareCenterVO; +import com.ruoyi.domain.vo.share.ShareRecordPageVO; +import com.ruoyi.user.service.ApiShareService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 客户端 - 主页"游戏分享"模块 + */ +@Api(tags = "客户端-游戏分享") +@RestController +@RequestMapping("/api/share") +public class ApiShareController extends BaseController { + + @Autowired + private ApiShareService apiShareService; + + /** + * 分享中心顶部数据:邀请码、今日收益、累计收益、邀请人数、分享链接 + */ + @ApiOperation("获取分享中心顶部数据") + @PostMapping("/center") + public R getCenter() { + return R.ok(apiShareService.getShareCenter(getUserId())); + } + + /** + * 分享记录列表(支持按下级用户ID搜索) + * + * @param query 查询参数:subUserId(可选)、pageNum、pageSize(均由前端传入) + */ + @ApiOperation("查询分享记录列表") + @PostMapping("/records") + public R records(@RequestBody ShareRecordQueryDTO query) { + return R.ok(apiShareService.listShareRecords(getUserId(), query.getSubUserId(), query.getPageNum(), query.getPageSize())); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiShoppingController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiShoppingController.java new file mode 100644 index 0000000..9f1df9f --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiShoppingController.java @@ -0,0 +1,153 @@ +package com.ruoyi.user.controller; + +import com.github.pagehelper.PageHelper; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RateLimiter; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.enums.LimitType; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.contract.api.request.ExchangeOrnamentsId; +import com.ruoyi.user.contract.api.request.GetShoppingValueRequest; +import com.ruoyi.user.contract.api.request.IntegratingConversionRequest; +import com.ruoyi.user.service.ApiShoppingService; +import com.ruoyi.domain.other.ApiShoppingBody; +import com.ruoyi.domain.vo.ApiShoppingDataVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Api(tags = "商城") +@Slf4j +@RestController +@RequestMapping("/api/shopping") +public class ApiShoppingController extends BaseController { + + private final ISysConfigService sysConfigService; + private final ApiShoppingService apiShoppingService; + private final TtUserService userService; + + @org.springframework.beans.factory.annotation.Autowired + private RedisLock redisLock; + + public ApiShoppingController(ISysConfigService sysConfigService, + ApiShoppingService apiShoppingService, + TtUserService userService) { + this.sysConfigService = sysConfigService; + this.apiShoppingService = apiShoppingService; + this.userService = userService; + } + + @ApiOperation("获取不同类型的商品") + @PostMapping("/getShoppingQuery") + public R>> getShoppingQuery(@RequestBody GetShoppingValueRequest request){ + List dictCache = null; + String value = request.getValue(); + if ("0".equals(value)) dictCache = DictUtils.getDictCache("ornaments_type_name"); + if ("1".equals(value)) dictCache = DictUtils.getDictCache("ornaments_rarity_name"); + if ("2".equals(value)) dictCache = DictUtils.getDictCache("ornaments_exterior_name"); + if ("3".equals(value)) dictCache = DictUtils.getDictCache("ornaments_quality_name"); + if (StringUtils.isNull(dictCache)) return R.fail("数据异常!!"); + List> list = dictCache.stream().map(sysDictData -> { + Map map = new HashMap<>(); + map.put(sysDictData.getDictLabel(), sysDictData.getDictValue()); + return map; + }).collect(Collectors.toList()); + return R.ok(list); + } + + @ApiOperation("商品列表") + @PostMapping("/list") + public PageDataInfo list(@RequestBody @Validated ApiShoppingBody shoppingBody) { + // 仅显示可兑换:必须拿到当前登录用户的积分才能过滤。 + // 未登录且勾选了开关:视为 0 积分,过滤后大概率为空,避免误导用户以为都能兑换。 + // 未登录且未勾选:userCredits=null,service 层不会加过滤。 + BigDecimal userCredits = null; + if (Boolean.TRUE.equals(shoppingBody.getIsExchange())) { + try { + TtUser ttUser = userService.getById(getUserId()); + userCredits = (ttUser != null && ttUser.getAccountCredits() != null) + ? ttUser.getAccountCredits() + : BigDecimal.ZERO; + } catch (Exception ignore) { + // 未登录 / token 失效,按 0 积分处理 + userCredits = BigDecimal.ZERO; + } + } + PageHelper.startPage(shoppingBody.getPageNum(), shoppingBody.getPageSize()); + List list = apiShoppingService.list(shoppingBody, userCredits); + return getPageData(list); + } + + @ApiOperation("商品兑换") + @PostMapping("/exchange") + @RateLimiter(time = 60, count = 30, limitType = LimitType.IP) + @UpdateUserCache + public R exchange(@RequestBody ExchangeOrnamentsId exchangeOrnamentsId) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String shoppingMaintenance = sysConfigService.selectConfigByKey("shoppingMaintenance"); + if ("1".equals(shoppingMaintenance)) { + return R.fail("商城功能正在维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + // 控制层防重锁:按 userId 串行,锁包裹整个 @Transactional 事务,解锁晚于提交, + // 避免弱网重复提交导致重复扣费/重复发放(锁粒度=单用户,不影响其他用户) + String lockKey = RedisConstants.SHOPPING_EXCHANGE_LOCK + ttUser.getUserId(); + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + log.warn("商城兑换加锁失败, userId={}", ttUser.getUserId()); + return R.fail("购买进行中,请勿重复操作"); + } + try { + // P1#2: 锁内重新读取用户信息,避免 TOCTOU 竞态 + ttUser = userService.getById(ttUser.getUserId()); + return apiShoppingService.exchange(ttUser, exchangeOrnamentsId.getOrnamentsIds()); + } finally { + redisLock.unlock(lockKey); + } + } + + @ApiOperation("积分转换") + @UpdateUserCache + @RateLimiter(time = 60, count = 30, limitType = LimitType.IP) + @PostMapping("/integratingConversion") + public R integratingConversion(@RequestBody IntegratingConversionRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......"); + TtUser ttUser = userService.getById(getUserId()); + // 控制层防重锁:按 userId 串行,锁包裹整个 @Transactional 事务, + // 避免弱网重复提交导致重复转换(锁粒度=单用户,不影响其他用户) + String lockKey = RedisConstants.SHOPPING_INTEGRATING_CONVERSION_LOCK + ttUser.getUserId(); + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + return R.fail("积分转换处理中,请勿重复操作"); + } + try { + String msg = apiShoppingService.integratingConversion(ttUser, request.getCredits()); + return StringUtils.isEmpty(msg) ? R.ok(true, "积分转换成功!") : R.fail(msg); + } finally { + redisLock.unlock(lockKey); + } + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiTaskCenterController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiTaskCenterController.java new file mode 100644 index 0000000..6edde30 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiTaskCenterController.java @@ -0,0 +1,55 @@ +package com.ruoyi.user.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.user.contract.api.request.GetRewardRequest; +import com.ruoyi.user.model.TtTaskCenterUser; +import com.ruoyi.user.model.vo.ApiTaskCenterVO; +import com.ruoyi.user.service.ApiTaskCenterService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Objects; + +@Api(tags = "任务中心") +@RestController +@RequestMapping("/api/taskCenter") +public class ApiTaskCenterController extends BaseController { + + @Autowired + private ApiTaskCenterService apiTaskCenterService; + + @ApiOperation("查询任务列表") + @ApiResponse(code = 200, message = "status【领取条件(0不满足 1满足)】" + + "
claimed【是否已领取(1已领取)】") + @PreAuthorize("isAuthenticated()") + @PostMapping("/list") + public TableDataInfo list() { + startPage(); + Long userId = getUserId(); + List list = apiTaskCenterService.selectApiTaskCenterVOList(userId.intValue()); + return getDataTable(list); + } + + @ApiOperation("领取任务奖励") + @PreAuthorize("isAuthenticated()") + @PostMapping("/getReward") + public AjaxResult getReward(@RequestBody GetRewardRequest request) { + Long userId = getUserId(); + // 判断是否具备领取条件 + TtTaskCenterUser ttTaskCenterUser = apiTaskCenterService.selectTtTaskCenterUserByTaskIdAndUserId(request.getTaskId(), userId.intValue()); + if (Objects.isNull(ttTaskCenterUser)) { + return AjaxResult.error("不具备领取条件"); + } + if ("1".equals(ttTaskCenterUser.getClaimed())) { + return AjaxResult.error("已领取过"); + } + return apiTaskCenterService.getReward(request.getTaskId(), userId.intValue()); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserAmountRecordsController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserAmountRecordsController.java new file mode 100644 index 0000000..c4c000b --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserAmountRecordsController.java @@ -0,0 +1,657 @@ +package com.ruoyi.user.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtDeliveryRecordService; +import com.ruoyi.admin.service.TtOrderService; +import com.ruoyi.admin.service.TtRechargeRankingRewardService; +import com.ruoyi.admin.service.TtUserAmountRecordsService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.MoneyUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.dto.sys.TeamUsersParam; +import com.ruoyi.domain.dto.userRecord.AmountRecordsDetailCondition; +import com.ruoyi.domain.dto.userRecord.DeliveryRecordsConfition; +import com.ruoyi.domain.dto.userRecord.OrderCondition; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.other.TtRechargeRankingReward; +import com.ruoyi.domain.vo.TeamDetailVO; +import com.ruoyi.domain.vo.TtUserAccountRecordsRankVO; +import com.ruoyi.domain.vo.TtUserAmountRecords.UserAmountDetailVO; +import com.ruoyi.domain.vo.UserBERankVO; +import com.ruoyi.domain.vo.delivery.DeliveryRecordVO; +import com.ruoyi.domain.vo.order.TtOrderVO; +import com.ruoyi.domain.vo.sys.SimpleTtUserVO; +import com.ruoyi.user.contract.api.request.AmountRankRequest; +import com.ruoyi.user.contract.api.request.PWelfareRecordsRequest; +import com.ruoyi.user.contract.api.request.PersonalRecordRequest; +import com.ruoyi.user.contract.api.response.PersonalRecordResponse; +import com.ruoyi.user.mapper.ApiUserBlendErcashMapper; +import com.ruoyi.user.model.vo.ApiConsumeRankingVO; +import com.ruoyi.user.model.vo.ApiRechargeRankingVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.sql.Timestamp; +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtAccountRecordSource.RECEIVE_RED_PACKET; +import static com.ruoyi.domain.common.constant.TtAccountRecordSource.RECHARGE; + +@Api(tags = "用户充值记录") +@RestController +@RequestMapping("/api/userAmountRecords") +@Slf4j +public class ApiUserAmountRecordsController extends BaseController { + + @Autowired + private TtUserAmountRecordsService ttUserAmountRecordsService; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + + @Autowired + private TtUserAmountRecordsService userAmountRecordsService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserMapper userMapper; + + @Autowired + private TtOrderService orderService; + + @Autowired + private TtDeliveryRecordService ttDeliveryRecordService; + + @Autowired + private TtOrderService ttOrderService; + + @Autowired + private TtPromotionUpdateMapper ttPromotionUpdateMapper; + + @Autowired + private ApiUserBlendErcashMapper apiUserBlendErcashMapper; + + @Autowired + private TtRechargeRankingRewardService ttRechargeRankingRewardService; + + /** + * 充值网流水排行榜 + * 1今天 2昨天 3近一周 + */ + @ApiOperation("充值网流水排行榜") + @Anonymous + @PostMapping("/amountRank") + public Page amountRank(@RequestBody AmountRankRequest request) { + + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + Timestamp end = null; + Timestamp begin = null; + + if (request.getType().equals(1)) { + begin = new Timestamp(c.getTimeInMillis()); + end = new Timestamp(System.currentTimeMillis()); + } else if (request.getType().equals(2)) { + end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.DAY_OF_MONTH, -1); + begin = new Timestamp(c.getTimeInMillis()); + } else if (request.getType().equals(3)) { + end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.WEEK_OF_MONTH, -1); + begin = new Timestamp(c.getTimeInMillis()); + } + + return ttUserAmountRecordsService.rank(begin, end, request.getPage(), request.getSize()); + } + + /** + * 充值网推广奖励明细 + */ + @ApiOperation("充值网推广奖励明细") + @PostMapping("/pWelfareRecords") + public R pWelfareRecords(@RequestBody PWelfareRecordsRequest request) { + if (request.getPage() < 1) { + return R.fail("page > 0"); + } + if (request.getSize() > 21) { + return R.fail("size <= 20 !!!"); + } + Long userId = getUserId(); + return ttUserAmountRecordsService.pWelfareRecords(userId.intValue(), request.getPage(), request.getSize()); + } + + /** + * 综合排行榜 + */ + @ApiOperation("流水排行榜") + @PostMapping("/blendErcashRank") + public R> blendErcashRank(@RequestBody RankParam param) { + param.setPage(1); + param.setSize(10); + + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + Timestamp end = null; + Timestamp begin = null; + + if (param.getType().equals(1)) { + begin = new Timestamp(c.getTimeInMillis()); + end = new Timestamp(System.currentTimeMillis()); + } else if (param.getType().equals(2)) { + end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.DAY_OF_MONTH, -1); + begin = new Timestamp(c.getTimeInMillis()); + } else if (param.getType().equals(3)) { + end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.WEEK_OF_MONTH, -1); + begin = new Timestamp(c.getTimeInMillis()); + } + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String beginT = dateFormat.format(begin); + String endT = dateFormat.format(end); + if (StringUtils.isBlank(beginT) || StringUtils.isBlank(beginT)) { + beginT = null; + endT = null; + } + + List source = new ArrayList<>(TtAccountRecordSource.getGameConsumeCodes()); + Integer limit = (param.getPage() - 1) * param.getSize(); + List rank = ttUserBlendErcashMapper.rank( + source, + beginT, + endT, + limit, + param.getSize()); + for (int i = 1; i <= rank.size(); i++) { + var item = rank.get(i - 1); + item.setBeRank(i); + item.setValue(MoneyUtil.toStr(item.getAmount().abs())); + } + return R.ok(rank); + } + + @ApiOperation("获取个人综合收支明细") + @PostMapping("/userAccountDetail") + public R> userAccountDetail(@RequestBody @Validated AmountRecordsDetailCondition param) { + param.setUserId(null); + + param.setUserId(getUserId().intValue()); + List list = userAmountRecordsService.userAccountDetail(param); + return R.ok(list); + } + + @ApiOperation("获取个人记录") + @PostMapping("/personalRecord") + public R queryPersonalRecord(@RequestBody @Validated PersonalRecordRequest request) { + final int TYPE_TODAY = 1; + final int TYPE_YESTERDAY = 2; + final int TYPE_LAST_7_DAYS = 3; + final int SOURCE_RECHARGE = 1; + final int SOURCE_CONSUME = 2; + final int SOURCE_RED_PACKET = 3; + final int Filtering_Options_OutPut = 0; + final int Filtering_Options_Input = 1; + + if (request.getType() != TYPE_TODAY && request.getType() != TYPE_YESTERDAY && request.getType() != TYPE_LAST_7_DAYS) { + return R.fail("参数错误: type 无效"); + } + if (request.getSource() != SOURCE_RECHARGE && request.getSource() != SOURCE_CONSUME && request.getSource() != SOURCE_RED_PACKET) { + return R.fail("参数错误: source 无效"); + } + Integer uid = getUserId().intValue(); + + + Page page = new Page<>(request.getPage(), request.getPageSize()); +// page.setOptimizeCountSql(false); + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(TtUserBlendErcash::getUserId, uid); + + // 1:今日 2:昨日 3:最近7天 + LocalDate today = LocalDate.now(); + switch (request.getType()) { + case TYPE_TODAY: + LocalDateTime startOfToday = today.atStartOfDay(); + LocalDateTime endOfToday = today.plusDays(1).atStartOfDay().minusNanos(1); + wrapper.between(TtUserBlendErcash::getCreateTime, startOfToday, endOfToday); + break; + case TYPE_YESTERDAY: + LocalDateTime startOfYesterday = today.minusDays(1).atStartOfDay(); + LocalDateTime endOfYesterday = today.atStartOfDay().minusNanos(1); + wrapper.between(TtUserBlendErcash::getCreateTime, startOfYesterday, endOfYesterday); + break; + case TYPE_LAST_7_DAYS: + LocalDateTime startOf7Days = today.minusDays(6).atStartOfDay(); + wrapper.ge(TtUserBlendErcash::getCreateTime, startOf7Days); + break; + default: + // 理论上不会进入,因为上面已经校验过 + break; + } + + + // 查询的数据源 1:充值 2:全部/支出/收入 3:红包 + switch (request.getSource()) { + case SOURCE_RECHARGE: + wrapper.eq(TtUserBlendErcash::getSource, RECHARGE.getCode()); + break; + case SOURCE_CONSUME: + //wrapper.eq(TtUserBlendErcash::getType, TtAccountRecordType.OUTPUT); + if (request.getFilteringOptions() != null) { + if (request.getFilteringOptions() == Filtering_Options_OutPut) { + wrapper.eq(TtUserBlendErcash::getType, TtAccountRecordType.OUTPUT.getCode()); + } else if (request.getFilteringOptions() == Filtering_Options_Input) { + wrapper.eq(TtUserBlendErcash::getType, TtAccountRecordType.INPUT.getCode()); + } + } + break; + case SOURCE_RED_PACKET: + wrapper.eq(TtUserBlendErcash::getSource, RECEIVE_RED_PACKET.getCode()); + break; + default: + break; + } + + wrapper.orderByDesc(TtUserBlendErcash::getCreateTime); + IPage listPage = ttUserBlendErcashMapper.selectPage(page, wrapper); + + List userList = listPage.getRecords(); + + var formatter = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss"); + var r = userList.stream().map(v -> { + PersonalRecordResponse.RecordVO item = new PersonalRecordResponse.RecordVO(); + item.setAmount(MoneyUtil.toStr(v.getAmount())); + item.setFinalAmount(MoneyUtil.toStr(v.getFinalAmount())); + item.setRemark(v.getRemark()); + item.setDatetime(v.getCreateTime().toLocalDateTime().format(formatter)); + item.setSource(v.getSource()); + return item; + }).collect(Collectors.toList()); + PersonalRecordResponse response = new PersonalRecordResponse(); + response.setRows(r); + response.setTotal(listPage.getTotal()); + + return R.ok(response); + } + + @ApiOperation("获取个人提货记录") + @PostMapping("/deliveryRecords") + public R deliveryRecords(@RequestBody @Validated DeliveryRecordsConfition param) { + param.setUIdList(null); + int userId = getUserId().intValue(); + if (ObjectUtil.isNull(userId)) return R.fail(401, "登录过期请重新登录。"); + param.setUIdList(Arrays.asList(userId)); + + List list = ttDeliveryRecordService.byCondition(param); + + return R.ok(list); + } + + @ApiOperation("获取团队用户列表") + // @Anonymous + @PostMapping("/teamUsers") + public R> teamUsers(@RequestBody @Validated TeamUsersParam param) { + param.setBossIds(null); + param.setEmployeeIds(null); + param.setBossIds(Arrays.asList(getUserId().intValue())); + if (ObjectUtil.isNull(param.getOrderByFie())) param.setOrderByFie(1); + + // 查询所有下级id + List allEmployeesId = userMapper.allEmployeesByParents(param.getBossIds()); + if (allEmployeesId.isEmpty()) { + return R.ok(new ArrayList<>()); + } + + // 解析时间 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date beginTime = null; + try { + beginTime = dateFormat.parse(param.getBeginTime()); + } catch (ParseException e) { + return R.fail("日期解析异常,检查日期格式是否正确。"); + } + + // main + if (param.getOrderByFie().equals(1)) { + + // 查询下级的最近一个绑定时间 + List mit = ttPromotionUpdateMapper.latelyUpdate(allEmployeesId); + if (mit.size() < allEmployeesId.size()) log.warn("下级已绑定上级,但未写入更新日志,请及时检查!!!"); + // 如果最近绑定时间大于本次查询的起始时间,以最近绑定时间为准 + List empIds1 = new ArrayList<>(); + List empIds2 = new ArrayList<>(); + for (TeamDetailVO item : mit) { + Timestamp latelyTime = item.getBeginTime(); + if (latelyTime.compareTo(new Timestamp(beginTime.getTime())) > 0) { + empIds2.add(item); + } else { + empIds1.add(item.getEmployeeId()); + } + } + + List batchRechargeTotal1 = new ArrayList<>(); + if (!empIds1.isEmpty()) { + // 主表充值统计 + batchRechargeTotal1 = orderService.batchRechargeTotal( + empIds1, + param.getBeginTime(), + param.getEndTime(), + param.getOrderType(), + param.getPage(), + param.getSize()); + System.err.println(batchRechargeTotal1); + } + + List batchRechargeTotal2 = new ArrayList<>(); + if (!empIds2.isEmpty()) { + + for (TeamDetailVO vo : empIds2) { + + String beginT = dateFormat.format(vo.getBeginTime()); + + List data = orderService.batchRechargeTotal( + Arrays.asList(vo.getEmployeeId()), + beginT, + param.getEndTime(), + param.getOrderType(), + param.getPage(), + param.getSize()); + + batchRechargeTotal2.addAll(data); + } + } + + batchRechargeTotal1.addAll(batchRechargeTotal2); + return R.ok(batchRechargeTotal1); + + } else if (param.getOrderByFie().equals(2) || param.getOrderByFie().equals(3) || param.getOrderByFie().equals(4)) { + if (StringUtils.isBlank(param.getBeginTime())) { + param.setBeginTime(null); + } + if (StringUtils.isBlank(param.getEndTime())) { + param.setEndTime(null); + } + if (ObjectUtil.isNull(param.getOrderType())) { + param.setOrderType(1); + } + + // 查询下级的最近一个绑定时间 + List mit = ttPromotionUpdateMapper.latelyUpdate(allEmployeesId); + if (mit.size() < allEmployeesId.size()) { + log.warn("下级已绑定上级,但未写入更新日志,请及时检查!!!"); + } + // 如果最近绑定时间大于本次查询的起始时间,以最近绑定时间为准 + List empIds1 = new ArrayList<>(); + List empIds2 = new ArrayList<>(); + for (TeamDetailVO item : mit) { + Timestamp latelyTime = item.getBeginTime(); + if (latelyTime.compareTo(new Timestamp(beginTime.getTime())) > 0) { + empIds2.add(item); + } else { + empIds1.add(item.getEmployeeId()); + } + } + + List batchConsumeTotal1 = new ArrayList<>(); + if (!empIds1.isEmpty()) { + // 主表消费统计 + batchConsumeTotal1 = ttUserBlendErcashMapper.batchConsumeTotal( + empIds1, + param.getBeginTime(), + param.getEndTime(), + param.getOrderByFie(), + param.getOrderType(), + (param.getPage() - 1) * param.getSize(), + param.getSize()); + } + + List batchConsumeTotal2 = new ArrayList<>(); + if (!empIds2.isEmpty()) { + for (TeamDetailVO vo : empIds2) { + + String beginT = dateFormat.format(vo.getBeginTime()); + + List data = ttUserBlendErcashMapper.batchConsumeTotal( + Arrays.asList(vo.getEmployeeId()), + beginT, + param.getEndTime(), + param.getOrderByFie(), + param.getOrderType(), + (param.getPage() - 1) * param.getSize(), + param.getSize() + ); + + batchConsumeTotal2.addAll(data); + } + } + + batchConsumeTotal1.addAll(batchConsumeTotal2); + + return R.ok(batchConsumeTotal1); + + } else { + return R.fail("非法的排序字段"); + } + + // return userService.teamUsers(param); + } + + @ApiOperation("获取个人充值明细") + @PostMapping("/rechargeRecords") + public R rechargeRecords(@RequestBody @Validated OrderCondition param) { + Long userId = getUserId(); + if (ObjectUtil.isNull(userId)) return R.fail(401, "登录过期,请重新登录。"); + + param.setUserIdList(Arrays.asList(userId.intValue())); + + List list = ttOrderService.byCondition(param); + return R.ok(list); + } + + @Deprecated + @ApiOperation("获取消费排行榜") + @Anonymous + @PostMapping("/rechargeRanking") + public AjaxResult getRechargeRanking() { + // 创建日期格式化对象,指定日期格式 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + + // 获取今天日期 + Date today = new Date(); + String formattedTodayDate = formatter.format(today); + List todayFightRanking = apiUserBlendErcashMapper.getRechargeRankingByDate(formattedTodayDate); + + // 获取昨天日期 + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -1); + Date yesterday = cal.getTime(); + String formattedYesterdayDate = formatter.format(yesterday); + List yesterdayFightRanking = apiUserBlendErcashMapper.getRechargeRankingByDate(formattedYesterdayDate); + + Map> map = new HashMap<>(); + map.put("todayFightRanking", todayFightRanking); + map.put("yesterdayFightRanking", yesterdayFightRanking); + + return AjaxResult.success(map); + } + + @ApiOperation("消费排行榜奖励说明") + @PostMapping("/rechargeRankingRewardsIntroduction") + public AjaxResult rechargeRankingRewardsIntroduction() { + List ttRechargeRankingRewards = ttRechargeRankingRewardService.selectTtRechargeRankingRewardList(null); + ttRechargeRankingRewards.forEach(r -> r.setRewardStr(MoneyUtil.toStr(r.getReward()))); + return success(ttRechargeRankingRewards); + } + + /** + * 今日消费流水排行榜 Top10。 + * 统计口径:tt_user_blend_ercash 中 type=0(OUTPUT)且 source 命中 + *排除机器人 (user_type='03'),按总消费降序,返回前 10。 + */ + @ApiOperation("今日消费流水排行榜 Top10") + @Anonymous + @PostMapping("/consumeRanking/top10") + public R> consumeRankingTop10() { + // 今日起止时间 + LocalDate today = LocalDate.now(); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String beginT = today.atStartOfDay().format(fmt); + String endT = today.plusDays(1).atStartOfDay().minusNanos(1).format(fmt); + + List sourceList = new ArrayList<>(TtAccountRecordSource.getGameBetCodes()); + List list = apiUserBlendErcashMapper.topConsumeRanking(beginT, endT, sourceList, 10); + if (list == null) list = new ArrayList<>(); + + // 填充格式化金额 与 各 TOP 的奖励金额 + for (ApiConsumeRankingVO vo : list) { + vo.setTotalConsumeStr(MoneyUtil.toStr(vo.getTotalConsume())); + BigDecimal reward = BigDecimal.ZERO; + try { + int rankInt = Integer.parseInt(vo.getRank()); + TtRechargeRankingReward cfg = ttRechargeRankingRewardService.selectTtRechargeRankingRewardById(rankInt); + if (cfg != null && cfg.getReward() != null) { + reward = cfg.getReward(); + } + } catch (NumberFormatException ignore) { + // rank 不是数字(理论上 Top10 不会出现),奖励保持 0 + } + vo.setReward(reward); + vo.setRewardStr(MoneyUtil.toStr(reward)); + } + return R.ok(list); + } + + /** + * 今日消费流水排行榜中【当前登录用户】的个人排名。 + */ + @ApiOperation("今日消费流水个人排名") + @PostMapping("/consumeRanking/myRank") + public R consumeRankingMyRank() { + Long userId = getUserId(); + if (ObjectUtil.isNull(userId)) { + return R.fail(401, "登录过期,请重新登录。"); + } + + com.ruoyi.domain.entity.sys.TtUser user = userService.getById(userId); + + LocalDate today = LocalDate.now(); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String beginT = today.atStartOfDay().format(fmt); + String endT = today.plusDays(1).atStartOfDay().minusNanos(1).format(fmt); + + List sourceList = new ArrayList<>(TtAccountRecordSource.getGameBetCodes()); + + // 个人今日总消费 + BigDecimal myTotal = apiUserBlendErcashMapper.userConsumeTotal(userId, beginT, endT, sourceList); + if (myTotal == null) myTotal = BigDecimal.ZERO; + + ApiConsumeRankingVO vo = new ApiConsumeRankingVO(); + vo.setUserId(userId); + if (user != null) { + vo.setNickName(user.getNickName()); + vo.setAvatar(user.getAvatar()); + } + vo.setTotalConsume(myTotal); + vo.setTotalConsumeStr(MoneyUtil.toStr(myTotal)); + + // 今日无消费直接不上榜 + if (myTotal.compareTo(BigDecimal.ZERO) <= 0) { + vo.setRank("---"); + vo.setReward(BigDecimal.ZERO); + vo.setRewardStr(MoneyUtil.toStr(BigDecimal.ZERO)); + return R.ok(vo); + } + + // 排名 = 消费严格大于我的人数 + 1 + Integer ahead = apiUserBlendErcashMapper.countUsersAheadOf(myTotal, beginT, endT, sourceList); + int rank = (ahead == null ? 0 : ahead) + 1; + + if (rank > 10) { + vo.setRank("---"); + vo.setReward(BigDecimal.ZERO); + vo.setRewardStr(MoneyUtil.toStr(BigDecimal.ZERO)); + } else { + vo.setRank(String.valueOf(rank)); + TtRechargeRankingReward cfg = ttRechargeRankingRewardService.selectTtRechargeRankingRewardById(rank); + BigDecimal reward = (cfg != null && cfg.getReward() != null) ? cfg.getReward() : BigDecimal.ZERO; + vo.setReward(reward); + vo.setRewardStr(MoneyUtil.toStr(reward)); + } + return R.ok(vo); + } + + public R checkLogin() { + Long userId; + try { + userId = getUserId(); + if (ObjectUtil.isEmpty(userId)) AjaxResult.error(401, "登录过期,请重新登录。"); + return R.ok(userId); + } catch (Exception e) { + return R.fail(401, "登录过期,请重新登录。"); + } + } + + @NoArgsConstructor + @AllArgsConstructor + @Data + public static class RankParam { + + @ApiModelProperty("类型(1今天 2昨天 3近七天)") + @NotEmpty(message = "时间区间不能为空") + private Integer type; + @Min(value = 1, message = "最小1") + private Integer page; + @Min(value = 1, message = "最小1") + private Integer size; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserController.java new file mode 100644 index 0000000..220ed66 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserController.java @@ -0,0 +1,548 @@ +package com.ruoyi.user.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtOrderMapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.service.TtDeliveryRecordService; +import com.ruoyi.admin.service.TtUserAmountRecordsService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.service.TtVipLevelService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrder; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.domain.entity.recorde.TtUserAmountRecords; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.ApiForgetPasswordBody; +import com.ruoyi.domain.other.ApiSafePasswordBody; +import com.ruoyi.domain.other.ApiUpdateUserDetailsBody; +import com.ruoyi.domain.other.RealNameAuthenticationBody; +import com.ruoyi.domain.other.TtDeliveryRecordBody; +import com.ruoyi.domain.other.TtUserAmountRecordsBody; +import com.ruoyi.domain.other.TtVipLevel; +import com.ruoyi.domain.vo.TtDeliveryRecordDataVO; +import com.ruoyi.domain.vo.TtUserPackSackDataVO; +import com.ruoyi.domain.vo.TtUserVipLevelVo; +import com.ruoyi.domain.vo.TtUserVipProgressVo; +import com.ruoyi.domain.vo.VipLevelCountVo; +import com.ruoyi.domain.vo.user.TtUseChildInfoVo; +import com.ruoyi.domain.vo.user.TtUserInfoVo; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.baidu.service.BaiduService; +import com.ruoyi.user.contract.api.request.*; +import com.ruoyi.user.contract.api.response.PeakMomentResponse; +import com.ruoyi.user.service.ApiUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; +import java.util.stream.Collectors; + +// 解析 fightBisectPrize 配置(饰品ID数组 JSON) +import cn.hutool.json.JSONUtil; + +@Api(tags = "用户管理") +@RestController +@RequestMapping("/api/user") +@Slf4j +public class ApiUserController extends BaseController { + + private final ISysConfigService sysConfigService; + private final TtUserService userService; + private final ApiUserService apiUserService; + private final TtOrderMapper ttOrderMapper; + private final TtBoxRecordsMapper ttBoxRecordsMapper; + private final TtOrnamentMapper ttOrnamentMapper; + private final TtDeliveryRecordService ttDeliveryRecordService; + private final TtUserAmountRecordsService userAmountRecordsService; + private final TtDeliveryRecordService deliveryRecordService; + + public ApiUserController(ISysConfigService sysConfigService, + TtUserService userService, + TtDeliveryRecordService ttDeliveryRecordService, + ApiUserService apiUserService, TtOrderMapper ttOrderMapper, + TtBoxRecordsMapper ttBoxRecordsMapper, + TtOrnamentMapper ttOrnamentMapper, + TtUserAmountRecordsService userAmountRecordsService, + TtDeliveryRecordService deliveryRecordService) { + this.sysConfigService = sysConfigService; + this.userService = userService; + this.ttDeliveryRecordService = ttDeliveryRecordService; + this.apiUserService = apiUserService; + this.ttOrderMapper = ttOrderMapper; + this.ttBoxRecordsMapper = ttBoxRecordsMapper; + this.ttOrnamentMapper = ttOrnamentMapper; + this.userAmountRecordsService = userAmountRecordsService; + this.deliveryRecordService = deliveryRecordService; + } + + @Autowired + private TtVipLevelService vipLevelService; + + @Autowired + private BaiduService baiduService; + + @ApiOperation("头像上传") + @UpdateUserCache + @PostMapping(value = "/profilePictureUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R profilePictureUpload(@RequestPart("file") MultipartFile file) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + if (StringUtils.isNull(file)) { + return R.fail("未选择头像文件!"); + } + TtUser ttUser = userService.getById(getUserId()); + String msg = apiUserService.profilePictureUpload(ttUser, file); + return StringUtils.isEmpty(msg) ? R.ok("头像上传成功!") : R.fail(msg); + } + + @ApiOperation("更新个人信息") + @UpdateUserCache + @PostMapping("/updateUserDetails") + public R updateUserDetails(@RequestBody ApiUpdateUserDetailsBody updateUserDetailsBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + + String msg = apiUserService.updateUserDetails(ttUser, updateUserDetailsBody); + + return StringUtils.isEmpty(msg) ? R.ok("个人信息更新成功!") : R.fail(msg); + } + + @ApiOperation("更新收货地址") + @UpdateUserCache + @PostMapping("/updateDeliveryAddress") + public R updateDeliveryAddress(@RequestBody ApiUpdateUserDetailsBody updateUserDetailsBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + + String msg = apiUserService.updateUserDetails(ttUser, updateUserDetailsBody); + + return StringUtils.isEmpty(msg) ? R.ok("收货地址更新成功!") : R.fail(msg); + } + + @ApiOperation("绑定推广人") + @UpdateUserCache + @PostMapping("/bindBoss") + public R bindBoss(@RequestBody ApiUpdateUserDetailsBody updateUserDetailsBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + + return apiUserService.bindBoss(ttUser, updateUserDetailsBody); + } + + @ApiOperation("绑定邀请码") + @UpdateUserCache + @PostMapping("/bindInvitationCode") + public R bindInvitationCode(@RequestBody @Validated BindInvitationCodeRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + return apiUserService.bindInvitationCode(ttUser, request.getInvitationCode()); + } + + @ApiOperation("修改密码") + // @UpdateUserCache + @PostMapping("/changePW") + public R changePW(@RequestBody @Validated ApiUpdateUserDetailsBody updateUserDetailsBody, HttpServletRequest request) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + + String token = CacheConstants.LOGIN_TOKEN_KEY + getToken(); + + return apiUserService.changePW(ttUser, updateUserDetailsBody, token); + } + + @ApiOperation("忘记密码") + @PostMapping("/forgetPassword") + public R forgetPassword(@RequestBody ApiForgetPasswordBody apiForgetPasswordBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + String msg = apiUserService.forgetPassword(apiForgetPasswordBody); + return StringUtils.isEmpty(msg) ? R.ok(true, "密码修改成功!") : R.fail(msg); + } + + @ApiOperation("设置安全密码(提货校验用,与登录密码独立)") + @PostMapping("/setSafePassword") + public R setSafePassword(@RequestBody ApiSafePasswordBody apiSafePasswordBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + return apiUserService.setSafePassword(ttUser, apiSafePasswordBody); + } + + @ApiOperation("实名认证") + @RepeatSubmit(interval = 60000, message = "操作过于频繁,请60秒后重试!") + @UpdateUserCache + @PostMapping("/realNameAuthentication") + public R realNameAuthentication(@RequestBody RealNameAuthenticationBody realNameAuthenticationBody) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + TtUser ttUser = userService.getById(getUserId()); + String msg = apiUserService.realNameAuthentication2(ttUser, realNameAuthenticationBody.getRealName(), + realNameAuthenticationBody.getIdNum(), realNameAuthenticationBody.getPhoneNumber(), + realNameAuthenticationBody.getCaptcha()); + return msg.startsWith("alipays") ? R.ok(msg) : R.fail(msg); + } + + @ApiOperation("认证成功") + @UpdateUserCache + @PostMapping("/authenticationOk") + public R authenticationOk() { + TtUser ttUser = userService.getById(getUserId()); + String msg = apiUserService.authenticationOk(ttUser); + return StringUtils.isEmpty(msg) ? R.ok(true, "认证成功!") : R.fail(false, msg); + } + + @ApiOperation("获取下级人数") + // @UpdateUserCache + @PostMapping("/rechargeCount") + public R rechargeCount() { + TtUser ttUser = userService.getById(getUserId()); + return R.ok((int) userService.count(new QueryWrapper().eq("parent_id", ttUser.getUserId()))); + } + + @ApiOperation("获取下级提取数") + @UpdateUserCache + @PostMapping("/getExtracts") + public R getExtracts() { + TtUser ttUser = userService.getById(getUserId()); + List ttUsers = userService.list(new QueryWrapper().eq("parent_id", ttUser.getUserId())); + // 判断是否有下级用户 + if (ttUsers == null || ttUsers.size() == 0) { + return R.ok(new BigDecimal(0)); + } + List collect = ttUsers.stream().map(i -> i.getUserId()).collect(Collectors.toList()); + List ttDeliveryRecords = ttDeliveryRecordService.list(new QueryWrapper().in("user_id", collect).eq("status", "10")); + BigDecimal bigDecimal = new BigDecimal(0); + ttDeliveryRecords.forEach(o -> { + bigDecimal.add(o.getOrnamentsPrice()); + }); + + return R.ok(bigDecimal); + } + + @ApiOperation("获取下级充值总金额") + // @UpdateUserCache + @PostMapping("/getOrdersAmounts") + public R getOrdersAmounts() { + TtUser ttUser = userService.getById(getUserId()); + List ttUsers = userService.list(new QueryWrapper().eq("parent_id", ttUser.getUserId())); + // 判断是否有下级用户 + if (ttUsers == null || ttUsers.size() == 0) { + return R.ok(new BigDecimal(0)); + } + List collect = ttUsers.stream().map(i -> i.getUserId()).collect(Collectors.toList()); + List ttOrders = ttOrderMapper.selectList(new QueryWrapper().in("user_id", collect).and(k -> k.eq("status", "3").or().eq("status", "4"))); + + BigDecimal bigDecimal = new BigDecimal(0); + ttOrders.forEach(o -> { + bigDecimal.add(o.getGoodsPrice()); + }); + + return R.ok(bigDecimal); + } + + @ApiOperation("获取下级流水详情") + @PostMapping("/getLsjlList") + public PageDataInfo getLsjlList(@RequestBody TtUserAmountRecordsBody ttUserAmountRecordsBody) { + startPage(); + TtUser ttUser = userService.getById(getUserId()); + List ttUsers = userService.list(new QueryWrapper().eq("parent_id", ttUser.getUserId())); + // 判断是否有下级用户 + if (ttUsers == null || ttUsers.size() == 0) { + return getPageData(new ArrayList()); + } + List userIds = ttUsers.stream().map(i -> i.getUserId()).collect(Collectors.toList()); + QueryWrapper ttUserAmountRecordsQueryWrapper = new QueryWrapper<>(); + // 时间段筛选 + if (ttUserAmountRecordsBody.getStartTime() != null) { + ttUserAmountRecordsQueryWrapper.ge("create_time", ttUserAmountRecordsBody.getStartTime()); + } + if (ttUserAmountRecordsBody.getEndTime() != null) { + ttUserAmountRecordsQueryWrapper.lt("create_time", ttUserAmountRecordsBody.getEndTime()); + } + if (ttUserAmountRecordsBody.getUserId() != null) { + ttUserAmountRecordsQueryWrapper.eq("user_id", ttUserAmountRecordsBody.getUserId()); + } + + List list = userAmountRecordsService.list(ttUserAmountRecordsQueryWrapper.in("user_id", userIds).orderByDesc("create_time")); + + return getPageData(list); + } + + @ApiOperation("获取出货记录") + @PostMapping("/getDeliveryRecordList") + public PageDataInfo getDeliveryRecordList(@RequestBody TtDeliveryRecordBody deliveryRecordBody) { + startPage(); + TtUser ttUser = userService.getById(getUserId()); + deliveryRecordBody.setUserId(ttUser.getUserId()); + List list = deliveryRecordService.getDeliveryRecordByUserList(deliveryRecordBody); + return getPageData(list); + } + + /** + * 每日出货排行榜1今天 2昨天 3近一周 + */ + @ApiOperation("每日出货排行榜") + @PostMapping("/propRankOfDa") + public R propRankOfDay(@RequestBody PropRankOfDayRequest request) { + + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + + Timestamp end = null; + Timestamp begin = null; + if (request.getType().equals(1)) { + begin = new Timestamp(c.getTimeInMillis()); + end = new Timestamp(System.currentTimeMillis()); + } else if (request.getType().equals(2)) { + end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.DAY_OF_MONTH, -1); + begin = new Timestamp(c.getTimeInMillis()); + } else if (request.getType().equals(3)) { + end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.WEEK_OF_MONTH, -1); + begin = new Timestamp(c.getTimeInMillis()); + } + request.setNumber(10); + List list = userService.propRankOfDay(begin, end, request.getNumber()); + // return getPageData(list); + return R.ok(list); + } + + @ApiOperation("VIP等级说明") + @PostMapping("/vipLevelIntroduction") + public AjaxResult vipLevelIntroduction() { + List list = vipLevelService.list(); + return success(list); + } + + @ApiOperation("VIP等级转赠/提货次数配置") + @PostMapping("/vipLevelCounts") + public AjaxResult vipLevelCounts() { + List list = vipLevelService.list(); + List result = list.stream() + .sorted(Comparator.comparing(TtVipLevel::getId, + Comparator.nullsLast(Comparator.naturalOrder()))) + .map(v -> { + VipLevelCountVo vo = new VipLevelCountVo(); + vo.setId(v.getId()); + vo.setName(v.getName()); + vo.setTransferCount(v.getTransferCount() != null ? v.getTransferCount() : 0); + vo.setDeliveryCount(v.getDeliveryCount() != null ? v.getDeliveryCount() : 0); + return vo; + }) + .collect(Collectors.toList()); + return success(result); + } + + @ApiOperation("VIP等级进度") + @PostMapping("/userVipLeverRate") + public AjaxResult userVipLeverRate() { + TtUserVipLevelVo vipLevel = userService.userVipLeverRate(getUserId().intValue()); + return success(vipLevel); + } + + @ApiOperation("VIP等级经验进度(1元=1经验)") + @PostMapping("/vipProgress") + public AjaxResult vipProgress() { + TtUserVipProgressVo vo = userService.userVipProgress(getUserId().intValue()); + return success(vo); + } + + @ApiOperation("用户信息包含下级信息") + @PostMapping("/userInfo") + public AjaxResult userInfo() { + TtUserInfoVo userInfo = userService.userInfo(getUserId().intValue()); + return success(userInfo); + } + + @ApiOperation("用户信息包含下级用户信息") + @PostMapping("/userChildInfo") + public AjaxResult userChildInfo() { + List userInfo = userService.userChildInfos(getUserId().intValue()); + return success(userInfo); + } + + @ApiOperation("用户领取VIP等级(支持一次传多个等级一起领取)") + @UpdateUserCache + @PostMapping("/userVipLevelRechargeThreshold") + public AjaxResult userVipLevelRechargeThreshold(@RequestBody UserVipLevelRequest request) { + return userService.userVipLevelRechargeThreshold(getUserId().intValue(), request.getVipLevels()); + } + + @ApiOperation("用户已经领取VIP等级") + @PostMapping("/userHasVipLevelRechargeThreshold") + public AjaxResult userHasVipLevelRechargeThreshold() { + return userService.userHasVipLevelRechargeThreshold(getUserId().intValue()); + } + + @ApiOperation("检查是否有可领取的VIP等级红包") + @PostMapping("/checkVipLevelClaimable") + public AjaxResult checkVipLevelClaimable() { + return userService.checkVipLevelClaimable(getUserId().intValue()); + } + + @ApiOperation("用户领取充值门槛达标奖励") + @UpdateUserCache + @PostMapping("/userRechargeBonus") + public AjaxResult userRechargeBonus(@RequestBody UserRechargeBonusRequest request) { + return userService.userRechargeBonus(getUserId().intValue(), request.getRecharge(), request.getBonus()); + } + + @ApiOperation("用户已领取充值门槛达标奖励") + @PostMapping("/userHasRechargeBonus") + public AjaxResult userHasRechargeBonus() { + return userService.userHasRechargeBonus(getUserId().intValue()); + } + + @ApiOperation("用户积分兑换金币") + @UpdateUserCache + @PostMapping("/userCreditsToAmount") + public AjaxResult userCreditsToAmount(@RequestBody UserCreditsToAmountRequest request) { + return userService.userCreditsToAmount(getUserId().intValue(), request.getCredits()); + } + + @ApiOperation("回调百度接口") + @UpdateUserCache + @PostMapping("/baidu") + public AjaxResult baidu(@RequestBody BaiduCallbackRequest request) { + baiduService.upload(getUserId().intValue(), request.getNewType(), request.getBdVid()); + return AjaxResult.success(); + } + + @ApiOperation("巅峰时刻 - 高品质TOP10") + @PostMapping("/peakMoment") + public R peakMoment() { + // 获取当前登录用户信息 + TtUser ttUser = userService.getById(getUserId()); + + // 读取对战平分奖配置并排除:平分奖不属于"最高获胜饰品",不应上榜 + // fightBisectPrize 存的是饰品ID数组 JSON,如 [32131234354] + List bisectPrizeIds = parseBisectPrizeIds(); + + // 查询该用户获得的所有饰品,按价格降序取TOP10(排除对战平分奖饰品) + var chain = new LambdaQueryChainWrapper<>(ttBoxRecordsMapper) + .eq(TtBoxRecords::getHolderUserId, ttUser.getUserId()); + if (!bisectPrizeIds.isEmpty()) { + chain.notIn(TtBoxRecords::getOrnamentId, bisectPrizeIds); + } + List records = chain + .orderByDesc(TtBoxRecords::getOrnamentsPrice) + .last("LIMIT 10") + .list(); + + // 批量查询饰品信息,获取 exteriorName(外观名称)与 name(完整名称) + Map ornamentMap = Map.of(); + if (!records.isEmpty()) { + List ornamentIds = records.stream() + .map(TtBoxRecords::getOrnamentId) + .filter(id -> id != null) + .distinct() + .collect(Collectors.toList()); + if (!ornamentIds.isEmpty()) { + List ornaments = ttOrnamentMapper.selectByOrnamentIds(ornamentIds); + ornamentMap = ornaments.stream() + .collect(Collectors.toMap(TtOrnament::getId, o -> o, (a, b) -> a)); + } + } + + // 组装响应 + PeakMomentResponse response = new PeakMomentResponse(); + response.setUserId(ttUser.getUserId()); + response.setNickName(ttUser.getNickName()); + response.setAvatar(ttUser.getAvatar()); + + Map finalOrnamentMap = ornamentMap; + List items = records.stream().map(r -> { + TtOrnament ornament = r.getOrnamentId() != null ? finalOrnamentMap.get(r.getOrnamentId()) : null; + PeakMomentResponse.PeakItem item = new PeakMomentResponse.PeakItem(); + item.setId(r.getId()); + item.setOrnamentName(r.getOrnamentName()); + // 完整名称优先取 tt_ornament.name,缺失时回退背包记录里的名称 + item.setName(ornament != null && ornament.getName() != null && !ornament.getName().isEmpty() + ? ornament.getName() : r.getOrnamentName()); + item.setImageUrl(r.getImageUrl()); + item.setOrnamentLevelImg(r.getOrnamentLevelImg()); + item.setExteriorName(ornament != null && ornament.getExteriorName() != null + ? ornament.getExteriorName() : ""); + item.setOrnamentsPrice(r.getOrnamentsPrice()); + item.setSource(r.getSource()); + item.setCreateTime(r.getCreateTime()); + return item; + }).collect(Collectors.toList()); + + response.setItems(items); + return R.ok(response); + } + + /** + * 解析对战平分奖配置(fightBisectPrize)返回饰品ID列表; + * 未配置 / 无饰品 / 解析失败时返回空列表(不排除任何记录)。 + */ + private List parseBisectPrizeIds() { + try { + String cfg = sysConfigService.selectConfigByKey("fightBisectPrize"); + if (StringUtils.isBlank(cfg)) { + return Collections.emptyList(); + } + List ids = JSONUtil.parseArray(cfg).toList(Long.class); + return ids != null ? ids : Collections.emptyList(); + } catch (Exception e) { + log.warn("解析 fightBisectPrize 配置失败, 巅峰时刻不排除平分奖", e); + return Collections.emptyList(); + } + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserCreditsRecordsController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserCreditsRecordsController.java new file mode 100644 index 0000000..8dafdfd --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserCreditsRecordsController.java @@ -0,0 +1,72 @@ +package com.ruoyi.user.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.service.TtUserCreditsRecordsService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords; +import com.ruoyi.domain.vo.TtUserCreditsRecordsRankVO; +import com.ruoyi.user.contract.api.request.CreditsRankRequest; +import com.ruoyi.user.contract.api.request.PWelfareCreditsRecordsRequest; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.sql.Timestamp; +import java.util.Calendar; + +@Api(tags = "用户信用记录") +@RestController +@RequestMapping("/api/userCreditsRecords") +@Slf4j +public class ApiUserCreditsRecordsController extends BaseController { + + private final TtUserCreditsRecordsService ttUserCreditsRecordsService; + + public ApiUserCreditsRecordsController(TtUserCreditsRecordsService ttUserCreditsRecordsService) { + this.ttUserCreditsRecordsService = ttUserCreditsRecordsService; + } + + /** + * 积分排行榜 1今天 2昨天 3近一周 + */ + @ApiOperation("流水网流水排行榜") + @PostMapping("/creditsRank") + @Anonymous + public Page creditsRank(@RequestBody CreditsRankRequest request) { + + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY,0); + c.set(Calendar.MINUTE,0); + c.set(Calendar.SECOND,0); + c.set(Calendar.MILLISECOND,0); + Timestamp end = null; + Timestamp begin = null; + + if (request.getType().equals(1)) { + begin = new Timestamp(c.getTimeInMillis()); + end = new Timestamp(System.currentTimeMillis()); + } else if (request.getType().equals(2)) { + end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.DAY_OF_MONTH,-1); + begin = new Timestamp(c.getTimeInMillis()); + } else if (request.getType().equals(3)) { + end = new Timestamp(c.getTimeInMillis()); + c.add(Calendar.WEEK_OF_MONTH,-1); + begin = new Timestamp(c.getTimeInMillis()); + } + + return ttUserCreditsRecordsService.rank(begin, end, request.getPage(), request.getSize()); + } + + /** + * 流水网推广奖励明细 1、今天;2、昨天;3、近七天 + */ + @ApiOperation("流水网推广奖励明细") + @PostMapping("/pWelfareRecords") + public Page pWelfareRecords(@RequestBody PWelfareCreditsRecordsRequest request) { + Long userId = getUserId(); + return ttUserCreditsRecordsService.pWelfareRecords(Integer.valueOf(String.valueOf(userId)),request.getType(),request.getPage(),request.getSize()); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserPackSackController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserPackSackController.java new file mode 100644 index 0000000..2209e9a --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserPackSackController.java @@ -0,0 +1,343 @@ +package com.ruoyi.user.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.common.annotation.UpdateUserCache; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.PageDataInfo; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.PageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import org.springframework.validation.annotation.Validated; +import com.ruoyi.domain.dto.packSack.DecomposeLogCondition; +import com.ruoyi.domain.dto.packSack.DecomposeParam; +import com.ruoyi.domain.dto.packSack.DeliveryParam; +import com.ruoyi.domain.dto.packSack.PackSackCondition; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.BoxTransferRecord; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import com.ruoyi.domain.vo.TtBoxRecordsDataVO; +import com.ruoyi.domain.vo.UserPackSackDataVO; +import com.ruoyi.domain.vo.client.PackSackGlobalData; +import com.ruoyi.domain.vo.client.WelfareBoxVO; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.contract.api.request.QueryTransferRecordRequest; +import com.ruoyi.user.contract.api.response.QueryTransferRecordResponse; +import com.ruoyi.user.mapper.ApiBoxTransferRecordMapper; +import com.ruoyi.user.model.TransferParam; +import com.ruoyi.user.model.vo.ApiTransferRecordVO; +import com.ruoyi.user.service.ApiUserPackSackService; +import com.ruoyi.user.service.ApiUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Api(tags = "玩家背包") +@RestController +@RequestMapping("/api/userPackSack") +public class ApiUserPackSackController extends BaseController { + + /** + * 假号(白号 / 机器人)用户 id 子查询:白号 is_blank_account=1,机器人 user_type='03'。 + * 与游戏侧/后台统计的口径保持一致(那边普遍用 is_blank_account != 1 AND user_type != '03' 排除假号)。 + */ + private static final String FAKE_ACCOUNT_SQL = + "SELECT user_id FROM tt_user WHERE is_blank_account = 1 OR user_type = '03'"; + + private final ISysConfigService sysConfigService; + private final ApiUserPackSackService userPackSackService; + private final TtUserService userService; + + @Autowired + private ApiBoxTransferRecordMapper boxTransferRecordMapper; + + @Autowired + private RedisLock redisLock; + + @Autowired + private ApiUserService apiUserService; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private TtOrnamentsLevelMapper ttOrnamentsLevelMapper; + + public ApiUserPackSackController(ISysConfigService sysConfigService, + ApiUserPackSackService userPackSackService, + TtUserService userService) { + this.sysConfigService = sysConfigService; + this.userPackSackService = userPackSackService; + this.userService = userService; + } + + @ApiOperation("用户申请提货") + @PostMapping("/delivery") + public R delivery(@RequestBody @Validated DeliveryParam param) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + // 获取用户信息 + TtUser ttUser = userService.getById(getUserId()); + + // 假号(白号 is_blank_account=1 / 机器人 user_type=03)虚假提货: + // 生成已完成提货记录供 cocos 展示、饰品移出背包,不真正发货、后台不展示 + if (isFakeAccount(ttUser)) { + return userPackSackService.blankDelivery(param, ttUser); + } + + if ("1".equals(ttUser.getDeliveryStatus())) { + return R.fail(false, "您的帐号提货功能已被禁用,请联系管理员!"); + } + // 校验安全密码(用户在「安全密码」中单独设置,与登录密码无关) + if (StringUtils.isBlank(ttUser.getSafePassword())) { + return R.fail(false, "请先设置安全密码!"); + } + if (!SecurityUtils.matchesPassword(param.getTwoFactorAuth(), ttUser.getSafePassword())) { + return R.fail(false, "安全密码错误,提货失败!"); + } + // 未充值的玩家、主播不允许提货 + R rechargeCheck = checkWithdrawRecharge(ttUser); + if (rechargeCheck != null) { + return rechargeCheck; + } + return userPackSackService.delivery(param, ttUser); + } + + @ApiOperation("分解饰品") + @UpdateUserCache + @PostMapping("/decompose") + public R decompose(@RequestBody @Validated DecomposeParam param) { + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + if ("1".equals(websiteMaintenance)) { + return R.fail("网站维护中......"); + } + if (!param.getIsAll() && ObjectUtils.isEmpty(param.getPackSackIds())) { + return R.fail("请传入待分解饰品ID"); + } + TtUser ttUser = userService.getById(getUserId()); + int result = userPackSackService.decompose(param, ttUser); + return R.ok("成功分解" + result + "件饰品"); + } + + @ApiOperation("饰品分解记录") + //@UpdateUserCache + @PostMapping("/decomposeLog") + public PageDataInfo decomposeLog(@RequestBody @Validated DecomposeLogCondition param) { + param.setUserId(null); + param.setUserId(getUserId().intValue()); + // TtUser ttUser = userService.getById(getUserId()); + List ttBoxRecords = userPackSackService.decomposeLog(param); + return getPageData(ttBoxRecords); + } + + @ApiOperation("玩家背包") + @PostMapping("/getPackSack") + public R getPackSack(@RequestBody @Validated PackSackCondition condition) { + Long userId = getUserId(); + if (ObjectUtil.isNull(userId)) { + return R.fail("登录过期。"); + } + + // 根据背包类型设置查询条件 + Integer packType = condition.getPackType(); + java.util.Map result = new java.util.HashMap<>(); + if (packType != null && packType == 3) { + // 福利箱子:查询福利表数据(已取消分页) + List welfareList = userPackSackService.getWelfareBoxList(userId, null, null); + result.put("total", welfareList == null ? 0 : welfareList.size()); + result.put("list", welfareList == null ? java.util.Collections.emptyList() : welfareList); + return R.ok(result); + } + + condition.setUidList(Arrays.asList(userId.intValue())); + if (packType != null && packType == 2) { + // 提货背包:状态为申请提货 + condition.setStatusList(Arrays.asList(2)); + } else { + // 默认我的库存:同时展示「在背包」(0) 与「已申请提货」(2) + condition.setStatusList(Arrays.asList(0, 2)); + } + + // 我的库存/提货背包取消分页,一次返回全量,以免前端分页加载逻辑 bug 造成与顶部统计不一致 + PageUtils.clearPage(); + List packSackData = userPackSackService.clientPackSack(condition); + result.put("total", packSackData == null ? 0 : packSackData.size()); + result.put("list", packSackData == null ? java.util.Collections.emptyList() : packSackData); + return R.ok(result); + } + + @ApiOperation("玩家背包统计数据") + @PostMapping("/packSackGlobalData") + public R packSackGlobalData(@RequestBody(required = false) PackSackCondition condition) { + Long userId = getUserId(); + if (ObjectUtil.isNull(userId)) { + return R.fail("登录过期。"); + } + Integer packType = (condition == null ? null : condition.getPackType()); + return userPackSackService.packSackGlobalData(userId.intValue(), packType); + } + + @ApiOperation("饰品转赠") + @PostMapping({"/transfer"}) + public R transfer(@RequestBody @Validated TransferParam param) { + TtUser ttUser = this.userService.getById(this.getUserId()); + // 被禁用的用户不允许转赠 + if (!"0".equals(ttUser.getStatus())) { + return R.fail("账号已被禁用,无法转赠"); + } + // 假号(白号 / 机器人)虚假转赠:仅写转赠记录供 cocos 展示,不真正转移饰品,副号接收方查询会排除假号来源 + if (isFakeAccount(ttUser)) { + String blankResult = this.userPackSackService.blankTransfer(param, ttUser); + return StringUtils.isNotBlank(blankResult) ? R.fail(blankResult) : R.ok("转赠成功!"); + } + // 未充值的玩家、主播不允许转赠 + R rechargeCheck = checkWithdrawRecharge(ttUser); + if (rechargeCheck != null) { + return rechargeCheck; + } + // 控制层防重锁:按 userId 串行,锁包裹整个 @Transactional 事务,解锁晚于提交, + // 避免弱网重复提交导致重复生成转赠申请(锁粒度=单用户,不影响其他用户) + String lockKey = RedisConstants.TRANSFER_LOCK + ttUser.getUserId(); + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + return R.fail("转赠进行中,请勿重复操作"); + } + String result; + try { + result = this.userPackSackService.transfer(param, ttUser); + } finally { + redisLock.unlock(lockKey); + } + if (StringUtils.isBlank(result)) { + return R.ok("转赠成功!"); + } else if (result.startsWith("PENDING:")) { + return R.ok(result.substring(8)); + } else { + return R.fail(result); + } + } + + /** + * 是否为假号(白号 / 机器人):假号不参与真实资金链路 —— 提货/转赠一律走虚假流程 + * (只生成供 cocos 展示的记录,不发货、不转移饰品归属)。 + * 白号判定:is_blank_account=1;机器人判定:user_type='03'(与其它模块的假号口径一致)。 + */ + private static boolean isFakeAccount(TtUser ttUser) { + if (ttUser == null) return false; + return Integer.valueOf(1).equals(ttUser.getIsBlankAccount()) + || UserType.ROBOT_USER.getCode().equals(ttUser.getUserType()); + } + + /** + * 提货/转赠充值校验:仅对玩家、主播校验,未充值则不允许操作。 + * 校验通过返回 null,未通过返回错误 R(沿用 checkRecharge 的 703 业务码)。 + */ + private R checkWithdrawRecharge(TtUser ttUser) { + String userType = ttUser.getUserType(); + if (UserType.COMMON_USER.getCode().equals(userType) || UserType.ANCHOR.getCode().equals(userType)) { + R rechargeCheck = apiUserService.checkRecharge(ttUser.getUserId()); + if (rechargeCheck.getCode() != R.SUCCESS) { + return R.fail(rechargeCheck.getCode(), "未充值用户不允许提货、转赠"); + } + } + return null; + } + + @ApiOperation("饰品转赠记录") + @PostMapping({"/transfer/records"}) + public R queryTransferRecords(@RequestBody QueryTransferRecordRequest param) { + if (param.getPageNum() == null) { + param.setPageNum(1); + } + if (param.getPageSize() == null) { + param.setPageSize(10); + } + + var userId = this.getUserId(); + Page page = Page.of(param.getPageNum(), param.getPageSize()); + var query = new LambdaQueryChainWrapper<>(this.boxTransferRecordMapper); + if (Objects.equals("src", param.getKind())) { + query.eq(BoxTransferRecord::getSrcUid, userId); + } else { + query.eq(BoxTransferRecord::getDstUid, userId); + // 副号接收方不展示来自假号(白号 / 机器人)的转赠记录(假转赠) + query.notInSql(BoxTransferRecord::getSrcUid, FAKE_ACCOUNT_SQL); + } + var pageData = query.page(page); + + var records = pageData.getRecords().stream().map(v -> { + ApiTransferRecordVO vo = new ApiTransferRecordVO(); + BeanUtils.copyProperties(v, vo); + vo.setId(v.getId()); + vo.setSrcUid(v.getSrcUid()); + vo.setDstUid(v.getDstUid()); + vo.setRecordsId(v.getBoxRecordId()); + vo.setOrnamentImg(v.getImageUrl()); + // 显式设置状态字段 + vo.setStatus(v.getStatus()); + // BoxTransferRecord.createTime 为 LocalDateTime,需转成 Date 避免 Jackson 无 JSR310 模块导致序列化报错 + if (v.getCreateTime() != null) { + vo.setCreateTime(java.util.Date.from(v.getCreateTime() + .atZone(java.time.ZoneId.systemDefault()).toInstant())); + } + + // 补全:数据中饰品名称/图片/等级图片为空时,通过 ornamentId 查询再去补全 + if (v.getOrnamentId() != null && + (StringUtils.isBlank(v.getOrnamentName()) || StringUtils.isBlank(v.getImageUrl()))) { + TtOrnament ornament = new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .eq(TtOrnament::getId, v.getOrnamentId()) + .one(); + if (ornament != null) { + if (StringUtils.isBlank(v.getOrnamentName())) { + vo.setOrnamentName(ornament.getShortName()); + } + if (StringUtils.isBlank(v.getImageUrl())) { + vo.setOrnamentImg(ornament.getImageUrl()); + } + if (StringUtils.isBlank(v.getMarketHashName())) { + vo.setMarketHashName(ornament.getMarketHashName()); + } + // 补全等级图片:tt_ornament.exterior 对应 tt_ornaments_level.id + if (StringUtils.isBlank(v.getOrnamentLevelImg()) && StringUtils.isNotEmpty(ornament.getExterior())) { + Integer levelId = Integer.valueOf(ornament.getExterior()); + TtOrnamentsLevel level = new LambdaQueryChainWrapper<>(ttOrnamentsLevelMapper) + .eq(TtOrnamentsLevel::getId, levelId) + .one(); + if (level != null) { + vo.setOrnamentLevelImg(level.getLevelImg()); + } + } + } + } + + return vo; + }).collect(Collectors.toList()); + + QueryTransferRecordResponse response = new QueryTransferRecordResponse(); + response.setTotal(pageData.getTotal()); + response.setRows(records); + + return R.ok(response); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserProfileRecordsController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserProfileRecordsController.java new file mode 100644 index 0000000..ce39957 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserProfileRecordsController.java @@ -0,0 +1,43 @@ +package com.ruoyi.user.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.userRecord.ProfileRecordsRequest; +import com.ruoyi.domain.vo.userRecord.ProfileRecordsResponse; +import com.ruoyi.user.service.ApiProfileRecordsService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 个人资产详情 - 我的记录 聚合接口 + * 一个接口承载 4 个 Tab:余额/充值/提货/赠送。 + * 前端 4 个 Tab 共用同一份请求/响应类型,切换 tab 只改 tab 与 subFilter。 + */ +@Api(tags = "个人资产详情-我的记录") +@Slf4j +@RestController +@RequestMapping("/api/user/profile") +public class ApiUserProfileRecordsController extends BaseController { + + @Autowired + private ApiProfileRecordsService profileRecordsService; + + @ApiOperation("我的记录 聚合查询(余额/充值/提货/赠送)") + @PostMapping("/records") + public R queryRecords(@RequestBody @Validated ProfileRecordsRequest request) { + Long userId = getUserId(); + if (ObjectUtil.isNull(userId)) { + return R.fail(401, "登录过期,请重新登录。"); + } + ProfileRecordsResponse response = profileRecordsService.queryRecords(userId.intValue(), request); + return R.ok(response); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserSubAccountController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserSubAccountController.java new file mode 100644 index 0000000..521cbeb --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiUserSubAccountController.java @@ -0,0 +1,99 @@ +package com.ruoyi.user.controller; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.TtUserSubAccount; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.user.service.ApiUserSubAccountService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Api(tags = "副号管理") +@RestController +@RequestMapping("/api/subAccount") +public class ApiUserSubAccountController extends BaseController { + + @Autowired + private ApiUserSubAccountService subAccountService; + + @Autowired + private TtUserService userService; + + @Data + public static class BindSubAccountParam { + @NotNull(message = "副号ID不能为空") + private Long subUid; + } + + @Data + public static class SubAccountVO { + private Long id; + private Long subUid; + private String nickName; + private String avatar; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + } + + @ApiOperation("绑定副号") + @PostMapping("/bind") + public R bindSubAccount(@RequestBody BindSubAccountParam param) { + Long mainUid = getUserId(); + return subAccountService.bindSubAccount(mainUid, param.getSubUid()); + } + + @ApiOperation("解绑副号") + @PostMapping("/unbind") + public R unbindSubAccount(@RequestBody BindSubAccountParam param) { + Long mainUid = getUserId(); + return subAccountService.unbindSubAccount(mainUid, param.getSubUid()); + } + + @ApiOperation("查询已绑定的副号列表") + @PostMapping("/list") + public R> getSubAccounts() { + Long mainUid = getUserId(); + R> result = subAccountService.getSubAccounts(mainUid); + if (!result.getCode().equals(200)) { + return R.fail(result.getMsg()); + } + // 未绑定任何副号时,直接返回空列表,避免后续批量查询触发异常 + List records = result.getData(); + if (records == null || records.isEmpty()) { + return R.ok(Collections.emptyList()); + } + // 批量查询副号用户信息,避免 N+1 问题 + List subUidList = records.stream() + .map(TtUserSubAccount::getSubUid) + .collect(Collectors.toList()); + Map userMap = userService.listByIds(subUidList).stream() + .collect(Collectors.toMap(TtUser::getUserId, Function.identity())); + + List voList = records.stream().map(record -> { + SubAccountVO vo = new SubAccountVO(); + vo.setId(record.getId()); + vo.setSubUid(record.getSubUid()); + vo.setCreateTime(record.getCreateTime()); + TtUser subUser = userMap.get(record.getSubUid().intValue()); + if (subUser != null) { + vo.setNickName(subUser.getNickName()); + vo.setAvatar(subUser.getAvatar()); + } + return vo; + }).collect(Collectors.toList()); + return R.ok(voList); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiWebsiteSetupController.java b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiWebsiteSetupController.java new file mode 100644 index 0000000..2fb1201 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/controller/ApiWebsiteSetupController.java @@ -0,0 +1,73 @@ +package com.ruoyi.user.controller; + +import com.ruoyi.domain.other.TtBanner; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.contract.api.request.ContentByTypeRequest; +import com.ruoyi.user.service.ApiWebsiteSetupService; +import com.ruoyi.domain.vo.ApiContentDataVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Api(tags = "网站设置") +@RestController +@RequestMapping("/api/websiteSetup") +public class ApiWebsiteSetupController { + + private final ApiWebsiteSetupService apiWebsiteSetupService; + private final ISysConfigService sysConfigService; + + public ApiWebsiteSetupController(ApiWebsiteSetupService apiWebsiteSetupService, + ISysConfigService sysConfigService) { + this.apiWebsiteSetupService = apiWebsiteSetupService; + this.sysConfigService = sysConfigService; + } + + @ApiOperation("获取横幅列表") + @Anonymous + @PostMapping("/getBannerList") + public R> getBannerList() { + List list = apiWebsiteSetupService.getBannerList(); + return R.ok(list); + } + + @ApiOperation("获取类型内容") + @PostMapping("/getContentByType") + @Anonymous + public R getContentByType(@RequestBody ContentByTypeRequest request) { + ApiContentDataVO data = apiWebsiteSetupService.getContentByType(request.getAlias()); + return R.ok(data); + } + + /** + * 全站维护状态查询。 + *

供前端启动时/轮询调用:status=1 正常访问;status=0 全站维护中(前端展示全站维护页面)。 + *

响应禁用浏览器缓存,即使前端存在缓存也能实时拿到最新维护状态。 + */ + @ApiOperation("获取全站维护状态") + @Anonymous + @PostMapping("/siteMaintenance") + public R> getSiteMaintenanceStatus(HttpServletResponse response) { + // 禁用缓存,保证维护开关实时生效 + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0L); + + // sys_config 中 websiteMaintenance:"1"=维护中,"0"/其它=正常 + String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); + boolean maintaining = "1".equals(websiteMaintenance); + + Map data = new HashMap<>(4); + // status:0=全站维护中(展示维护页),1=正常访问 + data.put("status", maintaining ? 0 : 1); + data.put("message", maintaining ? "全站维护中,请稍后再试" : ""); + return R.ok(data); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiAnnouncementMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiAnnouncementMapper.java new file mode 100644 index 0000000..e810533 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiAnnouncementMapper.java @@ -0,0 +1,18 @@ +package com.ruoyi.user.mapper; + +import com.ruoyi.domain.other.TtAnnouncement; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ApiAnnouncementMapper { + + List getAnnouncementList(Long userId); + + List getSystemAnnouncementList(); + + TtAnnouncement getAnnouncementByAnnouncementId(Integer announcementId); + + int countUnreadAnnouncement(Long userId); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiAnnouncementReadMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiAnnouncementReadMapper.java new file mode 100644 index 0000000..861d534 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiAnnouncementReadMapper.java @@ -0,0 +1,12 @@ +package com.ruoyi.user.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface ApiAnnouncementReadMapper { + + int countAnnouncementRead(@Param("announcementId") Integer announcementId, @Param("userId") Long userId); + + int addAnnouncementRead(@Param("announcementId") Integer announcementId, @Param("userId") Long userId); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiBoxTransferRecordMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiBoxTransferRecordMapper.java new file mode 100644 index 0000000..68a54ac --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiBoxTransferRecordMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.user.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.domain.entity.BoxTransferRecord; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ApiBoxTransferRecordMapper extends BaseMapper { +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiMessageMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiMessageMapper.java new file mode 100644 index 0000000..4d6490d --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiMessageMapper.java @@ -0,0 +1,12 @@ +package com.ruoyi.user.mapper; + +import com.ruoyi.domain.vo.ApiMessageDataVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApiMessageMapper { + List getMessageList(@Param("userId") Long userId, @Param("id") Integer id); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiNoticeMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiNoticeMapper.java new file mode 100644 index 0000000..aa513f4 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiNoticeMapper.java @@ -0,0 +1,35 @@ +package com.ruoyi.user.mapper; + +import com.ruoyi.domain.other.TtNotice; +import com.ruoyi.user.model.vo.ApiNoticeVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApiNoticeMapper { + + List getNoticeList(Long userId); + + List getNoticeListByCategoryPrefix(@Param("userId") Long userId, @Param("categoryPrefix") String categoryPrefix); + + ApiNoticeVO getNoticeByNoticeId(@Param("userId") Long userId, @Param("noticeId") Integer noticeId); + + int countUnreadNotice(Long userId); + + int addNotice(TtNotice ttNotice); + + int editNotice(TtNotice ttNotice); + + int removeNoticeByNoticeId(Integer noticeId); + + /** + * 批量设置指定分类的通知为已读 + * + * @param userId 用户 ID + * @param categoryPrefix 分类前缀(GAME_表示游戏通知,SYS_表示系统通知) + * @return 影响行数 + */ + int markNoticesAsReadByCategory(@Param("userId") Long userId, @Param("categoryPrefix") String categoryPrefix); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiShoppingMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiShoppingMapper.java new file mode 100644 index 0000000..6eb3c13 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiShoppingMapper.java @@ -0,0 +1,13 @@ +package com.ruoyi.user.mapper; + +import com.ruoyi.domain.other.ApiShoppingBody; +import com.ruoyi.domain.vo.ApiShoppingDataVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApiShoppingMapper { + List list(@Param("shoppingBody") ApiShoppingBody shoppingBody, @Param("exchangePriceRatio") Integer exchangePriceRatio); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiTaskCenterMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiTaskCenterMapper.java new file mode 100644 index 0000000..a4604e1 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiTaskCenterMapper.java @@ -0,0 +1,31 @@ +package com.ruoyi.user.mapper; + +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.user.model.TtTaskCenterUser; +import com.ruoyi.user.model.dto.YesterdayExpenditureDTO; +import com.ruoyi.user.model.vo.ApiTaskCenterVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface ApiTaskCenterMapper { + + List selectApiTaskCenterVOList(Integer userId); + + List getYesterdayExpenditure(); + + BigDecimal selectCreditByTaskIdAndUserId(@Param("taskId") Integer taskId, @Param("userId") Integer userId); + + TtTaskCenterUser selectTtTaskCenterUserByTaskIdAndUserId(@Param("taskId") Integer taskId, @Param("userId") Integer userId); + + int insertYesterdayExpenditureBonusPoints(List ttTaskCenterUserList); + + int deleteYesterdayExpenditureBonusPoints(Integer taskId); + + int markAsClaimedByUserIdAndType(@Param("taskId") Integer taskId, @Param("userId") Integer userId); + + int insertTaskCenterRecord(TtTaskCenterUser ttTaskCenterUser); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiUserBlendErcashMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiUserBlendErcashMapper.java new file mode 100644 index 0000000..22e4376 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiUserBlendErcashMapper.java @@ -0,0 +1,52 @@ +package com.ruoyi.user.mapper; + +import com.ruoyi.user.model.vo.ApiConsumeRankingVO; +import com.ruoyi.user.model.vo.ApiRechargeRankingVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface ApiUserBlendErcashMapper { + + /** + * 获取指定日期的消费排行榜 + */ + List getRechargeRankingByDate(String date); + + /** + * 今日消费流水 Top10。 + * 统计口径:tt_user_blend_ercash 中 type=0 (OUTPUT) 且 source 命中 + * {@link com.ruoyi.domain.common.constant.TtAccountRecordSource#getGameBetCodes()}, + * 按用户聚合后按总消费降序,排除机器人 (user_type='03')。 + * + * @param beginT 今日 00:00:00 + * @param endT 今日 23:59:59 + * @param sourceList 计入消费的 source 集合 + * @param limit 返回条数(默认 10) + */ + List topConsumeRanking(@Param("beginT") String beginT, + @Param("endT") String endT, + @Param("sourceList") List sourceList, + @Param("limit") Integer limit); + + /** + * 查询指定用户在 [beginT, endT] 区间内、命中 sourceList 的累计消费(绝对值)。 + * 没有任何流水时返回 0。 + */ + BigDecimal userConsumeTotal(@Param("userId") Long userId, + @Param("beginT") String beginT, + @Param("endT") String endT, + @Param("sourceList") List sourceList); + + /** + * 统计在同样口径下消费总额严格大于 myTotal 的用户数(用于推算个人排名)。 + * 排名 = countUsersAheadOf(myTotal) + 1。 + */ + Integer countUsersAheadOf(@Param("myTotal") BigDecimal myTotal, + @Param("beginT") String beginT, + @Param("endT") String endT, + @Param("sourceList") List sourceList); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiUserPackSackMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiUserPackSackMapper.java new file mode 100644 index 0000000..3567c73 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/ApiUserPackSackMapper.java @@ -0,0 +1,33 @@ +package com.ruoyi.user.mapper; + +import com.ruoyi.domain.vo.UserPackSackDataVO; +import com.ruoyi.domain.vo.client.PackSackGlobalData; +import com.ruoyi.domain.vo.client.WelfareBoxVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApiUserPackSackMapper { + List getPackSack(Integer userId); + + List clientPackSack( + @Param("uidList") List uidList, + @Param("statusList") List statusList, + @Param("sourceList") List sourceList, + @Param("name") String name, + @Param("beginTime") String beginTime, + @Param("endTime") String endTime, + @Param("priceSort") Integer priceSort, + @Param("timeSort") Integer timeSort); + + PackSackGlobalData packSackGlobalData(@Param("userId") Integer userId, + @Param("statusList") List statusList); + + PackSackGlobalData welfareBoxGlobalData(@Param("userId") Integer userId); + + List getWelfareBoxList(@Param("userId") Long userId, + @Param("limit") Integer limit, + @Param("size") Integer size); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/PromoCdkeyUserMapper.java b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/PromoCdkeyUserMapper.java new file mode 100644 index 0000000..66cb324 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/mapper/PromoCdkeyUserMapper.java @@ -0,0 +1,53 @@ +package com.ruoyi.user.mapper; + +import org.apache.ibatis.annotations.*; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.Map; + +/** + * 推广CDKEY兑换 Mapper(service-user模块直接访问promo_cdkey/promo_cdkey_redeem表) + */ +@Mapper +public interface PromoCdkeyUserMapper { + + /** + * 根据CDKEY码查询进行中的CDKEY + */ + @Select("SELECT id, title, cdkey_code, amount_min, amount_max, num, usable_count, " + + "used_count, status, creator_id, creator_name, opening_time, validity, create_time " + + "FROM promo_cdkey WHERE cdkey_code = #{code} AND status = 0 LIMIT 1") + Map findActiveCdkey(@Param("code") String code); + + /** + * 递减可用次数 + */ + @Update("UPDATE promo_cdkey SET usable_count = usable_count - 1, used_count = used_count + 1 " + + "WHERE id = #{id} AND usable_count > 0") + int decrementUsableCount(@Param("id") Long id); + + /** + * 当可用次数为0时更新状态为已结束 + */ + @Update("UPDATE promo_cdkey SET status = 1 WHERE id = #{id} AND usable_count <= 0") + int updateStatusIfExhausted(@Param("id") Long id); + + /** + * 检查用户是否已兑换过该CDKEY + */ + @Select("SELECT COUNT(*) FROM promo_cdkey_redeem WHERE cdkey_id = #{cdkeyId} AND redeemer_id = #{redeemerId}") + int countUserRedeem(@Param("cdkeyId") Long cdkeyId, @Param("redeemerId") Long redeemerId); + + /** + * 插入兑换记录 + */ + @Insert("INSERT INTO promo_cdkey_redeem (cdkey_id, cdkey_code, redeemer_id, redeemer_name, unit_value, redeem_time) " + + "VALUES (#{cdkeyId}, #{cdkeyCode}, #{redeemerId}, #{redeemerName}, #{unitValue}, #{redeemTime})") + int insertRedeemRecord(@Param("cdkeyId") Long cdkeyId, + @Param("cdkeyCode") String cdkeyCode, + @Param("redeemerId") Long redeemerId, + @Param("redeemerName") String redeemerName, + @Param("unitValue") BigDecimal unitValue, + @Param("redeemTime") Date redeemTime); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/TransferParam.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/TransferParam.java new file mode 100644 index 0000000..9bb4714 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/TransferParam.java @@ -0,0 +1,16 @@ +package com.ruoyi.user.model; + +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@Data +public class TransferParam { + @NotEmpty(message = "饰品ID列表不能为空") + private List packSackIds; + + @NotNull(message = "目标用户ID不能为空") + private Integer targetUid; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/TtTaskCenterUser.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/TtTaskCenterUser.java new file mode 100644 index 0000000..a19dcf1 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/TtTaskCenterUser.java @@ -0,0 +1,13 @@ +package com.ruoyi.user.model; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TtTaskCenterUser { + private Integer taskId; + private Integer userId; + private BigDecimal credit; + private String claimed; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/dto/RealNameAuthenticationDTO.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/dto/RealNameAuthenticationDTO.java new file mode 100644 index 0000000..255c922 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/dto/RealNameAuthenticationDTO.java @@ -0,0 +1,13 @@ +package com.ruoyi.user.model.dto; + +import lombok.Data; + +@Data +public class RealNameAuthenticationDTO { + + private String realName; + + private String idNum; + + private String certifyId; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/dto/YesterdayExpenditureDTO.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/dto/YesterdayExpenditureDTO.java new file mode 100644 index 0000000..a9b6f36 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/dto/YesterdayExpenditureDTO.java @@ -0,0 +1,11 @@ +package com.ruoyi.user.model.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class YesterdayExpenditureDTO { + private Integer userId; + private BigDecimal totalRecharge; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiConsumeRankingVO.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiConsumeRankingVO.java new file mode 100644 index 0000000..0f29ad2 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiConsumeRankingVO.java @@ -0,0 +1,40 @@ +package com.ruoyi.user.model.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 今日消费流水排行榜(Top10 + 个人排名)共享 VO + *

+ * - Top10 接口:rank 为 "1"~"10" + * - 个人排名接口:rank 命中 Top10 时为 "1"~"10",未命中(排名超过 10 或今日无消费)时为 "-" + * - 奖励:来自后台 tt_recharge_ranking_reward;个人排名超过 10 时 reward = 0 + * - 总消费流水统计口径:参考 {@link com.ruoyi.domain.common.constant.TtAccountRecordSource#getGameBetCodes()} + */ +@Data +public class ApiConsumeRankingVO { + + /** 排名:"1"~"10";个人未上榜时为 "-" */ + private String rank; + + private Long userId; + + /** 昵称 */ + private String nickName; + + /** 头像 */ + private String avatar; + + /** 今日总消费流水(绝对值) */ + private BigDecimal totalConsume; + + /** 今日总消费流水(格式化字符串) */ + private String totalConsumeStr; + + /** 排名奖励金额 */ + private BigDecimal reward; + + /** 排名奖励金额(格式化字符串) */ + private String rewardStr; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiNoticeVO.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiNoticeVO.java new file mode 100644 index 0000000..c1ea590 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiNoticeVO.java @@ -0,0 +1,23 @@ +package com.ruoyi.user.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +@Data +public class ApiNoticeVO { + + private Integer noticeId; + + private String title; + + private String content; + + private String category; + + private String read; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiRechargeRankingVO.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiRechargeRankingVO.java new file mode 100644 index 0000000..04e076b --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiRechargeRankingVO.java @@ -0,0 +1,25 @@ +package com.ruoyi.user.model.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ApiRechargeRankingVO { + + private Integer ranking; + + private Long userId; + + private String nickName; + + private String avatar; + + /** 金币 */ + private BigDecimal amount; + + /** 积分 */ + private BigDecimal credits; + + private BigDecimal totalRecharge; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiTaskCenterVO.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiTaskCenterVO.java new file mode 100644 index 0000000..197b06a --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiTaskCenterVO.java @@ -0,0 +1,23 @@ +package com.ruoyi.user.model.vo; + +import lombok.Data; + +@Data +public class ApiTaskCenterVO { + + private Integer taskId; + + private String taskName; + + private String description; + + /** + * 领取条件(0不满足 1满足) + */ + private String status; + + /** + * 是否已领取(1已领取) + */ + private String claimed; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiTransferRecordVO.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiTransferRecordVO.java new file mode 100644 index 0000000..bdbd283 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/ApiTransferRecordVO.java @@ -0,0 +1,46 @@ +package com.ruoyi.user.model.vo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class ApiTransferRecordVO { + + @TableId + private Long id; + + private Long srcUid; + private Long dstUid; + + private Long recordsId; + + + private Long ornamentId; + + + private String ornamentName; + + + private String ornamentImg; + + + private String ornamentLevelImg; + + + private String marketHashName; + + + private BigDecimal ornamentsPrice; + + /** + * 转赠状态:0-审核中,1-成功,2-失败 + */ + private Integer status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createTime; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/MessageItemVO.java b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/MessageItemVO.java new file mode 100644 index 0000000..5029627 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/model/vo/MessageItemVO.java @@ -0,0 +1,41 @@ +package com.ruoyi.user.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +/** + * 公告/通知统一展示项 + * + * 用于 /api/announcement/list 与 /api/announcement/detail 接口, + * 将「公告(tt_announcement)」与「用户通知(tt_notice)」合并为同一种数据结构返回。 + */ +@Data +public class MessageItemVO { + + /** 主键ID */ + private Integer id; + + /** 数据来源:1 公告 / 2 通知 */ + private Integer source; + + /** 标题 */ + private String title; + + /** 内容 */ + private String content; + + /** 消息分类:GAME_xxx 或 SYS_xxx */ + private String category; + + /** Tab 类型:0 系统 / 1 游戏 */ + private Integer tabType; + + /** 阅读状态:0 未读 / 1 已读 */ + private String read; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiAnnouncementService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiAnnouncementService.java new file mode 100644 index 0000000..c686ba8 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiAnnouncementService.java @@ -0,0 +1,16 @@ +package com.ruoyi.user.service; + +import com.ruoyi.domain.other.TtAnnouncement; + +import java.util.List; + +public interface ApiAnnouncementService { + + List getAnnouncementList(Long userId); + + List getSystemAnnouncementList(); + + TtAnnouncement getAnnouncementByAnnouncementId(Integer announcementId, Long userId); + + int countUnreadAnnouncement(Long userId); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiBonusService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiBonusService.java new file mode 100644 index 0000000..141e499 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiBonusService.java @@ -0,0 +1,15 @@ +package com.ruoyi.user.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.user.contract.api.response.QueryVipRewardResponse; +import com.ruoyi.user.contract.api.response.VipFlowRewardResponse; + +public interface ApiBonusService { + + R receiveRedPacket(String code, TtUser ttUser); + + R vipRewardList(TtUser ttUser); + + R vipFlowReward(TtUser ttUser); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiLoginService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiLoginService.java new file mode 100644 index 0000000..cbc3c31 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiLoginService.java @@ -0,0 +1,13 @@ +package com.ruoyi.user.service; + +import com.ruoyi.common.core.domain.AjaxResult; +import org.apache.commons.lang3.tuple.Pair; + +public interface ApiLoginService { + + Pair login(String username, String password); + + AjaxResult verificationCodeLogin(String phoneNumber, String code); + + AjaxResult verificationCodeLoginOrRegister(String phoneNumber, String code, String parentInvitationCode); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiMessageService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiMessageService.java new file mode 100644 index 0000000..bc2a8e4 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiMessageService.java @@ -0,0 +1,14 @@ +package com.ruoyi.user.service; + +import com.ruoyi.domain.vo.ApiMessageDataVO; + +import java.util.List; + +public interface ApiMessageService { + + List getMessageList(Long userId, Integer id); + + ApiMessageDataVO view(Long userId, Integer id); + + String batchOperation(Long userId, List ids, String status); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiNoticeService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiNoticeService.java new file mode 100644 index 0000000..a83920c --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiNoticeService.java @@ -0,0 +1,31 @@ +package com.ruoyi.user.service; + +import com.ruoyi.domain.other.TtNotice; +import com.ruoyi.user.model.vo.ApiNoticeVO; + +import java.util.List; + +public interface ApiNoticeService { + + List getNoticeList(Long userId); + + List getNoticeListByCategoryPrefix(Long userId, String categoryPrefix); + + ApiNoticeVO getNoticeByNoticeId(Long userId, Integer noticeId); + + int countUnreadNotice(Long userId); + + int addNotice(TtNotice ttNotice); + + int editNotice(TtNotice ttNotice); + + int removeNoticeByNoticeId(Integer noticeId); + + /** + * 批量设置指定分类的通知为已读 + * + * @param categoryPrefix 分类前缀(GAME_表示游戏通知,SYS_表示系统通知) + * @return 影响行数 + */ + int markNoticesAsReadByCategory(String categoryPrefix); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiProfileRecordsService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiProfileRecordsService.java new file mode 100644 index 0000000..e7db0e4 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiProfileRecordsService.java @@ -0,0 +1,19 @@ +package com.ruoyi.user.service; + +import com.ruoyi.domain.dto.userRecord.ProfileRecordsRequest; +import com.ruoyi.domain.vo.userRecord.ProfileRecordsResponse; + +/** + * 个人资产详情 - 我的记录 聚合查询服务 + * 一个接口承载 4 个 Tab:余额/充值/提货/赠送 + */ +public interface ApiProfileRecordsService { + + /** + * 统一查询入口 + * + * @param userId 当前登录用户ID + * @param request 请求参数 + */ + ProfileRecordsResponse queryRecords(Integer userId, ProfileRecordsRequest request); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiPromotionRecordService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiPromotionRecordService.java new file mode 100644 index 0000000..4351af1 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiPromotionRecordService.java @@ -0,0 +1,10 @@ +package com.ruoyi.user.service; + +import com.ruoyi.domain.vo.promotion.TtPromotionRecordVo; + +import java.util.List; + +public interface ApiPromotionRecordService { + + List getPromotionRecord(Integer userId); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiRegisterService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiRegisterService.java new file mode 100644 index 0000000..72aedc7 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiRegisterService.java @@ -0,0 +1,8 @@ +package com.ruoyi.user.service; + +import com.ruoyi.domain.other.ApiRegisterBody; + +public interface ApiRegisterService { + + String register(ApiRegisterBody registerBody); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiShareService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiShareService.java new file mode 100644 index 0000000..89f6807 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiShareService.java @@ -0,0 +1,25 @@ +package com.ruoyi.user.service; + +import com.ruoyi.domain.vo.share.ShareCenterVO; +import com.ruoyi.domain.vo.share.ShareRecordPageVO; + +/** + * 主页"游戏分享"模块服务接口 + */ +public interface ApiShareService { + + /** + * 获取分享中心顶部数据(邀请码、今日/累计收益、邀请人数、分享链接) + */ + ShareCenterVO getShareCenter(Long userId); + + /** + * 分页查询分享记录列表(含按下级用户ID搜索) + * + * @param userId 当前登录用户ID(即邀请人) + * @param subUserId 可选的下级用户ID过滤;为 null 或 <=0 表示不过滤 + * @param pageNum 页码,从 1 开始 + * @param pageSize 每页大小 + */ + ShareRecordPageVO listShareRecords(Long userId, Long subUserId, int pageNum, int pageSize); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiShoppingService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiShoppingService.java new file mode 100644 index 0000000..5dbab01 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiShoppingService.java @@ -0,0 +1,25 @@ +package com.ruoyi.user.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.ApiShoppingBody; +import com.ruoyi.domain.vo.ApiShoppingDataVO; + +import java.math.BigDecimal; +import java.util.List; + +public interface ApiShoppingService { + + /** + * 商城商品列表 + * @param shoppingBody 查询条件(含常规过滤与 isExchange) + * @param userCredits 当前登录用户的积分;未登录传 null。 + * 仅在 isExchange=true 时生效,用于过滤出当前积分可兑换的饰品 + * 过滤公式:use_price * exchangePriceRatio <= userCredits。 + */ + List list(ApiShoppingBody shoppingBody, BigDecimal userCredits); + + R exchange(TtUser ttUser, List ornamentsIds); + + String integratingConversion(TtUser ttUser, BigDecimal credits); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiTaskCenterService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiTaskCenterService.java new file mode 100644 index 0000000..7859cc4 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiTaskCenterService.java @@ -0,0 +1,18 @@ +package com.ruoyi.user.service; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.user.model.TtTaskCenterUser; +import com.ruoyi.user.model.vo.ApiTaskCenterVO; + +import java.util.List; + +public interface ApiTaskCenterService { + + List selectApiTaskCenterVOList(Integer userId); + + TtTaskCenterUser selectTtTaskCenterUserByTaskIdAndUserId(Integer taskId, Integer userId); + + void updateYesterdayBonusPoints(); + + AjaxResult getReward(Integer taskId, Integer userId); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiUserPackSackService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiUserPackSackService.java new file mode 100644 index 0000000..c828d10 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiUserPackSackService.java @@ -0,0 +1,46 @@ +package com.ruoyi.user.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.dto.packSack.DecomposeLogCondition; +import com.ruoyi.domain.dto.packSack.DecomposeParam; +import com.ruoyi.domain.dto.packSack.DeliveryParam; +import com.ruoyi.domain.dto.packSack.PackSackCondition; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.vo.TtBoxRecordsDataVO; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.UserPackSackDataVO; +import com.ruoyi.domain.vo.client.PackSackGlobalData; +import com.ruoyi.domain.vo.client.WelfareBoxVO; +import com.ruoyi.user.model.TransferParam; + +import java.util.List; + +public interface ApiUserPackSackService { + R delivery(DeliveryParam param, TtUser ttUser); + + /** + * 白号假提货:生成已完成的提货记录供 cocos 展示,将饰品移出背包,不真正发货。 + */ + R blankDelivery(DeliveryParam param, TtUser ttUser); + + int decompose(DecomposeParam param, TtUser ttUser); + + List getPackSack(Integer userId); + + List packSackHandle(List packSackIds, TtUser ttUser, Integer status); + + List decomposeLog(DecomposeLogCondition param); + + List clientPackSack(PackSackCondition condition); + + R packSackGlobalData(Integer userId, Integer packType); + + String transfer(TransferParam param, TtUser ttUser); + + /** + * 白号假转赠:生成转赠记录供 cocos 展示,不真正转移饰品到副号背包。 + */ + String blankTransfer(TransferParam param, TtUser ttUser); + + List getWelfareBoxList(Long userId, Integer page, Integer size); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiUserService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiUserService.java new file mode 100644 index 0000000..6364903 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiUserService.java @@ -0,0 +1,36 @@ +package com.ruoyi.user.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.ApiForgetPasswordBody; +import com.ruoyi.domain.other.ApiSafePasswordBody; +import com.ruoyi.domain.other.ApiUpdateUserDetailsBody; +import com.ruoyi.domain.other.RealNameAuthenticationBody; +import org.springframework.web.multipart.MultipartFile; + +public interface ApiUserService { + + String profilePictureUpload(TtUser ttUser, MultipartFile file); + + String updateUserDetails(TtUser ttUser, ApiUpdateUserDetailsBody updateUserDetailsBody); + + String forgetPassword(ApiForgetPasswordBody apiForgetPasswordBody); + + String realNameAuthentication(TtUser ttUser, RealNameAuthenticationBody realNameAuthenticationBody); + + String realNameAuthentication2(TtUser ttUser, String realName, String idNum, String phoneNum, String captcha); + + String authenticationOk(TtUser ttUser); + + R changePW(TtUser ttUser, ApiUpdateUserDetailsBody updateUserDetailsBody,String token); + + R setSafePassword(TtUser ttUser, ApiSafePasswordBody apiSafePasswordBody); + + R bindBoss(TtUser ttUser, ApiUpdateUserDetailsBody updateUserDetailsBody); + + Integer getSubordinateNumber(Long userId); + + R checkRecharge(Integer userId); + + R bindInvitationCode(TtUser ttUser, String invitationCode); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiUserSubAccountService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiUserSubAccountService.java new file mode 100644 index 0000000..48c271c --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiUserSubAccountService.java @@ -0,0 +1,29 @@ +package com.ruoyi.user.service; + +import com.ruoyi.common.core.domain.R; +import com.ruoyi.domain.entity.TtUserSubAccount; + +import java.util.List; + +public interface ApiUserSubAccountService { + + /** + * 绑定副号 + */ + R bindSubAccount(Long mainUid, Long subUid); + + /** + * 解绑副号 + */ + R unbindSubAccount(Long mainUid, Long subUid); + + /** + * 查询主号绑定的所有副号 + */ + R> getSubAccounts(Long mainUid); + + /** + * 校验目标用户是否是当前用户的副号 + */ + boolean isSubAccount(Long mainUid, Long subUid); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiWebsiteSetupService.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiWebsiteSetupService.java new file mode 100644 index 0000000..ced61e7 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/ApiWebsiteSetupService.java @@ -0,0 +1,13 @@ +package com.ruoyi.user.service; + +import com.ruoyi.domain.other.TtBanner; +import com.ruoyi.domain.vo.ApiContentDataVO; + +import java.util.List; + +public interface ApiWebsiteSetupService { + + List getBannerList(); + + ApiContentDataVO getContentByType(String alias); +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiAnnouncementServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiAnnouncementServiceImpl.java new file mode 100644 index 0000000..a703fa1 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiAnnouncementServiceImpl.java @@ -0,0 +1,44 @@ +package com.ruoyi.user.service.impl; + +import com.ruoyi.domain.other.TtAnnouncement; +import com.ruoyi.user.mapper.ApiAnnouncementMapper; +import com.ruoyi.user.mapper.ApiAnnouncementReadMapper; +import com.ruoyi.user.service.ApiAnnouncementService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ApiAnnouncementServiceImpl implements ApiAnnouncementService { + + @Autowired + private ApiAnnouncementMapper apiAnnouncementMapper; + + @Autowired + private ApiAnnouncementReadMapper apiAnnouncementReadMapper; + + @Override + public List getAnnouncementList(Long userId) { + return apiAnnouncementMapper.getAnnouncementList(userId); + } + + @Override + public List getSystemAnnouncementList() { + return apiAnnouncementMapper.getSystemAnnouncementList(); + } + + @Override + public TtAnnouncement getAnnouncementByAnnouncementId(Integer announcementId, Long userId) { + // 新增用户和公告关联,表示已读 + if (apiAnnouncementReadMapper.countAnnouncementRead(announcementId, userId) == 0) { + apiAnnouncementReadMapper.addAnnouncementRead(announcementId, userId); + } + return apiAnnouncementMapper.getAnnouncementByAnnouncementId(announcementId); + } + + @Override + public int countUnreadAnnouncement(Long userId) { + return apiAnnouncementMapper.countUnreadAnnouncement(userId); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiBonusServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiBonusServiceImpl.java new file mode 100644 index 0000000..721b68d --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiBonusServiceImpl.java @@ -0,0 +1,666 @@ +package com.ruoyi.user.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.config.RedisConstants; +import com.ruoyi.admin.mapper.TtRedPacketMapper; +import com.ruoyi.admin.mapper.TtRedPacketRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.service.TtUserTransferDailyService; +import com.ruoyi.admin.service.TtVipLevelService; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MoneyUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.entity.transfer.TtUserTransferDaily; +import com.ruoyi.domain.other.TtRedPacket; +import com.ruoyi.domain.other.TtRedPacketRecord; +import com.ruoyi.domain.other.TtVipLevel; +import com.ruoyi.domain.vo.TtUserVipProgressVo; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.user.mapper.PromoCdkeyUserMapper; +import com.ruoyi.user.contract.api.response.QueryVipRewardResponse; +import com.ruoyi.user.contract.api.response.VipFlowRewardResponse; +import com.ruoyi.user.service.ApiBonusService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +public class ApiBonusServiceImpl implements ApiBonusService { + + private final TtUserService userService; + private final TtRedPacketMapper redPacketMapper; + private final TtRedPacketRecordMapper redPacketRecordMapper; + private final RedisLock redisLock; + + public ApiBonusServiceImpl(TtUserService userService, + TtRedPacketMapper redPacketMapper, + TtRedPacketRecordMapper redPacketRecordMapper, + RedisLock redisLock) { + this.userService = userService; + this.redPacketMapper = redPacketMapper; + this.redPacketRecordMapper = redPacketRecordMapper; + this.redisLock = redisLock; + } + + @Autowired + private TtUserBlendErcashMapper userBlendErcashMapper; + + @Autowired + private TtVipLevelService vipLevelService; + + @Autowired + private TtUserTransferDailyService userTransferDailyService; + + @Autowired + private PromoCdkeyUserMapper promoCdkeyUserMapper; + + /** + * 归一化玩家粘贴进来的口令。 + *

+ * 后台「亏损返利」站内信把口令写成「兑换CDK口令:[XXXX]」,那对方括号是给客户端切口令用的标记。 + * 玩家很可能连括号一起复制粘贴过来,这里统一去掉,避免因为两个符号导致兑换失败 + * (tt_red_packet.password 与 promo_cdkey.cdkey_code 本身都不含方括号,去掉无副作用)。 + */ + private String normalizeCode(String code) { + if (StringUtils.isBlank(code)) return code; + return code.trim().replace("[", "").replace("]", ""); + } + + // 领红包检查 + private R receiveRedPacketCheck(String code, TtUser ttUser) { + if (StringUtils.isBlank(code)) return R.fail("口令错误。"); + + TtRedPacket ttRedPacket = new LambdaQueryChainWrapper<>(redPacketMapper) + .eq(TtRedPacket::getPassword, code) + .eq(TtRedPacket::getDelFlag, 0) + .one(); + if (ObjectUtil.isNull(ttRedPacket)) return R.fail("口令错误,没有合法的红包。"); + if (ttRedPacket.getStatus().equals(1)) return R.fail("红包已经抢光了。"); + + List allRedPackRecord = new LambdaQueryChainWrapper<>(redPacketRecordMapper) + .eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId()) + .list(); + if (allRedPackRecord.size() >= ttRedPacket.getNum()) return R.fail("红包已经抢光了!"); + if (Objects.isNull(ttRedPacket.getUseStatus()) || ttRedPacket.getUseStatus().equals(1)) + return R.fail("该红包被禁用,请联系管理员。"); + + // 指定领取人:非空时只允许本人领取(亏损返利CDK 用该字段绑定收款人, + // 避免口令被截图/转发后别人先兑走)。为空则是原有的"不限用户"行为。 + Integer receiverUserId = ttRedPacket.getReceiverUserId(); + if (receiverUserId != null && receiverUserId > 0 + && !Objects.equals(receiverUserId, ttUser.getUserId())) { + log.info("CDKEY兑换失败: userId={}, code={}, 原因=非指定领取人 receiverUserId={}", + ttUser.getUserId(), code, receiverUserId); + return R.fail("该CDK仅限本人领取。"); + } + + if (ObjectUtil.isNotEmpty(ttRedPacket.getUserId()) && Objects.equals(ttUser.getUserId(), ttRedPacket.getUserId())) + return R.fail("给粉丝的专属红包,您自己无法领取。"); + if (ObjectUtil.isNotEmpty(ttRedPacket.getUserId()) + && ttRedPacket.getUserId() != 0 + && !Objects.equals(ttUser.getParentId(), ttRedPacket.getUserId())) + return R.fail("粉丝专属红包,您尚未关注该主播。"); + + Date openingTime = ttRedPacket.getOpeningTime(); + if (DateUtils.getNowDate().compareTo(ttRedPacket.getOpeningTime()) < 0) + return R.fail("红包开启时间:" + DateUtil.format(openingTime, "yyyy-MM-dd HH:mm:ss") + ",敬请期待。"); + + Date validity = ttRedPacket.getValidity(); + if (StringUtils.isNotNull(validity) && DateUtils.getNowDate().compareTo(validity) > 0) { + ttRedPacket.setStatus(1); + new LambdaUpdateChainWrapper<>(redPacketMapper) + .eq(TtRedPacket::getId, ttRedPacket.getId()) + .set(TtRedPacket::getStatus, 1) + .update(); + return R.fail("红包已结束"); + } + + boolean alreadyReceived = allRedPackRecord.stream() + .anyMatch(r -> r.getUserId().equals(ttUser.getUserId())); + if (alreadyReceived) { + return R.fail("您已领取过该红包,请勿重复领取。"); + } + + return R.ok(ttRedPacket); + } + + @Override + public R receiveRedPacket(String code, TtUser ttUser) { + + code = normalizeCode(code); + log.info("receiveRedPacket: userId={}, code={}", ttUser.getUserId(), code); + R check = receiveRedPacketCheck(code, ttUser); + if (!check.getCode().equals(200)) { + // 红包找不到,尝试从推广CDKEY表兑换 + log.info("红包口令未匹配,转入CDKEY兑换: userId={}, code={}, 红包检查结果={}", ttUser.getUserId(), code, check.getMsg()); + return tryRedeemPromoCdkey(code, ttUser); + } + + TtRedPacket ttRedPacket = check.getData(); + + // 锁前快速检查:该用户是否已领取该红包(减少锁竞争,非安全校验) + int preCheckCount = new LambdaQueryChainWrapper<>(redPacketRecordMapper) + .eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId()) + .eq(TtRedPacketRecord::getUserId, ttUser.getUserId()) + .count().intValue(); + if (preCheckCount > 0) { + return R.fail("您已领取过该红包,请勿重复领取。"); + } + + // 用户级锁(锁粒度: redPacketId + userId,防止同一用户并发领取同一红包) + String lockName = RedisConstants.RECEIVE_RED_PACKET_LOCK + ttRedPacket.getId() + ":" + ttUser.getUserId(); + boolean lock = redisLock.tryLock(lockName, 0L, 30L, TimeUnit.SECONDS); + if (!lock) return R.fail("没抢到红包。"); + + // 红包级全局锁(锁粒度: redPacketId,防止不同用户并发领取导致库存超发) + String packetLockName = RedisConstants.RECEIVE_RED_PACKET_LOCK + ttRedPacket.getId(); + boolean packetLock = false; + for (int l = 0; l < 5; l++) { + packetLock = redisLock.tryLock(packetLockName, 5L, 10L, TimeUnit.SECONDS); + if (packetLock) break; + } + if (!packetLock) { + redisLock.unlock(lockName); + return R.fail("没抢到红包。"); + } + + try { + // 锁内二次检查(防TOCTOU竞态条件) + int doubleCheckCount = new LambdaQueryChainWrapper<>(redPacketRecordMapper) + .eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId()) + .eq(TtRedPacketRecord::getUserId, ttUser.getUserId()) + .count().intValue(); + if (doubleCheckCount > 0) { + log.warn("红包重复领取(锁内二次检查拦截): userId={}, redPacketId={}", ttUser.getUserId(), ttRedPacket.getId()); + return R.fail("您已领取过该红包,请勿重复领取。"); + } + + List redPacketRecords = new LambdaQueryChainWrapper<>(redPacketRecordMapper) + .eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId()) + .list(); + + if (ttRedPacket.getNum() > redPacketRecords.size()) { + + // 构建抢红包记录 + BigDecimal receiveAmount = RandomUtils.getRandomPrice(ttRedPacket.getAmount()); + receiveAmount = receiveAmount.setScale(2, RoundingMode.HALF_UP); + + TtRedPacketRecord redPacketRecord = new TtRedPacketRecord(); + redPacketRecord.setRedPacketId(ttRedPacket.getId()); + redPacketRecord.setUserId(ttUser.getUserId()); + redPacketRecord.setReceivePassword(code); + redPacketRecord.setReceiveAmount(receiveAmount); + redPacketRecord.setReceiveTime(DateUtils.getNowDate()); + + // 保存记录(捕获唯一约束异常作为最后防线) + try { + redPacketRecordMapper.insert(redPacketRecord); + } catch (DuplicateKeyException dke) { + log.warn("红包重复领取(唯一约束拦截): userId={}, redPacketId={}", ttUser.getUserId(), ttRedPacket.getId()); + return R.fail("您已领取过该红包,请勿重复领取。"); + } + + // 更新用户账户:userId > 0 为主播创建的CDK,使用推广CDKEY兑换收入;否则为平台福利CDK收入 + TtAccountRecordSource redeemSource = (ttRedPacket.getUserId() != null && ttRedPacket.getUserId() > 0) + ? TtAccountRecordSource.PROMO_CDKEY_REDEEM + : TtAccountRecordSource.RECEIVE_RED_PACKET; + userService.updateOnlyUserAccount(ttUser.getUserId(), receiveAmount, redeemSource); + + // 更新红包状态 + Integer count = new LambdaQueryChainWrapper<>(redPacketRecordMapper) + .eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId()) + .count().intValue(); + if (ttRedPacket.getNum() <= count) { + // 抢完更新为已结束 + ttRedPacket.setStatus(1); + new LambdaUpdateChainWrapper<>(redPacketMapper) + .eq(TtRedPacket::getId, ttRedPacket.getId()) + .set(TtRedPacket::getStatus, 1) + .update(); + } + + return R.ok(receiveAmount); + + } else { + new LambdaUpdateChainWrapper<>(redPacketMapper) + .eq(TtRedPacket::getId, ttRedPacket.getId()) + .set(TtRedPacket::getStatus, 1) + .update(); + return R.fail("红包已经抢光了!"); + } + + } catch (Exception e) { + log.error("任务执行发生异常", e); + return R.fail("系统繁忙,请稍后重试。"); + } finally { + redisLock.unlock(packetLockName); + redisLock.unlock(lockName); + } + } + + /** + * 尝试从推广CDKEY表兑换 + * 仅主播账号创建的CDKEY,且主播下级普通用户可领取 + */ + private R tryRedeemPromoCdkey(String code, TtUser ttUser) { + if (StringUtils.isBlank(code)) return R.fail("口令错误。"); + + java.util.Map cdkey = promoCdkeyUserMapper.findActiveCdkey(code); + if (cdkey == null) { + log.info("CDKEY兑换失败: userId={}, code={}, 原因=查不到进行中的CDKEY", ttUser.getUserId(), code); + return R.fail("口令错误,没有合法的红包。"); + } + + Long cdkeyId = ((Number) cdkey.get("id")).longValue(); + String cdkeyCode = (String) cdkey.get("cdkey_code"); + BigDecimal amountMin = cdkey.get("amount_min") != null ? new BigDecimal(cdkey.get("amount_min").toString()) : BigDecimal.ZERO; + BigDecimal amountMax = cdkey.get("amount_max") != null ? new BigDecimal(cdkey.get("amount_max").toString()) : BigDecimal.ZERO; + Integer usableCount = cdkey.get("usable_count") != null ? ((Number) cdkey.get("usable_count")).intValue() : 0; + Long creatorId = ((Number) cdkey.get("creator_id")).longValue(); + + log.info("CDKEY兑换开始: userId={}, cdkeyId={}, cdkeyCode={}, usableCount={}, creatorId={}, userType={}, parentId={}", + ttUser.getUserId(), cdkeyId, cdkeyCode, usableCount, creatorId, ttUser.getUserType(), ttUser.getParentId()); + + // 检查可用次数 + if (usableCount <= 0) { + log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=可用次数为{}", ttUser.getUserId(), cdkeyId, usableCount); + return R.fail("该CDKEY已兑完。"); + } + + // 检查开启时间 + Object openingTimeObj = cdkey.get("opening_time"); + if (openingTimeObj != null) { + java.time.LocalDateTime openingLdt = toLocalDateTime(openingTimeObj); + if (openingLdt != null && java.time.LocalDateTime.now().isBefore(openingLdt)) { + log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=开启时间未到, openingTime={}, now={}", + ttUser.getUserId(), cdkeyId, openingLdt, java.time.LocalDateTime.now()); + return R.fail("CDKEY尚未开启,敬请期待。"); + } + } + + // 检查有效期 + Object validityObj = cdkey.get("validity"); + if (validityObj != null) { + java.time.LocalDateTime validityLdt = toLocalDateTime(validityObj); + if (validityLdt != null && java.time.LocalDateTime.now().isAfter(validityLdt)) { + log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=已过期, validity={}, now={}", + ttUser.getUserId(), cdkeyId, validityLdt, java.time.LocalDateTime.now()); + return R.fail("该CDKEY已过期。"); + } + } + + // 检查用户类型:仅普通用户可兑换 + if (!UserType.COMMON_USER.getCode().equals(ttUser.getUserType())) { + log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=用户类型不合法, userType={}", + ttUser.getUserId(), cdkeyId, ttUser.getUserType()); + return R.fail("仅普通用户可兑换该CDKEY。"); + } + + // 检查是否为创建者下级 + if (ttUser.getParentId() == null || ttUser.getParentId() != creatorId.intValue()) { + log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=非创建者下级, parentId={}, creatorId={}", + ttUser.getUserId(), cdkeyId, ttUser.getParentId(), creatorId); + return R.fail("该CDKEY仅限主播下级用户领取。"); + } + + // 锁前快速检查(减少锁竞争,非安全校验) + int alreadyRedeemed = promoCdkeyUserMapper.countUserRedeem(cdkeyId, ttUser.getUserId().longValue()); + if (alreadyRedeemed > 0) { + log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=重复兑换", ttUser.getUserId(), cdkeyId); + return R.fail("您已兑换过该CDKEY。"); + } + + // 加锁兑换(锁粒度: cdkeyId + userId,防止同一用户并发兑换同一CDKEY) + String lockName = "PROMO_CDKEY_REDEEM_LOCK:" + cdkeyId + ":" + ttUser.getUserId(); + boolean lock = false; + for (int i = 0; i < 5; i++) { + lock = redisLock.tryLock(lockName, 3L, 7L, TimeUnit.SECONDS); + if (lock) break; + } + if (!lock) return R.fail("服务器繁忙,请稍后重试。"); + + try { + // 锁内二次检查(防TOCTOU竞态条件) + if (promoCdkeyUserMapper.countUserRedeem(cdkeyId, ttUser.getUserId().longValue()) > 0) { + log.info("CDKEY兑换失败(锁内二次检查): userId={}, cdkeyId={}, 原因=重复兑换", ttUser.getUserId(), cdkeyId); + return R.fail("您已兑换过该CDKEY。"); + } + + int updated = promoCdkeyUserMapper.decrementUsableCount(cdkeyId); + if (updated <= 0) return R.fail("CDKEY已兑完。"); + + // 计算兑换金额 + BigDecimal receiveAmount; + if (amountMin.compareTo(amountMax) >= 0) { + receiveAmount = amountMin; + } else { + double random = amountMin.doubleValue() + Math.random() * (amountMax.doubleValue() - amountMin.doubleValue()); + receiveAmount = BigDecimal.valueOf(random).setScale(2, RoundingMode.HALF_UP); + } + + // 记录兑换(捕获唯一约束异常作为最后防线) + Date now = new Date(); + try { + promoCdkeyUserMapper.insertRedeemRecord(cdkeyId, cdkeyCode, + ttUser.getUserId().longValue(), ttUser.getUserName(), receiveAmount, now); + } catch (DuplicateKeyException dke) { + log.warn("CDKEY兑换唯一约束冲突(最后防线拦截): userId={}, cdkeyId={}", ttUser.getUserId(), cdkeyId); + return R.fail("您已兑换过该CDKEY。"); + } + + // 可用次数用完则更新状态 + promoCdkeyUserMapper.updateStatusIfExhausted(cdkeyId); + + // 增加用户金币余额 + userService.updateOnlyUserAccount(ttUser.getUserId(), receiveAmount, TtAccountRecordSource.PROMO_CDKEY_REDEEM); + + return R.ok(receiveAmount); + } catch (Exception e) { + log.error("CDKEY兑换异常", e); + return R.fail("系统繁忙,请稍后重试。"); + } finally { + redisLock.unlock(lockName); + } + } + + @Override + public R vipRewardList(TtUser ttUser) { + // 今日游戏流水(所有游戏消费 source) + var todayAmountCf = AsyncManager.me().supply(() -> { + LocalDateTime beginTime = LocalDate.now().atStartOfDay(); + var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()) + .ge(TtUserBlendErcash::getCreateTime, beginTime) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes()).list(); + if (CollectionUtils.isEmpty(records)) { + return BigDecimal.ZERO; + } + return records.stream() + .map(TtUserBlendErcash::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add).abs(); + }); + + // 昨日游戏流水(所有游戏消费 source) + var yesterdayAmountCf = AsyncManager.me().supply(() -> { + LocalDate yesterday = LocalDate.now().minusDays(1); + LocalDateTime beginTime = yesterday.atStartOfDay(); + LocalDateTime endTime = yesterday.atTime(23, 59, 59); + + var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()) + .between(TtUserBlendErcash::getCreateTime, beginTime, endTime) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes()).list(); + if (CollectionUtils.isEmpty(records)) { + return BigDecimal.ZERO; + } + return records.stream() + .map(TtUserBlendErcash::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add).abs(); + }); + + // 已领奖励:昨日的 VIP 流水返利总额(实时发放模式下,昨日消费产生的返利在昨日已实时入账) + LocalDate yesterday = LocalDate.now().minusDays(1); + LocalDateTime yesterdayStart = yesterday.atStartOfDay(); + LocalDateTime yesterdayEnd = LocalDate.now().atStartOfDay().minusNanos(1); + var yesterdayRewardRecords = new LambdaQueryChainWrapper<>(userBlendErcashMapper) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.VIP_CONSUME_AWARD.getCode()) + .between(TtUserBlendErcash::getCreateTime, yesterdayStart, yesterdayEnd) + .list(); + BigDecimal yesterdayRewardTotal = CollectionUtils.isEmpty(yesterdayRewardRecords) + ? BigDecimal.ZERO + : yesterdayRewardRecords.stream() + .map(TtUserBlendErcash::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add).abs(); + + var todayAmount = AsyncManager.getMs(todayAmountCf, 1000); + var yesterdayAmount = AsyncManager.getMs(yesterdayAmountCf, 1000); + + QueryVipRewardResponse response = new QueryVipRewardResponse(); + if (todayAmount != null) { + response.setTodayAmount(MoneyUtil.toStr(todayAmount)); + if (ttUser.getVipLevel() != null && ttUser.getVipLevel() > 0) { + TtVipLevel ttVipLevel = vipLevelService.getById(ttUser.getVipLevel()); + if (ttVipLevel == null) { + log.warn("用户【{}】VIP等级不存在![{}]", ttUser.getUserId(), ttUser.getVipLevel()); + response.setTodayReward("0.00"); + } else { + // 使用 flowRebatePercent(若为 null 则回退到 commissions) + BigDecimal percent = ttVipLevel.getFlowRebatePercent() != null + ? ttVipLevel.getFlowRebatePercent() + : (ttVipLevel.getCommissions() != null ? ttVipLevel.getCommissions() : BigDecimal.ZERO); + BigDecimal rebate = percent.divide(BigDecimal.valueOf(100), 4, + RoundingMode.HALF_UP) + .multiply(todayAmount).setScale(2, RoundingMode.HALF_UP); + response.setTodayReward(MoneyUtil.toStr(rebate)); + } + + } else { + response.setTodayReward("0.00"); + } + } else { + response.setTodayReward("0.00"); + response.setTodayAmount("0.00"); + } + + if (yesterdayAmount != null) { + response.setYesterdayAmount(MoneyUtil.toStr(yesterdayAmount)); + } else { + response.setYesterdayAmount("0.00"); + } + if (yesterdayRewardTotal.compareTo(BigDecimal.ZERO) > 0) { + response.setYesterdayReward(MoneyUtil.toStr(yesterdayRewardTotal)); + } else { + response.setYesterdayReward("0.00"); + } + + // VIP 等级列表(全量返回) + List rewardLevels = vipLevelService.list().stream() + .filter(lv -> lv != null && lv.getId() != null) + .sorted(java.util.Comparator.comparing(TtVipLevel::getId)) + .collect(java.util.stream.Collectors.toList()); + response.setVipLevels(rewardLevels); + + // 填充当前用户今日转赠次数信息 + Integer userVipLevelId = ttUser.getVipLevel() != null ? ttUser.getVipLevel() : 0; + int dailyLimit = 0; + if (userVipLevelId > 0) { + TtVipLevel currentVip = vipLevelService.getById(userVipLevelId); + if (currentVip != null && currentVip.getTransferCount() != null) { + dailyLimit = currentVip.getTransferCount(); + } + } + TtUserTransferDaily dailyRecord = userTransferDailyService.getOrCreateToday( + ttUser.getUserId(), userVipLevelId, dailyLimit); + response.setTransferCount(dailyLimit); + int used = dailyRecord.getUsedCount() != null ? dailyRecord.getUsedCount() : 0; + int pending = dailyRecord.getPendingCount() != null ? dailyRecord.getPendingCount() : 0; + response.setAvailableTransferCount(Math.max(dailyLimit - used - pending, 0)); + + // 填充 VIP 经验进度信息 + TtUserVipProgressVo progress = userService.userVipProgress(ttUser.getUserId()); + if (progress != null) { + response.setCurrentVipLevel(progress.getVipLevel()); + response.setCurrentExp(progress.getCurrentExp()); + response.setNextLevelExp(progress.getNextLevelExp()); + response.setNeedExp(progress.getNeedExp()); + response.setMaxLevel(progress.getMaxLevel()); + response.setProgressText(progress.getProgressText()); + } + + return R.ok(response); + } + + @Override + public R vipFlowReward(TtUser ttUser) { + // 今日游戏流水 + var todayAmountCf = AsyncManager.me().supply(() -> { + LocalDateTime beginTime = LocalDate.now().atStartOfDay(); + var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()) + .ge(TtUserBlendErcash::getCreateTime, beginTime) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes()).list(); + if (CollectionUtils.isEmpty(records)) { + return BigDecimal.ZERO; + } + return records.stream() + .map(TtUserBlendErcash::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add).abs(); + }); + + // 昨日游戏流水 + var yesterdayAmountCf = AsyncManager.me().supply(() -> { + LocalDate yd = LocalDate.now().minusDays(1); + LocalDateTime begin = yd.atStartOfDay(); + LocalDateTime end = yd.atTime(23, 59, 59); + var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()) + .between(TtUserBlendErcash::getCreateTime, begin, end) + .in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes()).list(); + if (CollectionUtils.isEmpty(records)) { + return BigDecimal.ZERO; + } + return records.stream() + .map(TtUserBlendErcash::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add).abs(); + }); + + // 已领奖励:截至昨日 23:59:59 累计领取的 VIP 流水返利总额 + LocalDateTime todayStart = LocalDate.now().atStartOfDay(); + var yesterdayRewardRecords = new LambdaQueryChainWrapper<>(userBlendErcashMapper) + .eq(TtUserBlendErcash::getUserId, ttUser.getUserId()) + .eq(TtUserBlendErcash::getSource, TtAccountRecordSource.VIP_CONSUME_AWARD.getCode()) + .lt(TtUserBlendErcash::getCreateTime, todayStart) + .list(); + BigDecimal yesterdayRewardTotal = CollectionUtils.isEmpty(yesterdayRewardRecords) + ? BigDecimal.ZERO + : yesterdayRewardRecords.stream() + .map(TtUserBlendErcash::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add).abs(); + + var todayAmount = AsyncManager.getMs(todayAmountCf, 1000); + var yesterdayAmount = AsyncManager.getMs(yesterdayAmountCf, 1000); + + VipFlowRewardResponse response = new VipFlowRewardResponse(); + + // 今日流水 & 预计奖励 + if (todayAmount != null) { + response.setTodayAmount(MoneyUtil.toStr(todayAmount)); + if (ttUser.getVipLevel() != null && ttUser.getVipLevel() > 0) { + TtVipLevel ttVipLevel = vipLevelService.getById(ttUser.getVipLevel()); + if (ttVipLevel == null) { + response.setTodayReward("0.00"); + } else { + BigDecimal percent = ttVipLevel.getFlowRebatePercent() != null + ? ttVipLevel.getFlowRebatePercent() + : (ttVipLevel.getCommissions() != null ? ttVipLevel.getCommissions() : BigDecimal.ZERO); + BigDecimal rebate = percent.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP) + .multiply(todayAmount).setScale(2, RoundingMode.HALF_UP); + response.setTodayReward(MoneyUtil.toStr(rebate)); + } + } else { + response.setTodayReward("0.00"); + } + } else { + response.setTodayReward("0.00"); + response.setTodayAmount("0.00"); + } + + // 昨日流水 & 已领奖励 + response.setYesterdayAmount(yesterdayAmount != null ? MoneyUtil.toStr(yesterdayAmount) : "0.00"); + response.setYesterdayReward(yesterdayRewardTotal.compareTo(BigDecimal.ZERO) > 0 + ? MoneyUtil.toStr(yesterdayRewardTotal) : "0.00"); + + // VIP 经验进度(先获取,用于构建每级进度文本) + TtUserVipProgressVo progress = userService.userVipProgress(ttUser.getUserId()); + long currentExp = (progress != null && progress.getCurrentExp() != null) ? progress.getCurrentExp() : 0L; + + // VIP 等级列表(精简 + 每级进度文本) + List allLevels = vipLevelService.list().stream() + .filter(lv -> lv != null && lv.getId() != null) + .sorted(java.util.Comparator.comparing(TtVipLevel::getId)) + .collect(java.util.stream.Collectors.toList()); + + List levelItems = new java.util.ArrayList<>(); + for (int i = 0; i < allLevels.size(); i++) { + TtVipLevel lv = allLevels.get(i); + long threshold = lv.getRechargeThreshold() != null + ? lv.getRechargeThreshold().setScale(0, RoundingMode.CEILING).longValue() : 0L; + + // 进度文本计算:所有等级都显示到下一级阈值的距离 + String lvProgress; + if (i == allLevels.size() - 1) { + // 最后一级:显示满级状态 + lvProgress = threshold + "/max"; + } else if (threshold <= 0) { + lvProgress = currentExp + "/0"; + } else { + // 显示当前经验/下一级阈值(即使当前等级已满) + TtVipLevel nextLv = allLevels.get(i + 1); + long nextThreshold = nextLv.getRechargeThreshold() != null + ? nextLv.getRechargeThreshold().setScale(0, RoundingMode.CEILING).longValue() : 0L; + lvProgress = currentExp + "/" + nextThreshold; + } + levelItems.add(VipFlowRewardResponse.VipLevelItem.builder() + .name(lv.getName()) + .rechargeThreshold(lv.getRechargeThreshold()) + .flowRebatePercent(lv.getFlowRebatePercent() != null + ? lv.getFlowRebatePercent() + : (lv.getCommissions() != null ? lv.getCommissions() : BigDecimal.ZERO)) + .progressText(lvProgress) + .build()); + } + response.setVipLevels(levelItems); + + // 填充全局经验进度 + if (progress != null) { + response.setCurrentVipLevel(progress.getVipLevel()); + response.setCurrentExp(progress.getCurrentExp()); + response.setNextLevelExp(progress.getNextLevelExp()); + response.setNeedExp(progress.getNeedExp()); + response.setMaxLevel(progress.getMaxLevel()); + response.setProgressText(progress.getProgressText()); + } + + return R.ok(response); + } + + /** + * 兼容 LocalDateTime / java.util.Date 两种类型的时间字段 + */ + private java.time.LocalDateTime toLocalDateTime(Object obj) { + if (obj == null) return null; + if (obj instanceof java.time.LocalDateTime) { + return (java.time.LocalDateTime) obj; + } + if (obj instanceof java.util.Date) { + return ((java.util.Date) obj).toInstant() + .atZone(java.time.ZoneId.systemDefault()) + .toLocalDateTime(); + } + return null; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiLoginServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiLoginServiceImpl.java new file mode 100644 index 0000000..c953eb6 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiLoginServiceImpl.java @@ -0,0 +1,306 @@ +package com.ruoyi.user.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.RandomUtil; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtUserAvatarService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.util.RandomUtils; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.sys.TtPromotionUpdate; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtUserAvatar; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.web.service.SysLoginService; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.framework.websocket.connector.SessionRegistry; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.common.service.ApiSmsService; +import com.ruoyi.user.service.ApiLoginService; +import com.ruoyi.admin.service.UserIdGenerator; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class ApiLoginServiceImpl implements ApiLoginService { + + @Autowired + private ApiSmsService apiSmsService; + @Autowired + private TtUserMapper userMapper; + @Autowired + private SysLoginService sysLoginService; + @Autowired + private TokenService tokenService; + @Autowired + private RedisCache redisCache; + @Autowired + private TtUserService userService; + @Autowired + private TtUserAvatarService userAvatarService; + @Autowired + private SessionRegistry sessionRegistry; + @Autowired + private ISysConfigService configService; + @Autowired + private TtPromotionUpdateMapper ttPromotionUpdateMapper; + @Autowired + private UserIdGenerator userIdGenerator; + + @Override + public Pair login(String username, String password) { + sysLoginService.loginPreCheck(username, password); + LoginUser loginUser = sysLoginService.createLoginUser("api_" + username, password); + // 顶号处理:本次登录前清除该账号在其它设备上的登录Token,避免旧设备继续可调用接口(如开箱) + replicatedLogin(loginUser.getUsername()); + recordLoginInfo(loginUser.getUserId()); + return Pair.of(tokenService.createToken(loginUser), null); + } + + @Override + public AjaxResult verificationCodeLogin(String phoneNumber, String code) { + if (StringUtils.isEmpty(phoneNumber)) { + return AjaxResult.error("手机号不能为空"); + } + if (!Validator.isMobile(phoneNumber)) { + return AjaxResult.error("手机号格式错误,请检查手机号是否输入正确!"); + } + if (StringUtils.isEmpty(code)) { + return AjaxResult.error("验证码不能为空"); + } + + TtUser ttUser = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getPhoneNumber, phoneNumber).one(); + if (StringUtils.isNull(ttUser)) { + return AjaxResult.error("该手机号未在本站注册!"); + } + + // 分支①:4位数字 → 原有短信验证码登录逻辑 + if (NumberUtil.isNumber(code) && code.trim().length() == 4) { + String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "ApiLogin_" + phoneNumber); + if (!"success".equals(validateCaptcha)) { + return AjaxResult.error(validateCaptcha); + } + } + // 分支②:非4位数字 → 视为密码,前提是已完成滑块验证 + /*else { + String verifyKey = "captcha:verified:" + phoneNumber; + String verifiedCaptchaId = redisCache.getCacheObject(verifyKey); + if (StringUtils.isEmpty(verifiedCaptchaId)) { + return AjaxResult.error("请先完成滑块验证"); + } + // 滑块验证通过,将 code 视为密码直接登录 + Pair pair = this.login(ttUser.getUserName(), code.trim()); + redisCache.deleteObject(verifyKey); // 使用后清除,防止重放 + if (pair.getValue() != null) { + return pair.getValue(); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put(Constants.TOKEN, pair.getKey()); + return ajax; + }*/ + + // 短信验证通过后的原有登录逻辑 + if (StringUtils.isEmpty(ttUser.getRemark()) || !ttUser.getRemark().contains(":")) { + return AjaxResult.error("账号密码信息异常,请联系客服"); + } + String password = ttUser.getRemark().substring(ttUser.getRemark().indexOf(":") + 1); + Pair pair = this.login(ttUser.getUserName(), password); + if (pair.getValue() != null) { + return pair.getValue(); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put(Constants.TOKEN, pair.getKey()); + return ajax; + } + + @Override + public AjaxResult verificationCodeLoginOrRegister(String phoneNumber, String code, String parentInvitationCode) { + if (StringUtils.isEmpty(phoneNumber)) { + return AjaxResult.error("手机号不能为空"); + } + if (!Validator.isMobile(phoneNumber)) { + return AjaxResult.error("手机号格式错误,请检查手机号是否输入正确!"); + } + if (StringUtils.isEmpty(code)) { + return AjaxResult.error("验证码不能为空"); + } + if (!NumberUtil.isNumber(code) || code.trim().length() != 4) { + return AjaxResult.error("验证码错误"); + } + String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "ApiLogin_" + phoneNumber); + if (!"success".equals(validateCaptcha)) { + return AjaxResult.error(validateCaptcha); + } + + TtUser ttUser = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getPhoneNumber, phoneNumber).one(); + if (StringUtils.isNull(ttUser)) { + String result = register(phoneNumber, parentInvitationCode); + if (StringUtils.isNotEmpty(result)) { + return AjaxResult.error(result); + } + ttUser = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getPhoneNumber, phoneNumber).one(); + } + + if (StringUtils.isEmpty(ttUser.getRemark()) || !ttUser.getRemark().contains(":")) { + return AjaxResult.error("账号密码信息异常,请联系客服"); + } + String password = ttUser.getRemark().substring(ttUser.getRemark().indexOf(":") + 1); + Pair pair = this.login(ttUser.getUserName(), password); + if (pair.getValue() != null) { + return pair.getValue(); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put(Constants.TOKEN, pair.getKey()); + return ajax; + } + + private String register(String phoneNumber, String parentInvitationCode) { + String nickName = RandomUtils.getRandomName(new Random().nextInt(2)); + String password = RandomUtil.randomString(10); + + if (StringUtils.isEmpty(nickName)) return "用户昵称不能为空"; + if (StringUtils.isEmpty(phoneNumber)) return "手机号不能为空"; + if (!Validator.isMobile(phoneNumber)) return "手机号格式错误,请检查手机号是否输入正确!"; + if (StringUtils.isEmpty(password)) return "用户密码不能为空"; + + TtUser ttUser = TtUser.builder().build(); + ttUser.setUserName(phoneNumber); + ttUser.setEmail(phoneNumber + "@qq.com"); + ttUser.setPhoneNumber(phoneNumber); + ttUser.setAccountAmount(BigDecimal.ZERO); + ttUser.setAccountCredits(BigDecimal.ZERO); + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { + return "密码长度必须在5到20个字符之间"; + } + if (!userService.checkPhoneUnique(ttUser)) return "注册失败," + "手机号'" + phoneNumber + "'已被注册!"; + if (!userService.checkUserNameUnique(ttUser)) + return "注册失败," + "用户名'" + ttUser.getUserName() + "'已存在!"; + ttUser.setNickName(nickName); + ttUser.setUserType(UserType.COMMON_USER.getCode()); + ttUser.setCommissionRate(new BigDecimal("2")); + List userAvatarList = new LambdaQueryChainWrapper<>(userAvatarService.getBaseMapper()).eq(TtUserAvatar::getIsDefault, "1").list(); + if (!userAvatarList.isEmpty()) { + ttUser.setAvatar(userAvatarList.get(0).getAvatar()); + } else ttUser.setAvatar(""); + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + String registerRedPacketStr = configService.selectConfigByKey("registerRedPacket"); + BigDecimal registerRedPacket = new BigDecimal(registerRedPacketStr); + ttUser.setAccountAmount(registerRedPacket); + ttUser.setInvitationCode(userService.getInvitationCode().toLowerCase()); + if (StringUtils.isNotEmpty(parentInvitationCode) && parentInvitationCode.trim().length() == 6) { + TtUser parentUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase()) + .eq(TtUser::getDelFlag, "0").one(); + if (StringUtils.isNull(parentUser)) { + return "上级邀请码填写错误!"; + } else { + + } + ttUser.setParentId(parentUser.getUserId()); + } + ttUser.setIsRealCheck("0"); + ttUser.setCreateBy("网站注册"); + ttUser.setCreateTime(DateUtils.getNowDate()); + ttUser.setRemark("明文密码:" + password); + + var id = userIdGenerator.generateUserId(); + ttUser.setUserId(id); + + boolean regFlag = userService.save(ttUser); + if (!regFlag) return "注册失败,请联系系统管理人员"; + +// if (BigDecimal.ZERO.compareTo(registerRedPacket) < 0) { +// userService.insertUserAmountRecords(ttUser.getUserId(), TtAccountRecordType.INPUT, TtAccountRecordSource.REGIST_AWARD, registerRedPacket, ttUser.getAccountAmount()); +// } + AsyncManager.me().execute(AsyncFactory + .recordLogininfor("api_" + ttUser.getUserName(), Constants.REGISTER, MessageUtils.message("user.register.success"))); + + // 保存推广更新记录(非核心数据,失败不影响注册主流程) + try { + TtPromotionUpdate build = TtPromotionUpdate.builder() + .employeeId(ttUser.getUserId()) + .bossId(ttUser.getParentId()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttPromotionUpdateMapper.insert(build); + } catch (Exception e) { + log.warn("用户[{}]注册后保存推广更新记录失败: {}", ttUser.getUserId(), e.getMessage()); + } + return ""; + } + + + /** + * 检测用户是否在其它地方登录,如果存在则发送通知给当前用户, + * 并执行相应的清理工作,例如删除已登录用户的缓存信息 + */ + private AjaxResult replicatedLogin(String username) { + Collection redisKeys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); + if (redisKeys == null || redisKeys.isEmpty()) { + return null; + } + List loginUserList = redisKeys.stream().map(redisCache::getCacheObject) + .filter(Objects::nonNull) + // api端用户 user 恒为 null(走 userData),以此区分,避免误踢后台/运营用户 + .filter(loginUser -> loginUser.getUser() == null) + .filter(loginUser -> username.equals(loginUser.getUsername())) + .collect(Collectors.toList()); + if (loginUserList.isEmpty()) { + return null; + } + // 遍历踢掉该账号在其它设备上的所有历史登录(可能残留多个Token), + // 确保任意端(手机/电脑)同一账号严格只保留最新一次登录的Token,其余全部失效。 + for (LoginUser oldLoginUser : loginUserList) { + sessionRegistry.kickOut(oldLoginUser.getUserId(), "您的账号在别处登录,如果不是本人操作,请尽快修改密码!"); + redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + oldLoginUser.getToken()); + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor("api_" + username, + Constants.LOGOUT, "退出成功,账号已换机登录!")); + return null; + } + + /** + * 记录登录信息 + */ + public void recordLoginInfo(Long userId) { + // 改为同步执行,确保登录时必定更新 login_date(避免异步任务丢失或延迟) + try { + new LambdaUpdateChainWrapper<>(userMapper) + .eq(TtUser::getUserId, userId) + .set(TtUser::getLoginIp, IpUtils.getIpAddr()) + .set(TtUser::getLoginDate, DateUtils.getNowDate()) + .update(); + } catch (Exception e) { + log.error("更新用户登录信息失败, userId={}", userId, e); + } + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiMessageServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiMessageServiceImpl.java new file mode 100644 index 0000000..2c77cc3 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiMessageServiceImpl.java @@ -0,0 +1,59 @@ +package com.ruoyi.user.service.impl; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.domain.other.TtMessageSend; +import com.ruoyi.admin.service.TtMessageSendService; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.user.mapper.ApiMessageMapper; +import com.ruoyi.user.service.ApiMessageService; +import com.ruoyi.domain.vo.ApiMessageDataVO; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ApiMessageServiceImpl implements ApiMessageService { + + private final ApiMessageMapper apiMessageMapper; + private final TtMessageSendService messageSendService; + + public ApiMessageServiceImpl(ApiMessageMapper apiMessageMapper, + TtMessageSendService messageSendService) { + this.apiMessageMapper = apiMessageMapper; + this.messageSendService = messageSendService; + } + + @Override + public List getMessageList(Long userId, Integer id) { + return apiMessageMapper.getMessageList(userId, id); + } + + @Override + public ApiMessageDataVO view(Long userId, Integer id) { + TtMessageSend ttMessageSend = messageSendService.getById(id); + ttMessageSend.setStatus("1"); + ttMessageSend.setReadingTime(DateUtils.getNowDate()); + messageSendService.updateById(ttMessageSend); + List messageList = apiMessageMapper.getMessageList(userId, id); + if (messageList.isEmpty()) return null; + return messageList.get(0); + } + + @Override + public String batchOperation(Long userId, List ids, String status) { + List list = new LambdaQueryChainWrapper<>(messageSendService.getBaseMapper()) + .eq(TtMessageSend::getRecId, userId) + .in(TtMessageSend::getId, ids) + .list(); + list = list.stream().peek(ttMessageSend -> { + ttMessageSend.setStatus(status); + if (StringUtils.isNull(ttMessageSend.getReadingTime())) { + ttMessageSend.setReadingTime(DateUtils.getNowDate()); + } + }).collect(Collectors.toList()); + messageSendService.updateBatchById(list, 1); + return ""; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiNoticeServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiNoticeServiceImpl.java new file mode 100644 index 0000000..46cd55e --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiNoticeServiceImpl.java @@ -0,0 +1,72 @@ +package com.ruoyi.user.service.impl; + +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.domain.other.TtNotice; +import com.ruoyi.user.mapper.ApiNoticeMapper; +import com.ruoyi.user.model.vo.ApiNoticeVO; +import com.ruoyi.user.service.ApiNoticeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; + +@Service +public class ApiNoticeServiceImpl implements ApiNoticeService { + + @Autowired + private ApiNoticeMapper apiNoticeMapper; + + @Override + public List getNoticeList(Long userId) { + return apiNoticeMapper.getNoticeList(userId); + } + + @Override + public List getNoticeListByCategoryPrefix(Long userId, String categoryPrefix) { + return apiNoticeMapper.getNoticeListByCategoryPrefix(userId, categoryPrefix); + } + + @Override + public ApiNoticeVO getNoticeByNoticeId(Long userId, Integer noticeId) { + // 读取详情后更新为已读状态 + TtNotice ttNotice = new TtNotice(); + ttNotice.setNoticeId(noticeId); + ttNotice.setRead("1"); + editNotice(ttNotice); + return apiNoticeMapper.getNoticeByNoticeId(userId, noticeId); + } + + @Override + public int countUnreadNotice(Long userId) { + return apiNoticeMapper.countUnreadNotice(userId); + } + + @Override + public int addNotice(TtNotice ttNotice) { + ttNotice.setCreateTime(new Date()); + return apiNoticeMapper.addNotice(ttNotice); + } + + @Override + public int editNotice(TtNotice ttNotice) { + return apiNoticeMapper.editNotice(ttNotice); + } + + @Override + public int removeNoticeByNoticeId(Integer noticeId) { + return apiNoticeMapper.removeNoticeByNoticeId(noticeId); + } + + @Override + public int markNoticesAsReadByCategory(String categoryPrefix) { + Long userId = SecurityUtils.getUserId(); + if (userId == null) { + throw new RuntimeException("用户未登录"); + } + if (categoryPrefix == null || categoryPrefix.isEmpty()) { + throw new RuntimeException("分类前缀不能为空"); + } + return apiNoticeMapper.markNoticesAsReadByCategory(userId, categoryPrefix); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiProfileRecordsServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiProfileRecordsServiceImpl.java new file mode 100644 index 0000000..0e87ded --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiProfileRecordsServiceImpl.java @@ -0,0 +1,457 @@ +package com.ruoyi.user.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.TtRechargeRecordMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.dto.userRecord.ProfileRecordsRequest; +import com.ruoyi.domain.entity.BoxTransferRecord; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.domain.other.TtRechargeRecord; +import com.ruoyi.domain.vo.userRecord.ProfileRecordItemVO; +import com.ruoyi.domain.vo.userRecord.ProfileRecordsResponse; +import com.ruoyi.user.mapper.ApiBoxTransferRecordMapper; +import com.ruoyi.user.service.ApiProfileRecordsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 个人资产详情 - 我的记录 聚合查询实现。 + * 一个接口承载 4 个 Tab:余额/充值/提货/赠送,按 tab 分发到对应 handler。 + */ +@Slf4j +@Service +public class ApiProfileRecordsServiceImpl implements ApiProfileRecordsService { + + /** 余额-充值 subFilter=2 覆盖的 source 集合 */ + private static final Set BALANCE_RECHARGE_SOURCES = Set.of( + 1 + ); + + /** 余额-奖励 subFilter=5 覆盖的 source 集合 */ + private static final Set BALANCE_REWARD_SOURCES = Set.of( + 3, 5, 7, 9, 11, 13, 14, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + 37, 39, 41, 43, 44, 46, 47, 48, 49, 52, 56, + // 61=玫瑰女郎奖励, 62=招商周结佣金, 63=流水抽奖奖励 + 61, 62, 63 + ); + + /** 余额-消费 subFilter=1 覆盖的 source 集合(59=主播Roll房奖池扣款) */ + private static final Set BALANCE_CONSUMPTION_SOURCES = Set.of( + 2, 4, 6, 8, 12, 14, 33, 34, 38, 40, 42, 45, 50, 59, + // 53=主播创建CDKEY消费, 57=后台生成主播CDK红包扣款, 60=玫瑰女郎消费 + 53, 57, 60 + ); + + /** 余额-结算 subFilter=4 覆盖的 source 集合 */ + private static final Set BALANCE_SETTLEMENT_SOURCES = Set.of( + 10, 16, + // 54=主播CDKEY结束退还, 55=卡商销毁卡密退还, 64=疯狂转盘注单退款(兜底结算专用) + 54, 55, 64 + ); + + /** source -> 类型文案 */ + private static final Map SOURCE_MSG_MAP; + + /** 分页默认每页条数 */ + private static final int DEFAULT_PAGE_SIZE = 20; + + /** 分页每页最大条数 */ + private static final int MAX_PAGE_SIZE = 100; + + static { + Map m = new HashMap<>(); + for (TtAccountRecordSource s : TtAccountRecordSource.values()) { + m.put(s.getCode(), s.getMsg()); + } + SOURCE_MSG_MAP = Collections.unmodifiableMap(m); + } + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private TtRechargeRecordMapper ttRechargeRecordMapper; + + @Autowired + private com.ruoyi.admin.mapper.TtDeliveryRecordMapper ttDeliveryRecordMapper; + + @Autowired + private TtOrnamentMapper ttOrnamentMapper; + + @Autowired + private TtBoxRecordsMapper ttBoxRecordsMapper; + + @Autowired + private ApiBoxTransferRecordMapper boxTransferRecordMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Override + public ProfileRecordsResponse queryRecords(Integer userId, ProfileRecordsRequest request) { + Timestamp[] range = resolveTimeRange(request.getTimeRange()); + Timestamp beginTime = range[0]; + Timestamp endTime = range[1]; + + switch (request.getTab()) { + case 1: + return queryBalanceRecords(userId, beginTime, endTime, request); + case 2: + return queryRechargeRecords(userId, beginTime, endTime, request); + case 3: + return queryDeliveryRecords(userId, beginTime, endTime, request); + case 4: + return queryTransferRecords(userId, beginTime, endTime, request); + default: + return emptyResponse(request); + } + } + + /* ===================== Tab 1 余额记录 ===================== */ + + private ProfileRecordsResponse queryBalanceRecords(Integer userId, Timestamp beginTime, + Timestamp endTime, ProfileRecordsRequest request) { + // 不传或 0 视为全部;1=消费 2=充值 4=结算 5=奖励,均按 source 集合过滤 + Integer subFilter = request.getSubFilter() == null ? 0 : request.getSubFilter(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtUserBlendErcash::getUserId, userId) + .between(TtUserBlendErcash::getCreateTime, beginTime, endTime) + // 过滤金币与积分双零的脏数据;某些结算收入(如分解饰品、CDK)仅写 credits 列,应该也要展示 + .and(w -> w.ne(TtUserBlendErcash::getAmount, BigDecimal.ZERO) + .or().ne(TtUserBlendErcash::getCredits, BigDecimal.ZERO)) + // 排除 Roll房未发放退回主播的纯积分记录(source=56 且金币额为0):主播余额记录只展示金币退回,不展示积分退回 + .and(w -> w.ne(TtUserBlendErcash::getSource, TtAccountRecordSource.ROLL_UNISSUED_REFUND.getCode()) + .or().ne(TtUserBlendErcash::getAmount, BigDecimal.ZERO)); + + if (subFilter == 1) { // 消费 + wrapper.in(TtUserBlendErcash::getSource, BALANCE_CONSUMPTION_SOURCES); + } else if (subFilter == 2) { // 充值 + wrapper.in(TtUserBlendErcash::getSource, BALANCE_RECHARGE_SOURCES); + } else if (subFilter == 4) { // 结算 + wrapper.in(TtUserBlendErcash::getSource, BALANCE_SETTLEMENT_SOURCES); + } else if (subFilter == 5) { // 奖励 + wrapper.in(TtUserBlendErcash::getSource, BALANCE_REWARD_SOURCES); + } + // subFilter==0/null/其他值 → 全部,不附加 source 过滤 + wrapper.orderByDesc(TtUserBlendErcash::getCreateTime); + + IPage page = ttUserBlendErcashMapper.selectPage(buildPage(request), wrapper); + List records = page.getRecords(); + + List items = records.stream().map(e -> { + ProfileRecordItemVO vo = new ProfileRecordItemVO(); + vo.setRecordTime(e.getCreateTime()); + vo.setTypeCode(e.getSource()); + vo.setType(SOURCE_MSG_MAP.getOrDefault(e.getSource(), "其他")); + + // DB 中 amount/credits 已带正确符号(收入正、支出负),直接使用无需 abs/negate + BigDecimal amount = e.getAmount(); + if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) { + amount = e.getCredits() == null ? BigDecimal.ZERO : e.getCredits(); + } + vo.setAmount(amount); + vo.setBalance(e.getFinalAmount()); + return vo; + }).collect(Collectors.toList()); + + return buildResponse(items, page.getTotal(), request, null); + } + + /* ===================== Tab 2 充值记录 ===================== */ + + private ProfileRecordsResponse queryRechargeRecords(Integer userId, Timestamp beginTime, + Timestamp endTime, ProfileRecordsRequest request) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtRechargeRecord::getUserId, userId) + .eq(TtRechargeRecord::getStatus, "0") // 仅查询充值成功记录 + // 白号空白卡密充值记录(is_blank_card=1)需在 cocos 展示,故此处不再过滤;后台按 is_blank_card 过滤隐藏 + .between(TtRechargeRecord::getCreateTime, beginTime, endTime) + .orderByDesc(TtRechargeRecord::getCreateTime); + + IPage page = ttRechargeRecordMapper.selectPage(buildPage(request), wrapper); + List records = page.getRecords(); + + List items = records.stream().map(e -> { + ProfileRecordItemVO vo = new ProfileRecordItemVO(); + vo.setRecordTime(e.getCreateTime()); + vo.setOrderNo(e.getOrderId()); + vo.setAmount(e.getAmountActuallyPaid()); + return vo; + }).collect(Collectors.toList()); + + // 统计:当前时间段内所有成功充值的总金额(不分页) + BigDecimal totalAmount = sumRechargeAmount(userId, beginTime, endTime); + ProfileRecordsResponse.SummaryVO summary = new ProfileRecordsResponse.SummaryVO(); + summary.setTotalAmount(totalAmount); + return buildResponse(items, page.getTotal(), request, summary); + } + + private BigDecimal sumRechargeAmount(Integer userId, Timestamp beginTime, Timestamp endTime) { + QueryWrapper qw = new QueryWrapper<>(); + qw.select("IFNULL(SUM(amount_actually_paid), 0) AS total") + .eq("user_id", userId) + .eq("status", "0") + // 白号空白卡密充值需计入 cocos 充值总额,故此处不再过滤 is_blank_card + .between("create_time", beginTime, endTime); + Object total = ttRechargeRecordMapper.selectMaps(qw).stream() + .findFirst() + .map(m -> m.get("total")) + .orElse(null); + return total == null ? BigDecimal.ZERO : new BigDecimal(total.toString()); + } + + /* ===================== Tab 3 提货记录 ===================== */ + + private ProfileRecordsResponse queryDeliveryRecords(Integer userId, Timestamp beginTime, + Timestamp endTime, ProfileRecordsRequest request) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TtDeliveryRecord::getUserId, userId) + .between(TtDeliveryRecord::getCreateTime, beginTime, endTime) + .orderByDesc(TtDeliveryRecord::getCreateTime); + + IPage page = ttDeliveryRecordMapper.selectPage(buildPage(request), wrapper); + List records = page.getRecords(); + + // 批量查饰品信息(全名 + 图片) + Set ornamentIds = records.stream() + .map(TtDeliveryRecord::getOrnamentId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(HashSet::new)); + Map ornamentMap = ornamentIds.isEmpty() + ? Collections.emptyMap() + : new LambdaQueryChainWrapper<>(ttOrnamentMapper) + .in(TtOrnament::getId, ornamentIds) + .list().stream() + .collect(Collectors.toMap(TtOrnament::getId, o -> o, (a, b) -> a)); + + // 批量查 box_records 获取品质图片 + Set boxRecordIds = records.stream() + .map(TtDeliveryRecord::getBoxRecordsId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(HashSet::new)); + Map levelImgMap = boxRecordIds.isEmpty() + ? Collections.emptyMap() + : ttBoxRecordsMapper.selectBatchIds(boxRecordIds).stream() + .filter(br -> br.getOrnamentLevelImg() != null) + .collect(Collectors.toMap(TtBoxRecords::getId, TtBoxRecords::getOrnamentLevelImg, (a, b) -> a)); + + List items = records.stream().map(e -> { + ProfileRecordItemVO vo = new ProfileRecordItemVO(); + vo.setRecordTime(e.getCreateTime()); + vo.setAmount(e.getOrnamentsPrice()); + // 饰品全名 + 图片 + TtOrnament ornament = ornamentMap.get(e.getOrnamentId()); + if (ornament != null) { + vo.setOrnamentName(ornament.getName()); + vo.setOrnamentImg(ornament.getImageUrl()); + } + // 品质图片 + if (e.getBoxRecordsId() != null) { + vo.setOrnamentLevelImg(levelImgMap.get(e.getBoxRecordsId())); + } + return vo; + }).collect(Collectors.toList()); + + // 统计:当前时间段内所有提货金额总和(不分页) + BigDecimal totalAmount = sumDeliveryAmount(userId, beginTime, endTime); + ProfileRecordsResponse.SummaryVO summary = new ProfileRecordsResponse.SummaryVO(); + summary.setTotalAmount(totalAmount); + return buildResponse(items, page.getTotal(), request, summary); + } + + private BigDecimal sumDeliveryAmount(Integer userId, Timestamp beginTime, Timestamp endTime) { + QueryWrapper qw = new QueryWrapper<>(); + qw.select("IFNULL(SUM(ornaments_price), 0) AS total") + .eq("user_id", userId) + .between("create_time", beginTime, endTime); + Object total = ttDeliveryRecordMapper.selectMaps(qw).stream() + .findFirst() + .map(m -> m.get("total")) + .orElse(null); + return total == null ? BigDecimal.ZERO : new BigDecimal(total.toString()); + } + + /* ===================== Tab 4 赠送记录 ===================== */ + + private ProfileRecordsResponse queryTransferRecords(Integer userId, Timestamp beginTime, + Timestamp endTime, ProfileRecordsRequest request) { + Integer subFilter = request.getSubFilter() == null ? 0 : request.getSubFilter(); + long uid = userId.longValue(); + + IPage page = boxTransferRecordMapper.selectPage( + buildPage(request), buildTransferWrapper(userId, subFilter, beginTime, endTime)); + List records = page.getRecords(); + + // 批量收集对方 userId,一次性查询昵称,避免 N+1 + Set counterpartyIds = new HashSet<>(); + for (BoxTransferRecord e : records) { + Long counterparty = Objects.equals(e.getSrcUid(), uid) ? e.getDstUid() : e.getSrcUid(); + if (counterparty != null) { + counterpartyIds.add(counterparty.intValue()); + } + } + Map nickNameMap = counterpartyIds.isEmpty() + ? Collections.emptyMap() + : ttUserMapper.selectBatchIds(counterpartyIds).stream() + .collect(Collectors.toMap(TtUser::getUserId, TtUser::getNickName, (a, b) -> a)); + + List items = records.stream().map(e -> { + ProfileRecordItemVO vo = new ProfileRecordItemVO(); + // BoxTransferRecord.createTime 为 LocalDateTime,转 Date 保持 VO 统一 + if (e.getCreateTime() != null) { + vo.setRecordTime(Date.from(e.getCreateTime().atZone(ZoneId.systemDefault()).toInstant())); + } + vo.setOrnamentName(e.getOrnamentName()); + vo.setOrnamentImg(e.getImageUrl()); + vo.setOrnamentLevelImg(e.getOrnamentLevelImg()); + vo.setAmount(e.getOrnamentsPrice()); + int direction = Objects.equals(e.getSrcUid(), uid) ? 1 : 2; + vo.setDirection(direction); + vo.setTypeCode(direction); + vo.setType(direction == 1 ? "赠送" : "受赠"); + // 对方昵称及用户ID:赠送时为受赠人,受赠时为赠送人 + Long counterparty = direction == 1 ? e.getDstUid() : e.getSrcUid(); + if (counterparty != null) { + vo.setNickName(nickNameMap.get(counterparty.intValue())); + } + vo.setCounterpartyId(counterparty); + // 转赠记录三态:0-审核中,1-成功,2-失败 + Integer status = e.getStatus(); + if (status == null) { + vo.setStatus("审核中"); + } else { + switch (status) { + case 0: vo.setStatus("审核中"); break; + case 1: vo.setStatus("已完成"); break; // 审核通过标记为"已完成" + case 2: vo.setStatus("已驳回"); break; + default: vo.setStatus("未知状态"); break; + } + } + return vo; + }).collect(Collectors.toList()); + + ProfileRecordsResponse.SummaryVO summary = new ProfileRecordsResponse.SummaryVO(); + // completedCount = status=1(成功) + status=2(失败),pendingCount = status 为 null 或 0(审核中);统计整个时间段,不随分页变化 + long completedCount = boxTransferRecordMapper.selectCount( + buildTransferWrapper(userId, subFilter, beginTime, endTime) + .in(BoxTransferRecord::getStatus, 1, 2)); + long pendingCount = boxTransferRecordMapper.selectCount( + buildTransferWrapper(userId, subFilter, beginTime, endTime) + .and(w -> w.isNull(BoxTransferRecord::getStatus).or().eq(BoxTransferRecord::getStatus, 0))); + summary.setCompletedCount(completedCount); + summary.setPendingCount(pendingCount); + return buildResponse(items, page.getTotal(), request, summary); + } + + /** + * 构建赠送记录查询条件(分页查询与统计计数共用)。 + * 白号假转赠:作为受赠方(dstUid)时不展示来自白号的转赠记录;作为赠送方(srcUid)不受影响。 + */ + private LambdaQueryWrapper buildTransferWrapper(Integer userId, Integer subFilter, + Timestamp beginTime, Timestamp endTime) { + long uid = userId.longValue(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(BoxTransferRecord::getCreateTime, beginTime.toLocalDateTime(), endTime.toLocalDateTime()); + + // 假号(白号 is_blank_account=1 / 机器人 user_type='03')来源的转赠记录不展示给接收方(假转赠) + final String BLANK_SRC_EXCLUDE = + "SELECT user_id FROM tt_user WHERE is_blank_account = 1 OR user_type = '03'"; + if (subFilter == 1) { + wrapper.eq(BoxTransferRecord::getSrcUid, uid); + } else if (subFilter == 2) { + wrapper.eq(BoxTransferRecord::getDstUid, uid) + .notInSql(BoxTransferRecord::getSrcUid, BLANK_SRC_EXCLUDE); + } else { + wrapper.and(w -> w.eq(BoxTransferRecord::getSrcUid, uid) + .or(x -> x.eq(BoxTransferRecord::getDstUid, uid) + .notInSql(BoxTransferRecord::getSrcUid, BLANK_SRC_EXCLUDE))); + } + return wrapper.orderByDesc(BoxTransferRecord::getCreateTime); + } + + /* ===================== 工具方法 ===================== */ + + /** + * 解析时间区间。 + * 1=今日(0:00 ~ 当前) + * 2=昨日(昨日0:00 ~ 昨日23:59:59) + * 3=近7日(7天前0:00 ~ 当前);不传 timeRange 也走这里,即接口最多只返回近 7 天数据 + */ + private Timestamp[] resolveTimeRange(Integer timeRange) { + // 不传或非法值统一按 近7日 处理,保证接口任何情况下都不会返回 7 天以前的数据 + int range = (timeRange == null) ? 3 : timeRange; + LocalDate today = LocalDate.now(); + LocalDateTime begin; + LocalDateTime end; + if (range == 1) { + begin = today.atStartOfDay(); + end = LocalDateTime.now(); + } else if (range == 2) { + LocalDate yesterday = today.minusDays(1); + begin = yesterday.atStartOfDay(); + end = yesterday.atTime(LocalTime.MAX); + } else { + begin = today.minusDays(6).atStartOfDay(); + end = LocalDateTime.now(); + } + return new Timestamp[]{Timestamp.valueOf(begin), Timestamp.valueOf(end)}; + } + + /** 构建分页对象:pageNum 默认 1,pageSize 默认 20 且上限 100 */ + private Page buildPage(ProfileRecordsRequest request) { + int pageNum = (request.getPageNum() == null || request.getPageNum() < 1) ? 1 : request.getPageNum(); + int pageSize = (request.getPageSize() == null || request.getPageSize() < 1) + ? DEFAULT_PAGE_SIZE : Math.min(request.getPageSize(), MAX_PAGE_SIZE); + return new Page<>(pageNum, pageSize); + } + + private ProfileRecordsResponse buildResponse(List items, + Long total, + ProfileRecordsRequest request, + ProfileRecordsResponse.SummaryVO summary) { + ProfileRecordsResponse response = new ProfileRecordsResponse(); + response.setList(items == null ? new ArrayList<>() : items); + response.setTotal(total == null ? 0L : total); + response.setSummary(summary); + return response; + } + + private ProfileRecordsResponse emptyResponse(ProfileRecordsRequest request) { + ProfileRecordsResponse response = new ProfileRecordsResponse(); + response.setList(new ArrayList<>()); + response.setTotal(0L); + return response; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiPromotionRecordServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiPromotionRecordServiceImpl.java new file mode 100644 index 0000000..454bf22 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiPromotionRecordServiceImpl.java @@ -0,0 +1,65 @@ +package com.ruoyi.user.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtPromotionRecordMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.domain.entity.TtPromotionRecord; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.promotion.TtPromotionRecordVo; +import com.ruoyi.user.service.ApiPromotionRecordService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class ApiPromotionRecordServiceImpl implements ApiPromotionRecordService { + + @Autowired + private TtPromotionRecordMapper ttPromotionRecordMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Override + public List getPromotionRecord(Integer userId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(TtPromotionRecord::getUserId, userId); + wrapper.orderByDesc(TtPromotionRecord::getId); + List list = ttPromotionRecordMapper.selectList(wrapper); + if (CollectionUtils.isEmpty(list)) { + return Collections.emptyList(); + } + List vos = new ArrayList<>(); + List userIds = new ArrayList<>(); + for (TtPromotionRecord record : list) { + userIds.add(record.getSubordinateUserId()); + TtPromotionRecordVo vo = new TtPromotionRecordVo(); + BeanUtils.copyProperties(record, vo); + if (record.getRebate() == null || record.getRebate().compareTo(new BigDecimal(0)) <= 0) { + vo.setCommissions(new BigDecimal(0)); + } else if (record.getRechargePrice().compareTo(new BigDecimal(0)) > 0) { + vo.setCommissions(record.getRebate().multiply(BigDecimal.valueOf(100)) + .divide(record.getRechargePrice(), 1, RoundingMode.HALF_UP)); + } + vos.add(vo); + } + List ttUsers = ttUserMapper.selectBatchIds(userIds); + Map ttUserMap = ttUsers.stream().filter(Objects::nonNull).collect(Collectors.toMap( + TtUser::getUserId, // 使用TtUser的userId作为Map的键 + TtUser::getNickName // 使用TtUser的username作为Map的值 + )); + for (TtPromotionRecordVo vo : vos) { + vo.setSubordinateUserName(ttUserMap.getOrDefault(vo.getSubordinateUserId(), "")); + } + return vos; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiRegisterServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiRegisterServiceImpl.java new file mode 100644 index 0000000..c4b5119 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiRegisterServiceImpl.java @@ -0,0 +1,137 @@ +package com.ruoyi.user.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.NumberUtil; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.service.TtUserAvatarService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.utils.*; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.sys.TtPromotionUpdate; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.ApiRegisterBody; +import com.ruoyi.domain.other.TtUserAvatar; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.baidu.service.BaiduService; +import com.ruoyi.thirdparty.common.service.ApiSmsService; +import com.ruoyi.user.service.ApiRegisterService; +import com.ruoyi.admin.service.UserIdGenerator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.List; + +@Service +@Slf4j +public class ApiRegisterServiceImpl implements ApiRegisterService { + + @Autowired + private TtUserAvatarService userAvatarService; + @Autowired + private TtUserService userService; + @Autowired + private ISysConfigService configService; + @Autowired + private ApiSmsService apiSmsService; + + @Autowired + private TtPromotionUpdateMapper ttPromotionUpdateMapper; + + @Autowired + private BaiduService baiduService; + + @Autowired + private UserIdGenerator userIdGenerator; + + @Override + public String register(ApiRegisterBody registerBody) { + String nickName = registerBody.getNickName(), phoneNumber = registerBody.getPhoneNumber(), + password = registerBody.getPassword(), parentInvitationCode = registerBody.getParentInvitationCode(), + code = registerBody.getCode(); + if (StringUtils.isEmpty(nickName) || nickName.length() > UserConstants.NICKNAME_MAX_LENGTH) { + return "昵称必须在1到7个字符之间"; + } + if (StringUtils.isEmpty(phoneNumber)) return "手机号不能为空"; + if (!Validator.isMobile(phoneNumber)) return "手机号格式错误,请检查手机号是否输入正确!"; + if (StringUtils.isEmpty(password)) return "用户密码不能为空"; + if (StringUtils.isEmpty(code)) return "验证码不能为空"; + //// TODO: 上线前恢复 → if (!NumberUtil.isNumber(code) || code.trim().length() != 4) return "验证码错误"; + //// 临时降级:验证码改为与密码一致即可通过 + if (!code.trim().equals(password)) return "验证码错误,请输入与密码相同的内容"; + //if (!NumberUtil.isNumber(code) || code.trim().length() != 4) return "验证码错误"; + TtUser ttUser = TtUser.builder().build(); + ttUser.setUserName(phoneNumber); + ttUser.setEmail(phoneNumber + "@qq.com"); + ttUser.setPhoneNumber(phoneNumber); + ttUser.setAccountAmount(BigDecimal.ZERO); + ttUser.setAccountCredits(BigDecimal.ZERO); + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { + return "密码长度必须在5到20个字符之间"; + } + if (!userService.checkPhoneUnique(ttUser)) return "注册失败," + "手机号'" + phoneNumber + "'已被注册!"; + if (!userService.checkUserNameUnique(ttUser)) + return "注册失败," + "用户名'" + ttUser.getUserName() + "'已存在!"; + ttUser.setNickName(nickName); + ttUser.setUserType("02"); + List userAvatarList = new LambdaQueryChainWrapper<>(userAvatarService.getBaseMapper()).eq(TtUserAvatar::getIsDefault, "1").list(); + if (!userAvatarList.isEmpty()) { + ttUser.setAvatar(RandomUtil.choice(userAvatarList).getAvatar()); + } else ttUser.setAvatar(""); + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + String registerRedPacketStr = configService.selectConfigByKey("registerRedPacket"); + BigDecimal registerRedPacket = new BigDecimal(registerRedPacketStr); + ttUser.setAccountAmount(registerRedPacket); + ttUser.setInvitationCode(userService.getInvitationCode().toLowerCase()); + if (StringUtils.isNotEmpty(parentInvitationCode) && parentInvitationCode.trim().length() == 6) { + TtUser parentUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase()) + .eq(TtUser::getDelFlag, "0").one(); + if (StringUtils.isNull(parentUser)) { + return "上级邀请码填写错误!"; + } else { + + } + ttUser.setParentId(parentUser.getUserId()); + } + ttUser.setIsRealCheck("0"); + ttUser.setCreateBy("网站注册"); + ttUser.setCreateTime(DateUtils.getNowDate()); + ttUser.setRemark("明文密码:" + password); + // ===== 改后 ===== +// TODO: 上线前恢复 → +// String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "ApiRegister_" + phoneNumber); +// if (!"success".equals(validateCaptcha)) return validateCaptcha; + //String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "ApiRegister_" + phoneNumber); + //if (!"success".equals(validateCaptcha)) return validateCaptcha; + ttUser.setUserId(userIdGenerator.generateUserId()); + boolean regFlag = userService.save(ttUser); + if (!regFlag) return "注册失败,请联系系统管理人员"; + else { + if (BigDecimal.ZERO.compareTo(registerRedPacket) < 0) { + userService.insertUserAmountRecords(ttUser.getUserId(), TtAccountRecordType.INPUT, TtAccountRecordSource.REGIST_AWARD, registerRedPacket, ttUser.getAccountAmount()); + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor("api_" + ttUser.getUserName(), Constants.REGISTER, MessageUtils.message("user.register.success"))); + + // 保存推广更新记录 + TtPromotionUpdate build = TtPromotionUpdate.builder() + .employeeId(ttUser.getUserId()) + .bossId(ttUser.getParentId()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttPromotionUpdateMapper.insert(build); +// baiduService.upload(ttUser.getUserId(), 49, registerBody.getBdVid()); + } + return ""; + } + +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiShareServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiShareServiceImpl.java new file mode 100644 index 0000000..861c6da --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiShareServiceImpl.java @@ -0,0 +1,222 @@ +package com.ruoyi.user.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.admin.mapper.TtPromotionRecordMapper; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.entity.TtPromotionRecord; +import com.ruoyi.domain.entity.sys.TtPromotionUpdate; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.vo.share.ShareCenterVO; +import com.ruoyi.domain.vo.share.ShareRecordPageVO; +import com.ruoyi.domain.vo.share.ShareRecordVO; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.service.ApiShareService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * 主页"游戏分享"模块服务实现 + * + * 数据来源: + * - 充值返佣记录:tt_promotion_record(每次下级充值后由 TtBonusServiceImpl 写入) + * - 邀请绑定记录:tt_promotion_update(注册或补绑定时写入) + */ +@Slf4j +@Service +public class ApiShareServiceImpl implements ApiShareService { + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtPromotionRecordMapper ttPromotionRecordMapper; + + @Autowired + private TtPromotionUpdateMapper ttPromotionUpdateMapper; + + @Autowired + private ISysConfigService sysConfigService; + + /** SysConfig 中分享链接模板的 key(含 ${code} 占位符) */ + private static final String SHARE_KEY_ZHUBO = "app.zhubo.sharelink"; + private static final String SHARE_KEY_ZHAOSHANG = "app.zhaoshang.sharelink"; + private static final String SHARE_KEY_USER = "app.user.sharelink"; + + /** 邀请绑定固定奖励(可选 SysConfig 配置;未配置或非数字则视为 0) */ + private static final String INVITE_BIND_REWARD_KEY = "invite.bind.reward"; + + @Override + public ShareCenterVO getShareCenter(Long userId) { + ShareCenterVO vo = new ShareCenterVO(); + TtUser user = ttUserMapper.selectById(userId); + if (user == null) { + vo.setInvitationCode(""); + vo.setTodayEarnings(BigDecimal.ZERO); + vo.setTotalEarnings(BigDecimal.ZERO); + vo.setInviteCount(0); + vo.setShareLink(""); + vo.setCommissionRate("0%"); + return vo; + } + vo.setInvitationCode(user.getInvitationCode()); + + // 查询当前用户全部充值返佣记录,内存聚合今日/累计收益(避免新增 mapper 方法) + List records = ttPromotionRecordMapper.selectList( + Wrappers.lambdaQuery() + .eq(TtPromotionRecord::getUserId, user.getUserId())); + BigDecimal total = BigDecimal.ZERO; + BigDecimal today = BigDecimal.ZERO; + Date startOfDay = startOfToday(); + if (CollectionUtils.isNotEmpty(records)) { + for (TtPromotionRecord r : records) { + if (r.getRebate() == null) continue; + total = total.add(r.getRebate()); + if (r.getCreateTime() != null && !r.getCreateTime().before(startOfDay)) { + today = today.add(r.getRebate()); + } + } + } + vo.setTodayEarnings(today); + vo.setTotalEarnings(total); + + // 邀请人数:以 tt_promotion_update 中绑定关系为准 + Long bindCount = ttPromotionUpdateMapper.selectCount( + Wrappers.lambdaQuery() + .eq(TtPromotionUpdate::getBossId, user.getUserId()) + .isNotNull(TtPromotionUpdate::getEmployeeId)); + vo.setInviteCount(bindCount == null ? 0 : bindCount.intValue()); + + vo.setShareLink(buildShareLink(user)); + vo.setCommissionRate( + (user.getCommissionRate() == null || user.getCommissionRate().compareTo(BigDecimal.ZERO) == 0) + ? "0%" + : user.getCommissionRate().stripTrailingZeros().toPlainString() + "%"); + return vo; + } + + @Override + public ShareRecordPageVO listShareRecords(Long userId, Long subUserId, int pageNum, int pageSize) { + ShareRecordPageVO resp = new ShareRecordPageVO(); + if (pageNum <= 0) pageNum = 1; + if (pageSize <= 0) pageSize = 10; + + // 1) 充值返佣记录 + LambdaQueryWrapper recordWrapper = Wrappers.lambdaQuery() + .eq(TtPromotionRecord::getUserId, userId.intValue()); + if (subUserId != null && subUserId > 0) { + recordWrapper.eq(TtPromotionRecord::getSubordinateUserId, subUserId); + } + List records = ttPromotionRecordMapper.selectList(recordWrapper); + + // 2) 邀请绑定记录 + LambdaQueryWrapper bindWrapper = Wrappers.lambdaQuery() + .eq(TtPromotionUpdate::getBossId, userId.intValue()) + .isNotNull(TtPromotionUpdate::getEmployeeId); + if (subUserId != null && subUserId > 0) { + bindWrapper.eq(TtPromotionUpdate::getEmployeeId, subUserId); + } + List binds = ttPromotionUpdateMapper.selectList(bindWrapper); + + // 邀请奖励读取(无配置或非法值则按 0 处理) + BigDecimal inviteReward = readInviteBindReward(); + + // 3) 合并到统一 VO 列表 + List all = new ArrayList<>( + (records == null ? 0 : records.size()) + (binds == null ? 0 : binds.size())); + BigDecimal totalRebate = BigDecimal.ZERO; + if (CollectionUtils.isNotEmpty(records)) { + for (TtPromotionRecord r : records) { + ShareRecordVO row = new ShareRecordVO(); + row.setSubUserId(r.getSubordinateUserId()); + row.setStatus("充值"); + row.setRechargePrice(r.getRechargePrice() == null ? "--" : r.getRechargePrice().toPlainString()); + row.setRebate(r.getRebate() == null ? BigDecimal.ZERO : r.getRebate()); + row.setCreateTime(r.getCreateTime()); + all.add(row); + if (r.getRebate() != null) totalRebate = totalRebate.add(r.getRebate()); + } + } + if (CollectionUtils.isNotEmpty(binds)) { + for (TtPromotionUpdate b : binds) { + ShareRecordVO row = new ShareRecordVO(); + row.setSubUserId(b.getEmployeeId()); + row.setStatus("邀请"); + row.setRechargePrice("--"); // 邀请行无充值金额 + row.setRebate(inviteReward); + row.setCreateTime(b.getCreateTime() == null ? null : new Date(b.getCreateTime().getTime())); + all.add(row); + totalRebate = totalRebate.add(inviteReward); + } + } + + // 4) 时间倒序排序 + 内存分页 + all.sort(Comparator.comparing( + ShareRecordVO::getCreateTime, + Comparator.nullsLast(Comparator.reverseOrder()))); + long total = all.size(); + int from = Math.min((pageNum - 1) * pageSize, all.size()); + int to = Math.min(from + pageSize, all.size()); + List page = from >= to ? Collections.emptyList() : new ArrayList<>(all.subList(from, to)); + + resp.setTotalRebate(totalRebate); + resp.setTotal(total); + resp.setList(page); + return resp; + } + + /** 构造分享链接:根据用户类型选择 SysConfig 中的模板,将 ${code} 替换为邀请码 */ + private String buildShareLink(TtUser user) { + String code = StringUtils.isEmpty(user.getInvitationCode()) ? "" : user.getInvitationCode(); + // 根据用户类型选择对应的分享链接模板 + String key; + if (UserType.ANCHOR.getCode().equals(user.getUserType())) { + key = SHARE_KEY_ZHUBO; + } else if (UserType.BUSINESS_USER.getCode().equals(user.getUserType())) { + key = SHARE_KEY_ZHAOSHANG; + } else { + key = SHARE_KEY_USER; + } + String template = sysConfigService.selectConfigByKey(key); + if (StringUtils.isEmpty(template)) return ""; + return template.replace("${code}", code); + } + + /** 读取邀请绑定固定奖励配置;不存在或解析失败返回 0 */ + private BigDecimal readInviteBindReward() { + try { + String v = sysConfigService.selectConfigByKey(INVITE_BIND_REWARD_KEY); + if (StringUtils.isEmpty(v)) return BigDecimal.ZERO; + return new BigDecimal(v.trim()); + } catch (Exception e) { + return BigDecimal.ZERO; + } + } + + /** 取今天 00:00:00 时间点 */ + private Date startOfToday() { + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTime(); + } + + @SuppressWarnings("unused") + private boolean nonNull(Object o) { return Objects.nonNull(o); } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiShoppingImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiShoppingImpl.java new file mode 100644 index 0000000..67a9585 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiShoppingImpl.java @@ -0,0 +1,288 @@ +package com.ruoyi.user.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtOrnamentMapper; +import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.mapper.TtUserMapper; +import com.ruoyi.admin.service.TtOrnamentService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.other.TtOrnamentsLevel; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.TtOrnament; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.ApiShoppingBody; +import com.ruoyi.domain.vo.ApiShoppingDataVO; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.mapper.ApiShoppingMapper; +import com.ruoyi.user.service.ApiShoppingService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.ruoyi.domain.common.constant.TtboxRecordSource.MALL_EXCHANGE; + +@Service +@Slf4j +public class ApiShoppingImpl implements ApiShoppingService { + + private final TtUserService userService; + private final TtBoxRecordsMapper boxRecordsMapper; + private final ApiShoppingMapper shoppingMapper; + private final ISysConfigService configService; + private final TtOrnamentMapper ornamentsMapper; + private final TtOrnamentsLevelMapper ornamentsLevelMapper; + private final RedisLock redisLock; + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private TtUserMapper userMapper; + + public ApiShoppingImpl(TtUserService userService, + TtBoxRecordsMapper boxRecordsMapper, + ApiShoppingMapper shoppingMapper, + ISysConfigService configService, + TtOrnamentMapper ornamentsMapper, + TtOrnamentsLevelMapper ornamentsLevelMapper, + RedisLock redisLock) { + this.userService = userService; + this.boxRecordsMapper = boxRecordsMapper; + this.shoppingMapper = shoppingMapper; + this.configService = configService; + this.ornamentsMapper = ornamentsMapper; + this.ornamentsLevelMapper = ornamentsLevelMapper; + this.redisLock = redisLock; + } + + @Autowired + private TtOrnamentService ttOrnamentService; + + @Override + public List list(ApiShoppingBody param, BigDecimal userCredits) { + + // 商城兑换比例 + String exchangePriceRatio = configService.selectConfigByKey("exchangePriceRatio"); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 名称 + wrapper.like(ObjectUtil.isNotEmpty(param.getName()), TtOrnament::getShortName, param.getName()); + // 类型:type="0" 视为“全部饰品”,不拼接过滤条件;仅在传入其他具体类别(1-8)时才过滤。 + wrapper.eq(ObjectUtil.isNotEmpty(param.getType()) && !"0".equals(param.getType()), + TtOrnament::getType, param.getType()); + // 外观 + wrapper.eq(ObjectUtil.isNotEmpty(param.getExterior()), TtOrnament::getExterior, param.getExterior()); + // 品质 + wrapper.eq(ObjectUtil.isNotEmpty(param.getQuality()), TtOrnament::getQuality, param.getQuality()); + // 稀有程度 + wrapper.eq(ObjectUtil.isNotEmpty(param.getRarity()), TtOrnament::getRarity, param.getRarity()); + // 是否上架 + wrapper.eq(TtOrnament::getIsPutaway, "0"); + // market_hash_name 为空的饰品无法进行第三方提货,商城列表不展示,避免用户买到无法发货的饰品 + wrapper.isNotNull(TtOrnament::getMarketHashName) + .ne(TtOrnament::getMarketHashName, ""); + // 商城价格(store_price)为0或NULL的饰品不展示 + wrapper.isNotNull(TtOrnament::getStorePrice) + .gt(TtOrnament::getStorePrice, BigDecimal.ZERO); + // 图片(image_url)为空的饰品不展示 + wrapper.isNotNull(TtOrnament::getImageUrl) + .ne(TtOrnament::getImageUrl, ""); + + // 价格区间 + if (ObjectUtil.isNotNull(param.getMaxPrice())) { + // maxPrice < 0 则清空 + if (BigDecimal.ZERO.compareTo(param.getMaxPrice()) > 0) param.setMaxPrice(null); + // minPrice > maxPrice 时自动交换,容错处理 + else if (param.getMinPrice() != null && param.getMinPrice().compareTo(param.getMaxPrice()) > 0) { + BigDecimal temp = param.getMinPrice(); + param.setMinPrice(param.getMaxPrice()); + param.setMaxPrice(temp); + } + // minPrice < 0 时修正为 0 + if (BigDecimal.ZERO.compareTo(param.getMinPrice()) > 0) param.setMinPrice(BigDecimal.ZERO); + } + if (ObjectUtil.isNotNull(param.getMaxPrice())) { + // 商城显示的价格是 store_price,价格过滤应该用 store_price 而不是 usePrice + wrapper.between(TtOrnament::getStorePrice, param.getMinPrice(), param.getMaxPrice()); + } + + // 仅显示可兑换:根据当前登录用户的积分过滤,过滤出 store_price <= accountCredits 的饰品。 + // 商城购买价格直接使用 store_price,不再经过 exchangePriceRatio 换算。 + // 未登录时 userCredits 为 null,由 controller 负责处理未登录语义。 + if (Boolean.TRUE.equals(param.getIsExchange()) && userCredits != null) { + wrapper.apply("store_price <= {0}", userCredits); + } + + // 排序 + if (ObjectUtil.isEmpty(param.getSortBy())) wrapper.orderByDesc(TtOrnament::getStorePrice); + if (ObjectUtil.isNotEmpty(param.getSortBy()) && param.getSortBy().equals(1)) + wrapper.orderByAsc(TtOrnament::getStorePrice); + if (ObjectUtil.isNotEmpty(param.getSortBy()) && param.getSortBy().equals(2)) + wrapper.orderByDesc(TtOrnament::getStorePrice); + if (ObjectUtil.isNotEmpty(param.getSortBy()) && param.getSortBy().equals(3)) + wrapper.orderByAsc(TtOrnament::getUpdateTime); + if (ObjectUtil.isNotEmpty(param.getSortBy()) && param.getSortBy().equals(4)) + wrapper.orderByDesc(TtOrnament::getUpdateTime); + + List list = ttOrnamentService.list(wrapper); + + List res = list.stream().map(ornament -> { + ApiShoppingDataVO vo = new ApiShoppingDataVO(); + BeanUtil.copyProperties(ornament, vo); + // 将数据库的 name (长名称) 映射到 VO 的 name 字段 + vo.setName(ornament.getName()); + if (ObjectUtil.isNotEmpty(vo.getUsePrice())) { + vo.setCreditsPrice(vo.getUsePrice().multiply(new BigDecimal(exchangePriceRatio))); + } + return vo; + }).collect(Collectors.toList()); + + return res; + + // return shoppingMapper.list(shoppingBody, Integer.parseInt(exchangePriceRatio)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R exchange(TtUser ttUser, List ornamentsIds) { + // 防重锁已上移至控制层(ApiShoppingController.exchange),锁包裹整个事务且解锁晚于提交,避免弱网重复提交重复扣费/发放 + + if (StringUtils.isNull(ornamentsIds) || ornamentsIds.isEmpty()) { + return R.fail("请选择要兑换的饰品"); + } + + // 批次大小限制:单次最多购买20个饰品 + if (ornamentsIds.size() > 20) { + return R.fail("单次最多购买20个饰品"); + } + + // 查询所有饰品 + List ornaments = new LambdaQueryChainWrapper<>(ornamentsMapper) + .in(TtOrnament::getId, ornamentsIds) + .eq(TtOrnament::getIsPutaway, "0") + // market_hash_name 为空无法第三方提货,禁止兑换(配合列表过滤,双重防护) + .isNotNull(TtOrnament::getMarketHashName) + .ne(TtOrnament::getMarketHashName, "") + .list(); + + if (ornaments.isEmpty()) { + return R.fail("未查询到饰品信息,请联系管理员!"); + } + + if (ornaments.size() != ornamentsIds.size()) { + return R.fail("部分饰品已下架或不存在,请刷新后重试!"); + } + + // 计算总金额(商城购买使用 store_price 作为积分价格) + BigDecimal totalExchange = BigDecimal.ZERO; + for (TtOrnament ornament : ornaments) { + BigDecimal storePrice = ornament.getStorePrice(); + if (storePrice == null || storePrice.compareTo(BigDecimal.ZERO) <= 0) { + return R.fail("商品价格异常,无法兑换"); + } + totalExchange = totalExchange.add(storePrice); + } + + if (totalExchange.compareTo(ttUser.getAccountCredits()) > 0) { + return R.fail("您的积分不足!"); + } + if (totalExchange.compareTo(ttUser.getAccountAmount()) > 0) { + return R.fail("您的金币不足!"); + } + + // 同步扣减积分与金币(价格均为 store_price) + // 积分扣减:仅扣DB积分字段,不单独写流水记录(金币流水由下面统一记录) + int creditsCount = userMapper.subtractOnlyAccountCredits(Long.valueOf(ttUser.getUserId()), totalExchange); + if (creditsCount <= 0) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return R.fail("账户积分不足"); + } + // 金币扣减:扣DB金币字段 + 写一条金币流水记录 + R r2 = userService.updateUserAccountKeepCredits(ttUser.getUserId(), totalExchange.negate(), TtAccountRecordSource.EXCHANGE); + if (R.isError(r2)) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return R.fail(r2.getMsg()); + } + + // 批量插入兑换记录 + for (TtOrnament ttOrnament : ornaments) { + TtBoxRecords boxRecords = TtBoxRecords.builder().build(); + boxRecords.setUserId(ttUser.getUserId()); + boxRecords.setOrnamentId(ttOrnament.getId()); + boxRecords.setOrnamentName(ttOrnament.getShortName());//饰品名称 + boxRecords.setImageUrl(ttOrnament.getImageUrl());//饰品图片 + boxRecords.setMarketHashName(ttOrnament.getMarketHashName()); + boxRecords.setOrnamentsPrice(ttOrnament.getStorePrice()); + + // exterior 字段对应 tt_ornaments_level.id,补全等级图片信息 + if (StringUtils.isNotEmpty(ttOrnament.getExterior())) { + Integer levelId = Integer.valueOf(ttOrnament.getExterior()); + TtOrnamentsLevel level = new LambdaQueryChainWrapper<>(ornamentsLevelMapper) + .eq(TtOrnamentsLevel::getId, levelId) + .one(); + if (level != null) { + boxRecords.setOrnamentsLevelId(levelId);//饰品等级 ID + boxRecords.setOrnamentLevelImg(level.getLevelImg());//饰品等级图片 + } + } + + boxRecords.setStatus(TtboxRecordStatus.APPLY_DELIVERY.getCode()); // 商城购买直接进提货背包,用户手动点提货再发起发货流程 + boxRecords.setCreateTime(new Date()); + boxRecords.setSource(MALL_EXCHANGE.getCode()); + boxRecords.setHolderUserId(ttUser.getUserId()); + boxRecordsMapper.insert(boxRecords); + } + + return R.ok(); + } + + // 废弃方法 + public void parentAward(TtUser ttUser, BigDecimal exchange) { + BigDecimal t = exchange.multiply(new BigDecimal("0.03")); + TtUser parent = userService.getById(ttUser.getParentId()); + parent.setAccountCredits(parent.getAccountCredits().add(t)); + userService.updateUserById(parent); + userService.insertUserCreditsRecords(parent.getUserId(), TtAccountRecordType.INPUT, TtAccountRecordSource.P_WELFARE, t, parent.getAccountCredits()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String integratingConversion(TtUser ttUser, BigDecimal credits) { + if (credits == null || credits.compareTo(BigDecimal.ZERO) <= 0) return "转换积分必须大于零"; + if (credits.scale() > 2) return "积分小数点不能超过两位"; + if (credits.compareTo(ttUser.getAccountCredits()) > 0) return "您的积分不足!"; + // 原子 SQL:扣积分+加金币,自带 account_credits >= #{credits} 守卫, + // 避免旧写法 updateById 全字段覆盖在并发下丢失其他接口的余额变动 + int count = userMapper.creditsToAmount(Long.valueOf(ttUser.getUserId()), credits); + if (count <= 0) return "您的积分不足!"; + // 重新查询扣减后的真实余额写流水,避免快照计算与 DB 不一致 + TtUser after = userService.getById(ttUser.getUserId()); + userService.insertUserAmountRecords(after.getUserId(), TtAccountRecordType.INPUT, TtAccountRecordSource.INTEGRATING_CONVERSION, credits, after.getAccountAmount()); + userService.insertUserCreditsRecords(after.getUserId(), TtAccountRecordType.OUTPUT, TtAccountRecordSource.INTEGRATING_CONVERSION, credits.negate(), after.getAccountCredits()); + return ""; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiTaskCenterServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiTaskCenterServiceImpl.java new file mode 100644 index 0000000..5a1ef9f --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiTaskCenterServiceImpl.java @@ -0,0 +1,120 @@ +package com.ruoyi.user.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.mapper.*; +import com.ruoyi.admin.service.TtUserCreditsRecordsService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtTaskCenter; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.user.mapper.ApiTaskCenterMapper; +import com.ruoyi.user.model.TtTaskCenterUser; +import com.ruoyi.user.model.dto.YesterdayExpenditureDTO; +import com.ruoyi.user.model.vo.ApiTaskCenterVO; +import com.ruoyi.user.service.ApiTaskCenterService; +import io.swagger.models.auth.In; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +@Service +public class ApiTaskCenterServiceImpl implements ApiTaskCenterService { + + @Autowired + private ApiTaskCenterMapper apiTaskCenterMapper; + + @Autowired + private TtTaskCenterMapper ttTaskCenterMapper; + + @Autowired + private TtUserMapper ttUserMapper; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private ISysConfigService configService; + + @Override + public List selectApiTaskCenterVOList(Integer userId) { + return apiTaskCenterMapper.selectApiTaskCenterVOList(userId); + } + + @Override + public TtTaskCenterUser selectTtTaskCenterUserByTaskIdAndUserId(Integer taskId, Integer userId) { + return apiTaskCenterMapper.selectTtTaskCenterUserByTaskIdAndUserId(taskId, userId); + } + + @Transactional + @Override + public void updateYesterdayBonusPoints() { + // 根据任务标识获取任务详情 + TtTaskCenter ttTaskCenter = ttTaskCenterMapper.selectTtTaskCenterByIdentify("yesterday.expenditure.task"); + // 清空tt_task_center_user表中活动类型为昨日消费奖励的数据 + apiTaskCenterMapper.deleteYesterdayExpenditureBonusPoints(ttTaskCenter.getTaskId()); + + List yesterdayExpenditureDTOList = apiTaskCenterMapper.getYesterdayExpenditure(); + if (yesterdayExpenditureDTOList.size() == 0) { + return; + } + // 获取参数设置中消费返积分比例 + String rechargePointsRebateRatio = configService.selectConfigByKey("expenditure.points.rebate.ratio"); + BigDecimal ratio = new BigDecimal(rechargePointsRebateRatio); + List ttTaskCenterUserList = new ArrayList<>(); + + for (YesterdayExpenditureDTO yesterdayRechargeDTO : yesterdayExpenditureDTOList) { + TtTaskCenterUser ttTaskCenterUser = new TtTaskCenterUser(); + ttTaskCenterUser.setTaskId(ttTaskCenter.getTaskId()); + ttTaskCenterUser.setUserId(yesterdayRechargeDTO.getUserId()); + ttTaskCenterUser.setCredit(yesterdayRechargeDTO.getTotalRecharge().multiply(ratio)); + ttTaskCenterUserList.add(ttTaskCenterUser); + } + // 批量增加昨日的积分奖励数据 + apiTaskCenterMapper.insertYesterdayExpenditureBonusPoints(ttTaskCenterUserList); + } + + @Transactional + @Override + public AjaxResult getReward(Integer taskId, Integer userId) { + // 根据ID查询奖励积分 + BigDecimal credit = apiTaskCenterMapper.selectCreditByTaskIdAndUserId(taskId, userId); + // 将奖励积分写入到tt_user表 + TtUser ttUser = ttUserMapper.selectById(userId); + ttUser.setAccountCredits(ttUser.getAccountCredits().add(credit)); + int row = ttUserMapper.updateById(ttUser); + // 将奖励积分写入tt_user_blend_ercash表 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TtUserBlendErcash::getUserId, userId) + .orderByDesc(TtUserBlendErcash::getCreateTime) + .last("LIMIT 1"); + TtUserBlendErcash lastTtUserBlendErcash = ttUserBlendErcashMapper.selectOne(queryWrapper); + TtUserBlendErcash ttUserBlendErcash = new TtUserBlendErcash(); + ttUserBlendErcash.setUserId(userId); + ttUserBlendErcash.setCredits(credit); + ttUserBlendErcash.setCreateTime(new Timestamp(new Date().getTime())); + if (!Objects.isNull(lastTtUserBlendErcash) && !Objects.isNull(lastTtUserBlendErcash.getFinalCredits())) { + ttUserBlendErcash.setFinalCredits(lastTtUserBlendErcash.getFinalCredits().add(credit)); + } else { + ttUserBlendErcash.setFinalCredits(credit); + } + ttUserBlendErcash.setTotal(credit); + ttUserBlendErcash.setSource(6); + ttUserBlendErcash.setType(1); + ttUserBlendErcash.setRemark("昨日消费返积分"); + ttUserBlendErcashMapper.insert(ttUserBlendErcash); + // 标记为已领取 + apiTaskCenterMapper.markAsClaimedByUserIdAndType(taskId, userId); + return row > 0 ? AjaxResult.success("成功领取" + credit + "积分") : AjaxResult.error("领取失败"); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiUserPackSackServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiUserPackSackServiceImpl.java new file mode 100644 index 0000000..8a84a40 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiUserPackSackServiceImpl.java @@ -0,0 +1,916 @@ +package com.ruoyi.user.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.admin.mapper.TtBoxRecordsMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtDeliveryRecordService; +import com.ruoyi.admin.service.TtTransferApplyService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.admin.service.TtUserTransferDailyService; +import com.ruoyi.admin.service.TtVipLevelService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.redis.config.RedisLock; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.domain.common.constant.*; +import com.ruoyi.domain.dto.packSack.DecomposeLogCondition; +import com.ruoyi.domain.dto.packSack.DecomposeParam; +import com.ruoyi.domain.dto.packSack.DeliveryParam; +import com.ruoyi.domain.dto.packSack.PackSackCondition; +import com.ruoyi.domain.entity.BoxTransferRecord; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.delivery.TtDeliveryRecord; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.entity.transfer.TtTransferApply; +import com.ruoyi.domain.other.TtVipLevel; +import com.ruoyi.domain.vo.TtBoxRecordsDataVO; +import com.ruoyi.domain.vo.UserPackSackDataVO; +import com.ruoyi.domain.vo.client.PackSackGlobalData; +import com.ruoyi.domain.vo.client.WelfareBoxVO; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.common.service.DeliverGoodsService; +import com.ruoyi.user.mapper.ApiBoxTransferRecordMapper; +import com.ruoyi.user.mapper.ApiUserPackSackMapper; +import com.ruoyi.user.model.TransferParam; +import com.ruoyi.message.service.MessageService; +import com.ruoyi.user.service.ApiUserPackSackService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ObjectUtils; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.*; +import java.util.Date; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class ApiUserPackSackServiceImpl implements ApiUserPackSackService { + + private final ISysConfigService configService; + private final TtUserService userService; + private final ApiUserPackSackMapper userPackSackMapper; + private final TtBoxRecordsService boxRecordsService; + + + private final TtBoxRecordsMapper ttBoxRecordsMapper; + private final TtDeliveryRecordService deliveryRecordService; + private final RabbitTemplate rabbitTemplate; + private final RedisLock redisLock; + private final TtVipLevelService vipLevelService; + private final TtUserTransferDailyService userTransferDailyService; + + @Autowired + private DeliverGoodsService deliverGoodsService; + + @Autowired + private Executor customThreadPoolExecutor; + + @Autowired + private TtUserBlendErcashMapper TtUserBlendErcashMapper; + + @Autowired + private ApiBoxTransferRecordMapper boxTransferRecordMapper; + + @Autowired + private TtTransferApplyService transferApplyService; + + private final MessageService messageService; + + public ApiUserPackSackServiceImpl(ISysConfigService configService, TtUserService userService, + ApiUserPackSackMapper userPackSackMapper, + TtBoxRecordsService boxRecordsService, + TtBoxRecordsMapper ttBoxRecordsMapper, + TtDeliveryRecordService deliveryRecordService, + RabbitTemplate rabbitTemplate, + RedisLock redisLock, + TtVipLevelService vipLevelService, + TtUserTransferDailyService userTransferDailyService, + MessageService messageService) { + this.configService = configService; + this.userService = userService; + this.userPackSackMapper = userPackSackMapper; + this.boxRecordsService = boxRecordsService; + this.ttBoxRecordsMapper = ttBoxRecordsMapper; + this.deliveryRecordService = deliveryRecordService; + this.rabbitTemplate = rabbitTemplate; + this.redisLock = redisLock; + this.vipLevelService = vipLevelService; + this.userTransferDailyService = userTransferDailyService; + this.messageService = messageService; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R delivery(DeliveryParam param, TtUser ttUser) { + // Redis分布式锁:防止同一用户并发提货导致重复记录 + String lockKey = "user:delivery:lock:" + ttUser.getUserId(); + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + throw new ServiceException("正在提货中,请勿重复操作"); + } + try { + + // ==================== 【新增】提货次数预扣逻辑 ==================== + Integer applyVipLevelId = ttUser.getVipLevel() != null ? ttUser.getVipLevel() : 0; + int deliveryDailyLimit = 0; + if (applyVipLevelId > 0) { + TtVipLevel vipLevel = vipLevelService.getById(applyVipLevelId); + if (vipLevel != null && vipLevel.getDeliveryCount() != null) { + deliveryDailyLimit = vipLevel.getDeliveryCount(); + } + } + + // 获取或创建今日提货次数记录(按申请时的 VIP 等级) + userTransferDailyService.getOrCreateTodayWithDelivery(ttUser.getUserId(), applyVipLevelId, deliveryDailyLimit); + + // 预扣 1 次提货次数(冻结) + boolean deducted = userTransferDailyService.preDeductDelivery(ttUser.getUserId(), applyVipLevelId, 1); + if (!deducted) { + return R.fail(false, "今日提货次数已用完,请明日再试"); + } + // =========================================================== + + if (!param.getIsAll()) { + // 批次大小限制:单次最多提货5个饰品(与转赠一致) + if (param.getPackSackIds() != null && param.getPackSackIds().size() > 5) { + throw new ServiceException("单次最多提货5个饰品"); + } + + String transactionLink = ttUser.getTransactionLink(); + Long steamId = ttUser.getSteamId(); + + if (ObjectUtils.isEmpty(param.getPackSackIds())) { + return R.fail(false, "请选择需要提取的饰品!"); + } + + // 查询用户背包记录(status=0 或 status=2,后者用于商城购买直接进提货背包的饰品) + List boxRecordsList = new LambdaQueryChainWrapper<>(boxRecordsService.getBaseMapper()) + .eq(TtBoxRecords::getHolderUserId, ttUser.getUserId()) + .in(TtBoxRecords::getStatus, TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode()) + .in(TtBoxRecords::getId, param.getPackSackIds()) + .list(); + if (!boxRecordsList.isEmpty()) { + boxRecordsList = boxRecordsList.stream().peek(ttBoxRecords -> { + ttBoxRecords.setStatus(TtboxRecordStatus.APPLY_DELIVERY.getCode()); + ttBoxRecords.setUpdateTime(DateUtils.getNowDate()); + }).collect(Collectors.toList()); + } + + if (boxRecordsList.isEmpty()) { + return R.fail(false, "请选择需要提取的饰品!"); + } else { + // 检查是否包含道具 + Integer deliveryAble = ttBoxRecordsMapper.checkDeliveryAble(param.getPackSackIds()); + if (deliveryAble > 0) { + return R.fail("道具不可用于提货"); + } + } + + if (StringUtils.isEmpty(transactionLink)) { + return R.fail(false, "您未绑定交易链接,请绑定Steam交易链接后重试!"); + } + + // 更新开箱记录状态 + if (!boxRecordsService.updateBatchById(boxRecordsList, 1)) { + R.fail("饰品提取异常,请联系管理员!"); + } + + // 构建提货记录数据 + String batchId = "BATCH_" + IdUtils.fastSimpleUUID().substring(0, 8).toUpperCase(); + List deliveryRecordList = new ArrayList<>(); + for (TtBoxRecords ttBoxRecords : boxRecordsList) { + TtDeliveryRecord ttDeliveryRecord = TtDeliveryRecord.builder() + .userId(ttUser.getUserId()) + .boxRecordsId(ttBoxRecords.getId()) + .ornamentId(ttBoxRecords.getOrnamentId()) + .marketHashName(ttBoxRecords.getMarketHashName()) + .ornamentsPrice(ttBoxRecords.getOrnamentsPrice()) + .outTradeNo(IdUtils.fastSimpleUUID().toUpperCase()) // 独立订单号 + .batchId(batchId) // 批次号(共用) + .applyVipLevelId(applyVipLevelId) // 申请时的 VIP 等级 + .build(); + // 自动发货最小价格 + String autoDeliveryMinPriceStr = configService.selectConfigByKey("autoDeliveryMinPrice"); + if (new BigDecimal(autoDeliveryMinPriceStr).compareTo(ttBoxRecords.getOrnamentsPrice()) > 0) { + ttDeliveryRecord.setDelivery(DeliveryPattern.AUTO.getCode()); + } else { + ttDeliveryRecord.setDelivery(DeliveryPattern.MANUAL.getCode()); + } + ttDeliveryRecord.setStatus(DeliveryOrderStatus.DELIVERY_BEFORE.getCode()); + // TODO 如果移除一键同步,需要解除注释,并且修改后台查询SQL + // ttDeliveryRecord.setMessage(DeliveryOrderStatus.DELIVERY_BEFORE.getMsg()); + ttDeliveryRecord.setCreateBy(ttUser.getNickName()); + ttDeliveryRecord.setCreateTime(DateUtils.getNowDate()); + deliveryRecordList.add(ttDeliveryRecord); + } + + if (!deliveryRecordService.saveBatch(deliveryRecordList, 1)) { + R.fail("提货记录更新失败。"); + } + + // 发送提货邮件通知 + for (TtBoxRecords boxRecord : boxRecordsList) { + sendDeliveryNotice(ttUser.getUserId(), boxRecord); + } + + // 异步处理自动发货 + // CompletableFuture.runAsync(() -> { + // deliverGoodsService.autoDelivery(ttUser.getUserId()); + // }, customThreadPoolExecutor); + + // String message = String.valueOf(ttUser.getUserId()); + // rabbitTemplate.convertAndSend(DelayedQueueConfig.DELIVERY_QUEUE, message); + return R.ok(true); + } else { + + String transactionLink = ttUser.getTransactionLink(); + Long steamId = ttUser.getSteamId(); + + List boxRecordsList = new LambdaQueryChainWrapper<>(boxRecordsService.getBaseMapper()) + .eq(TtBoxRecords::getHolderUserId, ttUser.getUserId()) + .in(TtBoxRecords::getStatus, TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode()) + .list(); + + if (boxRecordsList.isEmpty()) { + return R.ok("操作完成,背包没有物品。"); + } + + // 全部提货:超过5个只取前5个,与转赠单次上限保持一致 + if (boxRecordsList.size() > 5) { + boxRecordsList = boxRecordsList.subList(0, 5); + } + + // 检查是否包含道具 + //Integer deliveryAble = ttBoxRecordsMapper.checkDeliveryAble(param.getPackSackIds()); + Integer deliveryAble = ttBoxRecordsMapper.checkAllDeliveryAble(ttUser.getUserId()); + if (deliveryAble > 0) { + return R.fail("道具不可用于提货"); + } + + boxRecordsList = boxRecordsList.stream().peek(ttBoxRecords -> { + ttBoxRecords.setStatus(TtboxRecordStatus.APPLY_DELIVERY.getCode()); + ttBoxRecords.setUpdateTime(DateUtils.getNowDate()); + }).collect(Collectors.toList()); + + if (boxRecordsList.isEmpty()) return R.fail(false, "请选择需要提取的饰品!"); + if (StringUtils.isEmpty(transactionLink)) + return R.fail(false, "您未绑定交易链接,请绑定 Steam 交易链接后重试!"); + + // 更新开箱记录状态 + if (!boxRecordsService.updateBatchById(boxRecordsList, 1)) { + R.fail("饰品提取异常,请联系管理员!"); + } + + // 构建提货记录数据 + String batchId = "BATCH_" + IdUtils.fastSimpleUUID().substring(0, 8).toUpperCase(); + List deliveryRecordList = new ArrayList<>(); + for (TtBoxRecords ttBoxRecords : boxRecordsList) { + TtDeliveryRecord ttDeliveryRecord = TtDeliveryRecord.builder() + .userId(ttUser.getUserId()) + .boxRecordsId(ttBoxRecords.getId()) + .ornamentId(ttBoxRecords.getOrnamentId()) + .marketHashName(ttBoxRecords.getMarketHashName()) + .ornamentsPrice(ttBoxRecords.getOrnamentsPrice()) + .outTradeNo(IdUtils.fastSimpleUUID().toUpperCase()) // 独立订单号 + .batchId(batchId) // 批次号(共用) + .applyVipLevelId(applyVipLevelId) // 申请时的 VIP 等级 + .build(); + if (ttUser.getUserType().equals(UserType.ANCHOR.getCode())) { + // 主播直接发货完成 + ttDeliveryRecord.setDelivery(DeliveryPattern.ANCHOR.getCode()); + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_COMPLETE.getCode()); + ttDeliveryRecord.setMessage("订单完成"); + ttDeliveryRecord.setCreateBy(ttUser.getNickName()); + ttDeliveryRecord.setCreateTime(DateUtils.getNowDate()); + } else if (ttUser.getUserType().equals(UserType.COMMON_USER.getCode())) { + // 自动发货最小价格 + String autoDeliveryMinPriceStr = configService.selectConfigByKey("autoDeliveryMinPrice"); + if (new BigDecimal(autoDeliveryMinPriceStr).compareTo(ttBoxRecords.getOrnamentsPrice()) > 0) { + ttDeliveryRecord.setDelivery(DeliveryPattern.AUTO.getCode()); + } + ttDeliveryRecord.setDelivery(DeliveryPattern.MANUAL.getCode()); + ttDeliveryRecord.setCreateBy(ttUser.getNickName()); + ttDeliveryRecord.setCreateTime(DateUtils.getNowDate()); + } + + // 自动发货最小价格 + String autoDeliveryMinPriceStr = configService.selectConfigByKey("autoDeliveryMinPrice"); + if (new BigDecimal(autoDeliveryMinPriceStr).compareTo(ttBoxRecords.getOrnamentsPrice()) > 0) { + ttDeliveryRecord.setDelivery(DeliveryPattern.AUTO.getCode()); + } + ttDeliveryRecord.setDelivery(DeliveryPattern.MANUAL.getCode()); + ttDeliveryRecord.setCreateBy(ttUser.getNickName()); + ttDeliveryRecord.setCreateTime(DateUtils.getNowDate()); + + deliveryRecordList.add(ttDeliveryRecord); + } + + if (!deliveryRecordService.saveBatch(deliveryRecordList, 1)) { + R.fail("提货记录更新失败。"); + } + + // 发送提货邮件通知 + for (TtBoxRecords boxRecord : boxRecordsList) { + sendDeliveryNotice(ttUser.getUserId(), boxRecord); + } + + // todo 异步处理自动发货(整个方法可以优化) +// CompletableFuture.runAsync(() -> { +// deliverGoodsService.autoDelivery(ttUser.getUserId()); +// }, customThreadPoolExecutor); + + // String message = String.valueOf(ttUser.getUserId()); + // rabbitTemplate.convertAndSend(DelayedQueueConfig.DELIVERY_QUEUE, message); + return R.ok(true); + } + } finally { + redisLock.unlock(lockKey); + } + } + + /** + * 发送提货邮件通知 + */ + private void sendDeliveryNotice(Integer userId, TtBoxRecords boxRecord) { + if (userId == null || boxRecord == null) { + return; + } + String ornamentName = boxRecord.getOrnamentName() != null ? boxRecord.getOrnamentName() : ""; + String content = "您的【" + ornamentName + "】饰品提货已申请,请耐心等待。"; + messageService.sendNotice(Long.valueOf(userId), + MessageCategoryEnum.SYS_PICKUP.getTitle(), + content, + MessageCategoryEnum.SYS_PICKUP.getCode()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R blankDelivery(DeliveryParam param, TtUser ttUser) { + // Redis分布式锁:与delivery共用同一锁key,防止普通提货和白号提货并发冲突 + String lockKey = "user:delivery:lock:" + ttUser.getUserId(); + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + throw new ServiceException("正在提货中,请勿重复操作"); + } + try { + // 白号假提货:生成已完成提货记录供 cocos 展示,将饰品移出背包,不真正发货。 + // 后台发货管理按 is_blank_account 过滤,不展示白号提货记录。 + LambdaQueryChainWrapper query = new LambdaQueryChainWrapper<>(boxRecordsService.getBaseMapper()) + .eq(TtBoxRecords::getHolderUserId, ttUser.getUserId()) + .in(TtBoxRecords::getStatus, TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode()); + if (param.getIsAll() == null || !param.getIsAll()) { + if (ObjectUtils.isEmpty(param.getPackSackIds())) { + return R.fail(false, "请选择需要提取的饰品!"); + } + query.in(TtBoxRecords::getId, param.getPackSackIds()); + } + List boxRecordsList = query.list(); + if (boxRecordsList.isEmpty()) { + return R.fail(false, "请选择需要提取的饰品!"); + } + + String autoDeliveryMinPriceStr = configService.selectConfigByKey("autoDeliveryMinPrice"); + BigDecimal autoDeliveryMinPrice = StringUtils.isEmpty(autoDeliveryMinPriceStr) + ? BigDecimal.ZERO : new BigDecimal(autoDeliveryMinPriceStr); + + List deliveryRecordList = new ArrayList<>(); + for (TtBoxRecords ttBoxRecords : boxRecordsList) { + TtDeliveryRecord ttDeliveryRecord = TtDeliveryRecord.builder() + .userId(ttUser.getUserId()) + .boxRecordsId(ttBoxRecords.getId()) + .ornamentId(ttBoxRecords.getOrnamentId()) + .marketHashName(ttBoxRecords.getMarketHashName()) + .ornamentsPrice(ttBoxRecords.getOrnamentsPrice()) + .outTradeNo(IdUtils.fastSimpleUUID().toUpperCase()) + .build(); + if (autoDeliveryMinPrice.compareTo(ttBoxRecords.getOrnamentsPrice()) > 0) { + ttDeliveryRecord.setDelivery(DeliveryPattern.AUTO.getCode()); + } else { + ttDeliveryRecord.setDelivery(DeliveryPattern.MANUAL.getCode()); + } + // 白号假提货:直接标记订单完成 + ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_COMPLETE.getCode()); + ttDeliveryRecord.setMessage("订单完成"); + ttDeliveryRecord.setCreateBy(ttUser.getNickName()); + ttDeliveryRecord.setCreateTime(DateUtils.getNowDate()); + deliveryRecordList.add(ttDeliveryRecord); + + // 从背包移除:置为已提货(3),使「我的库存」(0,2) 不再展示,避免重复提货 + ttBoxRecords.setStatus(TtboxRecordStatus.DELIVERY_YET.getCode()); + ttBoxRecords.setUpdateTime(DateUtils.getNowDate()); + } + boxRecordsService.updateBatchById(boxRecordsList, 1); + deliveryRecordService.saveBatch(deliveryRecordList, 1); + return R.ok(true, "提货申请已提交,预计1-3个工作日内到账"); + } finally { + redisLock.unlock(lockKey); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int decompose(DecomposeParam param, TtUser ttUser) { + // Redis分布式锁:防止同一用户并发分解导致重复加余额 + String lockKey = "user:decompose:lock:" + ttUser.getUserId(); + boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS); + if (!locked) { + throw new ServiceException("正在分解中,请勿重复操作"); + } + try { + + // 批次大小限制:单次最多分解50个饰品 + // 注:对战最多15箱、三人对战获胜方最多可获得45件饰品并支持“立即分解”,故上限需覆盖45并留余量 + final int DECOMPOSE_BATCH_LIMIT = 50; + + if (!param.getIsAll()) { + // 指定ID分解:超过上限直接拒绝 + if (param.getPackSackIds() != null && param.getPackSackIds().size() > DECOMPOSE_BATCH_LIMIT) { + throw new ServiceException("单次最多分解" + DECOMPOSE_BATCH_LIMIT + "个饰品"); + } + // 兼容「我的库存」(status=0) 与「提货背包」(status=2) 两类饰品都允许分解,与 getPackSack 展示口径保持一致 + List boxRecordsList = new LambdaQueryChainWrapper<>(boxRecordsService.getBaseMapper()) + .eq(TtBoxRecords::getHolderUserId, ttUser.getUserId()) + .in(TtBoxRecords::getStatus, TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode()) + .in(TtBoxRecords::getId, param.getPackSackIds()) + .list(); + if (boxRecordsList.isEmpty()) { + return 0; + } + + // 校验:是否有「正在提货中」的饰品(tt_delivery_record 存在未取消的记录) + // 商城购买未提货 / 已被后台退回申请 的饰品不会命中(前者无记录、后者 status=11) + List selectedBoxIds = boxRecordsList.stream() + .map(TtBoxRecords::getId).collect(Collectors.toList()); + List deliveringRecords = new LambdaQueryChainWrapper<>(deliveryRecordService.getBaseMapper()) + .in(TtDeliveryRecord::getBoxRecordsId, selectedBoxIds) + .ne(TtDeliveryRecord::getStatus, DeliveryOrderStatus.ORDER_CANCEL.getCode()) + .list(); + if (!deliveringRecords.isEmpty()) { + java.util.Set deliveringBoxIds = deliveringRecords.stream() + .map(TtDeliveryRecord::getBoxRecordsId).collect(Collectors.toSet()); + String names = boxRecordsList.stream() + .filter(b -> deliveringBoxIds.contains(b.getId())) + .map(b -> StringUtils.isNotBlank(b.getOrnamentName()) ? b.getOrnamentName() : "未命名饰品") + .distinct() + .collect(Collectors.joining("、")); + throw new ServiceException("【" + names + "】正在提货中,无法分解"); + } + boxRecordsList = boxRecordsList.stream().peek(ttBoxRecords -> { + ttBoxRecords.setStatus(TtboxRecordStatus.RESOLVE.getCode()); + ttBoxRecords.setUpdateTime(DateUtils.getNowDate()); + }).collect(Collectors.toList()); + + //List boxRecordsList = packSackHandle(param.getPackSackIds(), ttUser, TtboxRecordStatus.RESOLVE.getCode()); + + boolean batchUpdated = boxRecordsService.updateBatchById(boxRecordsList, 1); + if (!batchUpdated) { + throw new ServiceException("饰品状态更新失败,请重试"); + } + + // 分解结算:金币与积分同步增加,按饰品来源分组分别记流水(与「全部分解」口径保持一致) + settleDecomposeBySource(ttUser, boxRecordsList); + + return boxRecordsList.size(); + } else { + + // 全部分解:兼容 status IN (0,2,20),与「我的库存」展示口径保持一致 + LambdaQueryWrapper wrapper1 = new LambdaQueryWrapper<>(); + wrapper1 + .eq(TtBoxRecords::getHolderUserId, ttUser.getUserId()) + .in(TtBoxRecords::getStatus, TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode(), TtboxRecordStatus.TRANSFER_PENDING.getCode()); + List list = boxRecordsService.list(wrapper1); + + // 过滤掉「正在提货中」的饰品(有未取消的提货记录):全部分解时静默跳过,不阻断其余分解 + if (!list.isEmpty()) { + List allBoxIds = list.stream().map(TtBoxRecords::getId).collect(Collectors.toList()); + List deliveringRecords = new LambdaQueryChainWrapper<>(deliveryRecordService.getBaseMapper()) + .in(TtDeliveryRecord::getBoxRecordsId, allBoxIds) + .ne(TtDeliveryRecord::getStatus, DeliveryOrderStatus.ORDER_CANCEL.getCode()) + .list(); + if (!deliveringRecords.isEmpty()) { + java.util.Set deliveringBoxIds = deliveringRecords.stream() + .map(TtDeliveryRecord::getBoxRecordsId).collect(Collectors.toSet()); + list = list.stream() + .filter(b -> !deliveringBoxIds.contains(b.getId())) + .collect(Collectors.toList()); + } + } + if (list.isEmpty()) { + return 0; + } + + // 全部分解:超过上限只取前50个,让用户分批操作 + if (list.size() > DECOMPOSE_BATCH_LIMIT) { + log.warn("[decompose] 全部分解截断: userId={}, total={}, processing={}", + ttUser.getUserId(), list.size(), DECOMPOSE_BATCH_LIMIT); + list = list.subList(0, DECOMPOSE_BATCH_LIMIT); + } + + // 对转赠审核中的饰品,先驳回相关待审核转赠申请(饰品即将被分解,无法完成转赠) + List transferPendingRecords = list.stream() + .filter(b -> TtboxRecordStatus.TRANSFER_PENDING.getCode().equals(b.getStatus())) + .collect(Collectors.toList()); + rejectPendingTransferApply(transferPendingRecords); + + // 更新物品状态 + Timestamp now = new Timestamp(System.currentTimeMillis()); + List collect = list.stream().peek(item -> { + item.setStatus(TtboxRecordStatus.RESOLVE.getCode()); + item.setUpdateTime(now); + }).collect(Collectors.toList()); + boxRecordsService.updateBatchById(collect, 1); + + // 分解结算:金币与积分同步增加,按饰品来源分组分别记流水 + settleDecomposeBySource(ttUser, list); + + return list.size(); + } + } finally { + redisLock.unlock(lockKey); + } + + } + + /** + * 分解转赠审核中(status=20)的饰品前,自动驳回相关待审核转赠申请及转赠记录。 + * 避免饰品被分解后,转赠申请单仍停留在"审核中"导致数据不一致。 + */ + private void rejectPendingTransferApply(List transferPendingRecords) { + if (transferPendingRecords == null || transferPendingRecords.isEmpty()) { + return; + } + List boxRecordIds = transferPendingRecords.stream() + .map(TtBoxRecords::getId) + .collect(Collectors.toList()); + Date now = DateUtils.getNowDate(); + + // 驳回转赠申请单 + List applies = new LambdaQueryChainWrapper<>(transferApplyService.getBaseMapper()) + .in(TtTransferApply::getBoxRecordId, boxRecordIds) + .eq(TtTransferApply::getStatus, 0) + .list(); + if (!applies.isEmpty()) { + for (TtTransferApply apply : applies) { + apply.setStatus(2); + apply.setRejectReason("饰品已被分解"); + apply.setUpdateTime(now); + } + transferApplyService.updateBatchById(applies); + } + + // 同步更新用户端赠送记录 + List transferRecords = new LambdaQueryChainWrapper<>(boxTransferRecordMapper) + .in(BoxTransferRecord::getBoxRecordId, boxRecordIds) + .eq(BoxTransferRecord::getStatus, 0) + .list(); + if (!transferRecords.isEmpty()) { + for (BoxTransferRecord record : transferRecords) { + record.setStatus(2); + record.setUpdateTime(LocalDateTime.now()); + boxTransferRecordMapper.updateById(record); + } + } + } + + /** + * 分解结算:按饰品来源(source字段)分组,每个来源单独生成一条余额流水。 + * 完全基于后端数据判断,不依赖前端传参,避免前端参数不可信问题。 + * 例如混选经典开箱与对战的饰品分解,将分别产生「经典开箱游戏奖励」「对战游戏奖励」两条流水。 + */ + private void settleDecomposeBySource(TtUser ttUser, List boxRecordsList) { + if (boxRecordsList == null || boxRecordsList.isEmpty()) { + return; + } + // 按映射后的流水类型分组求和(未知来源统一兜底为背包分解,可能与其他未知来源合并为一条) + Map totalBySource = boxRecordsList.stream() + .collect(Collectors.groupingBy( + record -> mapDecomposeSource(record.getSource()), + LinkedHashMap::new, + Collectors.mapping(TtBoxRecords::getOrnamentsPrice, + Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)))); + totalBySource.forEach((accountSource, total) -> + userService.updateUserAccount(ttUser.getUserId(), total, accountSource)); + } + + /** + * 单个饰品来源到余额流水类型的映射;未知来源统一按背包分解处理。 + */ + private TtAccountRecordSource mapDecomposeSource(Integer source) { + if (source == null) { + return TtAccountRecordSource.DECOMPOSE_ORNAMENT; + } + if (source.equals(TtboxRecordSource.BLIND_BOX.getCode())) { + return TtAccountRecordSource.GAME_TYPE_01_AWARD; + } + if (source.equals(TtboxRecordSource.FIGHT.getCode())) { + return TtAccountRecordSource.GAME_TYPE_02_AWARD; + } + if (source.equals(TtboxRecordSource.UPGRADE.getCode())) { + return TtAccountRecordSource.GOLD_DEVICE_REWARD; + } + if (source.equals(TtboxRecordSource.WHEEL_BOX.getCode())) { + return TtAccountRecordSource.LUCKLY_WHEEL_REWARD; + } + if (source.equals(TtboxRecordSource.BULLET_BOX.getCode())) { + return TtAccountRecordSource.BULLET_REWARD; + } + if (source.equals(TtboxRecordSource.OLYMPUS_BOX.getCode())) { + return TtAccountRecordSource.OLYMPUS_REWARD; + } + if (source.equals(TtboxRecordSource.ROLL.getCode())) { + return TtAccountRecordSource.GAME_TYPE_03_AWARD; + } + if (source.equals(TtboxRecordSource.WELFARE_BOX.getCode())) { + return TtAccountRecordSource.WELFARE_BOX_REWARD; + } + if (source.equals(TtboxRecordSource.TURNOVER_LOTTERY.getCode())) { + return TtAccountRecordSource.TURNOVER_LOTTERY_REWARD; + } + // 未知来源统一按背包分解处理 + return TtAccountRecordSource.DECOMPOSE_ORNAMENT; + } + + @Override + public List getPackSack(Integer userId) { + return userPackSackMapper.getPackSack(userId); + } + + // 查询并修改boxRecord的状态(不改数据库) + @Override + public List packSackHandle(List packSackIds, TtUser ttUser, Integer status) { + + List boxRecordsList = new LambdaQueryChainWrapper<>(boxRecordsService.getBaseMapper()) + .eq(TtBoxRecords::getHolderUserId, ttUser.getUserId()) + .eq(TtBoxRecords::getStatus, TtboxRecordStatus.IN_PACKSACK_ON.getCode()) + .in(TtBoxRecords::getId, packSackIds) + .list(); + if (!boxRecordsList.isEmpty()) { + boxRecordsList = boxRecordsList.stream().peek(ttBoxRecords -> { + ttBoxRecords.setStatus(status); + ttBoxRecords.setUpdateTime(DateUtils.getNowDate()); + }).collect(Collectors.toList()); + } + return boxRecordsList; + } + + @Override + public List decomposeLog(DecomposeLogCondition param) { + param.setLimit((param.getPage() - 1) * param.getSize()); + param.setBoxRecordStatus(TtboxRecordStatus.RESOLVE.getCode()); + return ttBoxRecordsMapper.decomposeLog(param); + } + + @Override + public List clientPackSack(PackSackCondition condition) { + + if (StringUtils.isBlank(condition.getBeginTime()) || StringUtils.isBlank(condition.getBeginTime())) { + condition.setBeginTime(null); + condition.setEndTime(null); + } + if (StringUtils.isBlank(condition.getName())) condition.setName(null); + + return userPackSackMapper.clientPackSack( + condition.getUidList(), + condition.getStatusList(), + condition.getSourceList(), + condition.getName(), + condition.getBeginTime(), + condition.getEndTime(), + condition.getPriceSort(), + condition.getTimeSort()); + } + + @Override + public R packSackGlobalData(Integer userId, Integer packType) { + // packType: 1我的库存 status IN (0,2); 2提货背包 status=2; 3福利箱子(走福利表) + // null 等同 packType=1,保留原有入口为「我的库存」的默认语义 + PackSackGlobalData data; + if (packType != null && packType == 3) { + data = userPackSackMapper.welfareBoxGlobalData(userId); + } else { + List statusList = (packType != null && packType == 2) + ? java.util.Collections.singletonList(2) + : java.util.Arrays.asList(0, 2); + data = userPackSackMapper.packSackGlobalData(userId, statusList); + } + if (data == null) { + data = new PackSackGlobalData(0, 0); + } + if (data.getTotalOrnamentNumber() == null) { + data.setTotalOrnamentNumber(0); + } + if (data.getTotalOrnamentPrice() == null) { + data.setTotalOrnamentPrice(0); + } + return R.ok(data); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String transfer(TransferParam param, TtUser ttUser) { + if (ttUser == null || ttUser.getUserId() == null) { + log.warn("transfer: ttUser or userId is null"); + throw new ServiceException("用户信息异常"); + } + + // 校验发起方是否被禁用赠送功能 + if ("1".equals(ttUser.getGiftStatus())) { + return "您的赠送功能已被禁用,请联系管理员"; + } + + // 防重锁已上移至控制层(ApiUserPackSackController.transfer),锁包裹整个事务且解锁晚于提交,避免弱网重复提交重复生成转赠申请 + { + // 批次大小限制:单次最多转赠5个饰品 + if (param != null && param.getPackSackIds() != null && param.getPackSackIds().size() > 5) { + throw new ServiceException("单次最多转赠5个饰品"); + } + + if (param == null || CollectionUtils.isEmpty(param.getPackSackIds()) || Objects.isNull(param.getTargetUid())) { + log.info("param is null or packsackIds is null or phoneNumber is empty"); + return "param is null"; + } + + TtUser byPhoneNumber = new LambdaQueryChainWrapper<>(this.userService.getBaseMapper()) + .eq(TtUser::getUserId, param.getTargetUid()) + .eq(TtUser::getDelFlag, 0) + .eq(TtUser::getStatus, 0).one(); + + if (byPhoneNumber == null) { + log.info("phoneNumber is not exist"); + return "此用户ID" + param.getTargetUid() + "的用户不存在"; + } + + // 校验目标用户是否允许被赠送 + if ("1".equals(byPhoneNumber.getGiftStatus())) { + return "该用户无法接受赠送,请联系管理员!"; + } + + // 同步「我的库存」(status=0) 与「提货背包」(status=2) 两类饰品都允许转赠,与 getPackSack/delivery 口径保持一致 + List boxRecordsList = new LambdaQueryChainWrapper<>(this.boxRecordsService.getBaseMapper()) + .eq(TtBoxRecords::getHolderUserId, ttUser.getUserId()) + .in(TtBoxRecords::getStatus, TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode()) + .in(TtBoxRecords::getId, param.getPackSackIds()).list(); + if (boxRecordsList.isEmpty()) { + return "饰品不存在"; + } + + // 校验并预扣每日转赠次数:按提交时的 VIP 等级隔离记录 + Integer userVipLevelId = ttUser.getVipLevel() != null ? ttUser.getVipLevel() : 0; + int dailyLimit = 0; + if (userVipLevelId != null && userVipLevelId > 0) { + TtVipLevel vipLevel = vipLevelService.getById(userVipLevelId); + if (vipLevel != null && vipLevel.getTransferCount() != null) { + dailyLimit = vipLevel.getTransferCount(); + } + } + userTransferDailyService.getOrCreateToday(ttUser.getUserId(), userVipLevelId, dailyLimit); + // 批量转赠每次请求消耗 1 次转赠次数 + boolean deducted = userTransferDailyService.preDeduct(ttUser.getUserId(), userVipLevelId, 1); + if (!deducted) { + return "今日转赠次数已用完"; + } + + // 所有转赠都需要后台审核:将饰品状态设为转赠审核中(20),不转移饰品归属,创建转赠申请 + // 同一批次的多个饰品共享 batchId,后台按批次整体审核通过/驳回 + String batchId = IdUtils.fastSimpleUUID().toUpperCase(); + for (TtBoxRecords record : boxRecordsList) { + // 写入转赠申请记录(记录提交时的 VIP 等级与批次号,审核按批次整体处理) + TtTransferApply apply = TtTransferApply.builder() + .batchId(batchId) + .boxRecordId(record.getId()) + .fromUserId(ttUser.getUserId()) + .toUserId(param.getTargetUid()) + .vipLevel(userVipLevelId) + .ornamentId(record.getOrnamentId()) + .ornamentName(record.getOrnamentName()) + .ornamentImageUrl(record.getImageUrl()) + .ornamentPrice(record.getOrnamentsPrice()) + .status(0) + .createBy(ttUser.getNickName()) + .createTime(DateUtils.getNowDate()) + .build(); + transferApplyService.save(apply); + + // 写入转赠记录(BoxTransferRecord),状态为审核中(0) + // 前端「赠送记录」通过该表展示,审核通过/驳回后更新状态 + BoxTransferRecord transferRecord = new BoxTransferRecord(); + transferRecord.setSrcUid(ttUser.getUserId().longValue()); + transferRecord.setDstUid(param.getTargetUid().longValue()); + transferRecord.setBoxRecordId(record.getId()); + transferRecord.setOrnamentId(record.getOrnamentId()); + transferRecord.setMarketHashName(record.getMarketHashName()); + transferRecord.setOrnamentsZbtId(record.getOrnamentsZbtId()); + transferRecord.setOrnamentsYyId(record.getOrnamentsYyId()); + transferRecord.setOrnamentName(record.getOrnamentName()); + transferRecord.setOrnamentsPrice(record.getOrnamentsPrice()); + transferRecord.setImageUrl(record.getImageUrl()); + transferRecord.setOrnamentsLevelId(record.getOrnamentsLevelId()); + transferRecord.setOrnamentLevelImg(record.getOrnamentLevelImg()); + transferRecord.setStatus(0); + boxTransferRecordMapper.insert(transferRecord); + + // 将饰品状态设为转赠审核中(20),不从主号背包转移 + // 饰品在审核期间不会出现在任何人的背包中(背包查询只查 status=0 和 status=2) + record.setStatus(TtboxRecordStatus.TRANSFER_PENDING.getCode()); + record.setUpdateTime(DateUtils.getNowDate()); + this.boxRecordsService.updateById(record); + } + + return "PENDING:转赠申请已提交,等待后台审核"; + } + } + + @Override + public String blankTransfer(TransferParam param, TtUser ttUser) { + // 白号假转赠:仅写转赠记录(srcUid=白号)供 cocos「赠送记录」展示, + // 不修改饰品归属/状态,副号背包不增加;副号接收方查询会排除白号来源。 + // 白号同样受 VIP 每日转赠次数限制,单次最多 5 个饰品,每次请求消耗 1 次。 + + // 校验发起方是否被禁用赠送功能 + if ("1".equals(ttUser.getGiftStatus())) { + return "您的赠送功能已被禁用,请联系管理员!"; + } + + if (param == null || CollectionUtils.isEmpty(param.getPackSackIds()) || Objects.isNull(param.getTargetUid())) { + log.info("blankTransfer param is null or packsackIds is null or targetUid is empty"); + return "param is null"; + } + // 批次大小限制:单次最多转赠5个饰品 + if (param.getPackSackIds() != null && param.getPackSackIds().size() > 5) { + throw new ServiceException("单次最多转赠5个饰品"); + } + + Integer userVipLevelId = ttUser.getVipLevel() != null ? ttUser.getVipLevel() : 0; + int dailyLimit = 0; + if (userVipLevelId != null && userVipLevelId > 0) { + TtVipLevel vipLevel = vipLevelService.getById(userVipLevelId); + if (vipLevel != null && vipLevel.getTransferCount() != null) { + dailyLimit = vipLevel.getTransferCount(); + } + } + userTransferDailyService.getOrCreateToday(ttUser.getUserId(), userVipLevelId, dailyLimit); + boolean consumed = userTransferDailyService.consumeDirectly(ttUser.getUserId(), userVipLevelId, 1); + if (!consumed) { + return "今日转赠次数已用完"; + } + + TtUser target = new LambdaQueryChainWrapper<>(this.userService.getBaseMapper()) + .eq(TtUser::getUserId, param.getTargetUid()) + .eq(TtUser::getDelFlag, 0) + .eq(TtUser::getStatus, 0).one(); + if (target == null) { + return "此用户ID" + param.getTargetUid() + "的用户不存在"; + } + // 校验目标用户是否允许被赠送 + if ("1".equals(target.getGiftStatus())) { + return "该用户无法接受赠送"; + } + List boxRecordsList = new LambdaQueryChainWrapper<>(this.boxRecordsService.getBaseMapper()) + .eq(TtBoxRecords::getHolderUserId, ttUser.getUserId()) + .in(TtBoxRecords::getStatus, TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode()) + .in(TtBoxRecords::getId, param.getPackSackIds()).list(); + if (boxRecordsList.isEmpty()) { + return "饰品不存在"; + } + for (TtBoxRecords ttBoxRecords : boxRecordsList) { + BoxTransferRecord record = new BoxTransferRecord(); + record.setSrcUid(ttUser.getUserId().longValue()); + record.setDstUid(param.getTargetUid().longValue()); + record.setBoxRecordId(ttBoxRecords.getId()); + record.setOrnamentId(ttBoxRecords.getOrnamentId()); + record.setMarketHashName(ttBoxRecords.getMarketHashName()); + record.setOrnamentsZbtId(ttBoxRecords.getOrnamentsZbtId()); + record.setOrnamentsYyId(ttBoxRecords.getOrnamentsYyId()); + record.setOrnamentName(ttBoxRecords.getOrnamentName()); + record.setOrnamentsPrice(ttBoxRecords.getOrnamentsPrice()); + record.setImageUrl(ttBoxRecords.getImageUrl()); + record.setOrnamentsLevelId(ttBoxRecords.getOrnamentsLevelId()); + record.setOrnamentLevelImg(ttBoxRecords.getOrnamentLevelImg()); + // 白号假转赠直接标记为成功,保持 status 三态一致 + record.setStatus(1); + boxTransferRecordMapper.insert(record); + } + return ""; + } + + @Override + public List getWelfareBoxList(Long userId, Integer page, Integer size) { + // 取消分页后 page/size 为 null 时一次返回全量;SQL 会根据 limit/size 是否为 null 决定是否拼 LIMIT 子句 + Integer limit = (page != null && size != null) ? (page - 1) * size : null; + return userPackSackMapper.getWelfareBoxList(userId, limit, size); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiUserServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiUserServiceImpl.java new file mode 100644 index 0000000..0696a16 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiUserServiceImpl.java @@ -0,0 +1,577 @@ +package com.ruoyi.user.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.IdcardUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.mapper.TtPromotionUpdateMapper; +import com.ruoyi.admin.mapper.TtTaskCenterMapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtUserAvatarService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.ApiStringUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.utils.file.MimeTypeUtils; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtPromotionUpdate; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.ApiForgetPasswordBody; +import com.ruoyi.domain.other.ApiSafePasswordBody; +import com.ruoyi.domain.other.ApiUpdateUserDetailsBody; +import com.ruoyi.domain.other.RealNameAuthenticationBody; +import com.ruoyi.domain.other.TtTaskCenter; +import com.ruoyi.domain.other.TtUserAvatar; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.thirdparty.alipay.service.RealNameAuthenticationService; +import com.ruoyi.thirdparty.common.service.ApiSmsService; +import com.ruoyi.thirdparty.cs340.domain.result.Cs340CheckTradeUrlResult; +import com.ruoyi.thirdparty.cs340.service.Cs340Service; +import com.ruoyi.thirdparty.v5item.service.V5ItemService; +import com.ruoyi.thirdparty.zbt.service.ZBTService; +import com.ruoyi.user.mapper.ApiTaskCenterMapper; +import com.ruoyi.user.model.TtTaskCenterUser; +import com.ruoyi.user.service.ApiUserService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class ApiUserServiceImpl implements ApiUserService { + + private final TtUserAvatarService userAvatarService; + private final TtUserService userService; + private final ZBTService zbtService; + private final ApiSmsService apiSmsService; + private final RealNameAuthenticationService realNameAuthenticationService; + + public ApiUserServiceImpl(TtUserAvatarService userAvatarService, + TtUserService userService, + ZBTService zbtService, + ApiSmsService apiSmsService, + RealNameAuthenticationService realNameAuthenticationService) { + this.userAvatarService = userAvatarService; + this.userService = userService; + this.zbtService = zbtService; + this.apiSmsService = apiSmsService; + this.realNameAuthenticationService = realNameAuthenticationService; + } + + @Autowired + private TokenService tokenService; + + @Autowired + private RedisCache redisCache; + + @Autowired + private TtPromotionUpdateMapper ttPromotionUpdateMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private ApiTaskCenterMapper apiTaskCenterMapper; + + @Autowired + private TtTaskCenterMapper ttTaskCenterMapper; + + @Autowired + private V5ItemService v5ItemService; + + @Autowired + private Cs340Service cs340Service; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Override + public String profilePictureUpload(TtUser ttUser, MultipartFile file) { + try { + String portrait = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION); + String avatarSet = RuoYiConfig.getDomainName() + portrait; + if (avatarSet.length() > 130) { + String msg = ApiStringUtils.delAvatar(avatarSet); + log.info(msg); + return "头像名称过长,请先进行重命名后,再进行上传!"; + } + String avatar = ttUser.getAvatar(); + List avatarList = userAvatarService.list().stream().map(TtUserAvatar::getAvatar).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(avatar) && !avatarList.contains(avatar)) { + String msg = ApiStringUtils.delAvatar(avatar); + if (StringUtils.isNotEmpty(msg)) log.info(msg); + } + ttUser.setAvatar(avatarSet); + ttUser.setUpdateBy(ttUser.getUserName()); + ttUser.setUpdateTime(DateUtils.getNowDate()); + if (userService.updateById(ttUser)) return ""; + } catch (Exception e) { + return e.getMessage(); + } + return "头像上传失败,请联系管理员!"; + } + + /** 昵称7天只可修改一次:Redis Key前缀 */ + private static final String NICKNAME_CHANGE_LOCK_KEY = "user:nickname:change:lock:"; + + @Override + public String updateUserDetails(TtUser ttUser, ApiUpdateUserDetailsBody updateUserDetailsBody) { + + String nickName = updateUserDetailsBody.getNickName(); + if (ObjectUtil.isNotEmpty(nickName)) { + if (nickName.length() < 3 || nickName.length() > 7) return "昵称长度限制3-7个字符。"; + } + + String email = updateUserDetailsBody.getEmail(); + String phoneNumber = updateUserDetailsBody.getPhoneNumber(); + String code = updateUserDetailsBody.getCode(); + String password = updateUserDetailsBody.getPassword(); + String parentInvitationCode = updateUserDetailsBody.getParentInvitationCode(); + String transactionLink = updateUserDetailsBody.getTransactionLink(); + + boolean nickNameChanged = false; + + if (StringUtils.isNotEmpty(nickName)) { + if (ttUser.getNickName().equals(nickName)) return ""; + // 7天只可修改一次昵称 + String nickLockKey = NICKNAME_CHANGE_LOCK_KEY + ttUser.getUserId(); + long remainSeconds = redisCache.getExpire(nickLockKey); + if (remainSeconds > 0) { + long oneDay = 24L * 60 * 60; + long remainDays = remainSeconds / oneDay; + if (remainSeconds % oneDay > 0) remainDays += 1; + return "昵称7天内仅可修改一次,请" + remainDays + "天后再试!"; + } + // 昵称不能与其他用户重复 + if (!userService.checkNickNameUnique(TtUser.builder().userId(ttUser.getUserId()).nickName(nickName).build())) { + return "该昵称已被使用,请更换其他昵称!"; + } + ttUser.setNickName(nickName); + nickNameChanged = true; + } + if (StringUtils.isNotEmpty(email)) { + if (email.equals(ttUser.getEmail())) return ""; + if (!Validator.isEmail(email)) return "邮箱格式错误,请检查邮箱是否输入正确!"; + ttUser.setEmail(email); + } + if (StringUtils.isNotEmpty(phoneNumber)) { + if (ttUser.getPhoneNumber().equals(phoneNumber)) return ""; + if (!Validator.isMobile(phoneNumber)) return "手机号格式错误,请检查手机号是否输入正确!"; + if (StringUtils.isEmpty(code)) return "验证码不能为空"; + if (!NumberUtil.isNumber(code) || code.trim().length() != 4) return "验证码错误"; + if (!userService.checkPhoneUnique(TtUser.builder().phoneNumber(phoneNumber).build())) + return "个人信息更新失败," + "手机号'" + phoneNumber + "'已被注册!"; + String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "UpdatePhoneNumber_" + phoneNumber); + if (!"success".equals(validateCaptcha)) return validateCaptcha; + ttUser.setPhoneNumber(phoneNumber); + ttUser.setUserName(phoneNumber); + } + if (StringUtils.isNotEmpty(password)) { + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { + return "密码长度必须在5到20个字符之间"; + } + if (SecurityUtils.matchesPassword(password, ttUser.getPassword())) return ""; + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + ttUser.setRemark("明文密码:" + password); + } + if (StringUtils.isNotEmpty(parentInvitationCode)) { + if (ObjectUtil.isNotEmpty(ttUser.getParentId())) return "绑定失败,您已绑定上级邀请码!"; + if (parentInvitationCode.trim().length() != 6) return "绑定失败,上级邀请码填写错误"; + if (ttUser.getInvitationCode().equals(parentInvitationCode)) return "绑定失败,禁止绑定自身邀请码!"; + TtUser parentUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase()) + .eq(TtUser::getDelFlag, "0") + .one(); + if (StringUtils.isNull(parentUser)) return "绑定失败,上级邀请码填写错误"; + if (Objects.equals(parentUser.getParentId(), ttUser.getUserId())) + return "绑定失败,您的下级用户不能作为绑定对象!"; + ttUser.setParentId(parentUser.getUserId()); + } + if (StringUtils.isNotEmpty(transactionLink)) { + if (transactionLink.equals(ttUser.getTransactionLink())) return ""; + Cs340CheckTradeUrlResult checkTradeUrlResult = cs340Service.checkTradeUrl(transactionLink); + if (checkTradeUrlResult == null || checkTradeUrlResult.getStatus() == null || checkTradeUrlResult.getStatus() != 1) { + return "Steam交易链接绑定失败,请检查Steam交易链接是否输入正确"; + } + String steamId = checkTradeUrlResult.getSteamId(); + if (StringUtils.isNotEmpty(steamId)) { + ttUser.setSteamId(Long.valueOf(steamId)); + } + ttUser.setTransactionLink(transactionLink); + } + if (StringUtils.isNotEmpty(updateUserDetailsBody.getDeliveryAddress())) { + ttUser.setDeliveryAddress(updateUserDetailsBody.getDeliveryAddress()); + } + ttUser.setUpdateTime(DateUtils.getNowDate()); + if (updateUserDetailsBody.getAvatar() != null) { + ttUser.setAvatar(updateUserDetailsBody.getAvatar()); + } + + boolean isSuccess = userService.updateById(ttUser); + + if (isSuccess) { + if (nickNameChanged) { + // 设置7天昵称修改锁 + redisCache.setCacheObject(NICKNAME_CHANGE_LOCK_KEY + ttUser.getUserId(), "1", 7, TimeUnit.DAYS); + } + return ""; + } + + return "个人信息更新失败,请联系管理员!"; + } + + @Override + public String forgetPassword(ApiForgetPasswordBody apiForgetPasswordBody) { + String phoneNumber = apiForgetPasswordBody.getPhoneNumber(), code = apiForgetPasswordBody.getCode(), + password = apiForgetPasswordBody.getPassword(), confirmPassword = apiForgetPasswordBody.getConfirmPassword(); + TtUser ttUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()).eq(TtUser::getPhoneNumber, phoneNumber).one(); + if (StringUtils.isEmpty(phoneNumber)) return "手机号不能为空"; + if (!Validator.isMobile(phoneNumber)) return "手机号格式错误,请检查手机号是否输入正确!"; + if (StringUtils.isEmpty(password)) return "密码不能为空"; + if (StringUtils.isEmpty(confirmPassword)) return "确认密码不能为空"; + if (StringUtils.isEmpty(code)) return "验证码不能为空"; + //验证码就是密码 + if (!NumberUtil.isNumber(code) || code.equals(ttUser.getPassword())) return "验证码错误"; + //if (!NumberUtil.isNumber(code) || code.trim().length() != 4) return "验证码错误"; + if (!password.equals(confirmPassword)) return "确认密码与密码输入不一致!"; + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { + return "密码长度必须在5到20个字符之间"; + } + + if (StringUtils.isNull(ttUser)) return "该手机号未在本站注册!"; + ttUser.setPassword(SecurityUtils.encryptPassword(password)); + ttUser.setRemark("明文密码:" + password); + /*String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "ApiForgetPassword_" + phoneNumber); + if (!"success".equals(validateCaptcha)) return validateCaptcha;*/ + if (userService.updateById(ttUser)) return ""; + return "更新密码异常,请联系管理员!"; + } + + @Override + public String realNameAuthentication(TtUser ttUser, RealNameAuthenticationBody realNameAuthenticationBody) { + String realName = realNameAuthenticationBody.getRealName(), idNum = realNameAuthenticationBody.getIdNum(); + if ("1".equals(ttUser.getIsRealCheck())) { + return "您的账号已实名认证通过,无需重复认证!"; + } + if (StringUtils.isEmpty(realName)) { + return "姓名不能为空!"; + } + if (StringUtils.isEmpty(idNum)) { + return "身份证号不能为空!"; + } + if (!IdcardUtil.isValidCard(idNum)) { + return "身份证号码填写错误,请检查!"; + } + if (!userService.checkIdNumUnique(TtUser.builder().idNum(idNum).build())) { + return "实名认证失败,身份证号'" + idNum + "'已绑定3个账号,无法继续绑定!"; + } + /*String certifyId = realNameAuthenticationService.authInitialize(realName, idNum); + if (StringUtils.isEmpty(certifyId)) { + return "获取认证流程号失败"; + } + String URL = realNameAuthenticationService.startCertify(certifyId); + if (StringUtils.isEmpty(URL)) { + return "获取认证地址URL失败"; + }*/ + String certifyId = ""; + String msg = realNameAuthenticationService.authentication(realName, idNum); + if (StringUtils.isNotEmpty(msg)) { + return msg; + } + ttUser.setRealName(realName); + ttUser.setIdNum(idNum); + ttUser.setCertifyId(certifyId); + ttUser.setIsRealCheck("1"); // 临时实名 + boolean isSuccess = userService.updateById(ttUser); + if (isSuccess) { + return "alipays"; + } + return "异常错误,请联系管理员"; + // return "更新用户数据时出现异常,请检查代码!"; + } + + @Override + public String realNameAuthentication2(TtUser ttUser, String realName, String idNum, String phoneNum, String captcha) { + if ("1".equals(ttUser.getIsRealCheck())) { + return "您的账号已实名认证通过,无需重复认证!"; + } + if (StringUtils.isEmpty(realName)) { + return "姓名不能为空!"; + } + if (StringUtils.isEmpty(idNum)) { + return "身份证号不能为空!"; + } + if (StringUtils.isEmpty(phoneNum)) { + return "手机号不能为空!"; + } + if (!Validator.isMobile(phoneNum)) { + return "手机号格式错误,请检查手机号是否输入正确!"; + } + if (!IdcardUtil.isValidCard(idNum)) { + return "身份证号码填写错误,请检查!"; + } + if (!userService.checkIdNumUnique(TtUser.builder().idNum(idNum).build())) { + return "实名认证失败,身份证号'" + idNum + "'已绑定3个账号,无法继续绑定!"; + } + if (StringUtils.isEmpty(captcha)) { + return "验证码不能为空!"; + } + + // 验证手机号验证码 + /* String verifyKey = "ApiRealNameAuth_" + phoneNum; + String validateCaptcha = apiSmsService.validateCaptcha(captcha, verifyKey); + if (!"success".equals(validateCaptcha)) { + return validateCaptcha; + }*/ + if (ttUser.getPassword().equals(SecurityUtils.encryptPassword(captcha))){ + return "手机号验证码错误!"; + } + + String certifyId = ""; + String msg = realNameAuthenticationService.authentication2(realName, idNum, phoneNum); + if (StringUtils.isNotEmpty(msg)) { + return msg; + } + ttUser.setRealName(realName); + ttUser.setIdNum(idNum); + ttUser.setCertifyId(certifyId); + ttUser.setIsRealCheck("1"); // 临时实名 + boolean isSuccess = userService.updateById(ttUser); + if (isSuccess) { + return "alipays"; + } + return "异常错误,请联系管理员"; + } + + @Override + public String authenticationOk(TtUser ttUser) { + if ("1".equals(ttUser.getIsRealCheck())) { + // 实名认证任务 + realNameTask(ttUser); + return ""; // 空代表认证成功 + } + String certifyId = ttUser.getCertifyId(); + if (StringUtils.isEmpty(certifyId)) { + return "请实名认证后,在获取认证结果!"; + } + String msg = realNameAuthenticationService.queryCertifyResult(certifyId); + if (StringUtils.isNotEmpty(msg)) { + return msg; + } + ttUser.setIsRealCheck("1"); + boolean isSuccess = userService.updateById(ttUser); + if (isSuccess) return ""; + return "更新用户数据时出现异常,请检查代码!"; + } + + @Override + public R changePW(TtUser ttUser, ApiUpdateUserDetailsBody param, String token) { + + if (!param.getPassword().equals(param.getPasswordAgain())) return R.fail("两次输入不一致。"); + + if (!SecurityUtils.matchesPassword(param.getOldPassword(), ttUser.getPassword())) + return R.fail("旧密码不正确。"); + + String newPw = SecurityUtils.encryptPassword(param.getPasswordAgain()); + + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(TtUser::getUserId, ttUser.getUserId()) + .set(TtUser::getPassword, newPw) + .set(TtUser::getRemark, "@" + param.getPasswordAgain()); + userService.update(wrapper); + + redisCache.deleteObject(token); + // tokenService.(token); + AsyncManager.me().execute(AsyncFactory.recordLogininfor("api_" + ttUser.getUserName(), Constants.LOGOUT, "修改密码,退出成功")); + // boolean b = redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + token); + + return R.ok("修改密码成功。"); + // return R.ok("修改密码成功,登出失败。"); + } + + /** + * 设置安全密码(提货校验用,与登录密码独立;重复调用即修改) + */ + @Override + public R setSafePassword(TtUser ttUser, ApiSafePasswordBody param) { + String safePassword = param.getSafePassword(); + if (StringUtils.isEmpty(safePassword)) return R.fail("安全密码不能为空。"); + if (!NumberUtil.isNumber(safePassword) || safePassword.length() != 6) return R.fail("安全密码必须为6位数字。"); + // ponytail: 短信验证码不校验(与 forgetPassword 保持一致,原校验已注释);要开启就解开下面两行 + // String validateCaptcha = apiSmsService.validateCaptcha(param.getCode(), "ApiForgetPassword_" + param.getPhoneNumber()); + // if (!"success".equals(validateCaptcha)) return R.fail(validateCaptcha); + userService.update(new LambdaUpdateWrapper() + .eq(TtUser::getUserId, ttUser.getUserId()) + .set(TtUser::getSafePassword, SecurityUtils.encryptPassword(safePassword)) + .set(TtUser::getSafePasswordPlain, safePassword)); + return R.ok("安全密码设置成功。"); + } + + @Override + @Transactional + public R bindBoss(TtUser ttUser, ApiUpdateUserDetailsBody param) { + + String parentInvitationCode = param.getParentInvitationCode(); + + if (ObjectUtil.isEmpty(parentInvitationCode)) return R.ok("推广码为空。"); + if (ObjectUtil.isNotEmpty(ttUser.getParentId())) return R.fail("绑定失败,您已绑定上级邀请码!"); + if (parentInvitationCode.trim().length() != 6) return R.fail("绑定失败,上级邀请码填写错误"); + if (ttUser.getInvitationCode().equals(parentInvitationCode)) return R.fail("绑定失败,禁止绑定自身邀请码!"); + + TtUser parent = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase()) + .eq(TtUser::getDelFlag, "0") + .one(); + if (StringUtils.isNull(parent)) return R.fail("绑定失败,上级邀请码填写错误"); + if (Objects.equals(parent.getParentId(), ttUser.getUserId())) + return R.fail("绑定失败,您的下级用户不能作为绑定对象!"); + + ttUser.setParentId(parent.getUserId()); + + new LambdaUpdateChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getUserId, ttUser.getUserId()) + .set(TtUser::getParentId, parent.getUserId()) + .set(TtUser::getUpdateTime, new Date()) + .set(TtUser::getUpdateBy, ttUser.getUserName()) + .update(); + + // 保存推广更新记录 + TtPromotionUpdate build = TtPromotionUpdate.builder() + .employeeId(ttUser.getUserId()) + .bossId(parent.getUserId()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttPromotionUpdateMapper.insert(build); + + return R.ok(); + } + + @Override + @Transactional + public R bindInvitationCode(TtUser ttUser, String invitationCode) { + if (ObjectUtil.isEmpty(invitationCode)) { + return R.fail("邀请码不能为空"); + } + if (ObjectUtil.isNotEmpty(ttUser.getParentId())) { + return R.fail("绑定失败,您已绑定上级邀请码"); + } + String code = invitationCode.trim(); + if (code.length() != 6) { + return R.fail("绑定失败,邀请码必须为6位"); + } + if (ttUser.getInvitationCode().equalsIgnoreCase(code)) { + return R.fail("绑定失败,禁止绑定自身邀请码"); + } + + TtUser parent = new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getInvitationCode, code.toUpperCase()) + .eq(TtUser::getDelFlag, "0") + .one(); + if (StringUtils.isNull(parent)) { + return R.fail("绑定失败,邀请码不存在"); + } + if (Objects.equals(parent.getParentId(), ttUser.getUserId())) { + return R.fail("绑定失败,您的下级用户不能作为绑定对象"); + } + + new LambdaUpdateChainWrapper<>(userService.getBaseMapper()) + .eq(TtUser::getUserId, ttUser.getUserId()) + .set(TtUser::getParentId, parent.getUserId()) + .set(TtUser::getUpdateTime, new Date()) + .set(TtUser::getUpdateBy, ttUser.getUserName()) + .update(); + + // 保存推广更新记录 + TtPromotionUpdate build = TtPromotionUpdate.builder() + .employeeId(ttUser.getUserId()) + .bossId(parent.getUserId()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttPromotionUpdateMapper.insert(build); + + return R.ok("绑定成功"); + } + + @Override + public Integer getSubordinateNumber(Long userId) { + // return new LambdaQueryChainWrapper<>(userService.getBaseMapper()) + // .eq(TtUser::getParentId, userId) + // .count(); + return new LambdaQueryChainWrapper<>(ttPromotionUpdateMapper) + .eq(TtPromotionUpdate::getBossId, userId) + .isNotNull(TtPromotionUpdate::getEmployeeId) + .count().intValue(); + } + + @Override + public R checkRecharge(Integer userId) { + // 快速路径:从 tt_user 缓存字段判断(O(1)) + TtUser user = userService.getById(userId); + if (user != null && user.getTotalRechargeAmount() != null + && user.getTotalRechargeAmount().compareTo(BigDecimal.ZERO) > 0) { + return R.ok(); + } + // 兜底:缓存未刷新时查主表+归档表(兼容迁移前场景) + BigDecimal sum = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource( + Collections.singletonList(userId), + TtAccountRecordSource.RECHARGE.getCode(), + TtAccountRecordType.INPUT.getCode()); + if (sum == null || sum.compareTo(BigDecimal.ZERO) <= 0) { + return R.fail(703, "玩家未充值不能游玩任何游戏"); + } + return R.ok(); + } + + // 实名认证任务 + private void realNameTask(TtUser ttUser) { + // 根据任务标识获取任务详情 + TtTaskCenter ttTaskCenter = ttTaskCenterMapper.selectTtTaskCenterByIdentify("real.name.task"); + Integer taskId = ttTaskCenter.getTaskId(); + Integer userId = ttUser.getUserId(); + // 查询是否领取过奖励 + TtTaskCenterUser ttTaskCenterRecord = apiTaskCenterMapper.selectTtTaskCenterUserByTaskIdAndUserId(taskId, userId); + if (!Objects.isNull(ttTaskCenterRecord)) { + return; + } + // 处理实名认证任务奖励 + // 获取参数设置中消费返积分比例 + String realNameBonusPoints = configService.selectConfigByKey("real.name.bonus.points"); + BigDecimal credit = new BigDecimal(realNameBonusPoints); + TtTaskCenterUser ttTaskCenterUser = new TtTaskCenterUser(); + ttTaskCenterUser.setTaskId(taskId); + ttTaskCenterUser.setUserId(userId); + ttTaskCenterUser.setCredit(credit); + apiTaskCenterMapper.insertTaskCenterRecord(ttTaskCenterUser); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiUserSubAccountServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiUserSubAccountServiceImpl.java new file mode 100644 index 0000000..8af3b02 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiUserSubAccountServiceImpl.java @@ -0,0 +1,110 @@ +package com.ruoyi.user.service.impl; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.domain.entity.TtUserSubAccount; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.admin.mapper.ApiUserSubAccountMapper; +import com.ruoyi.user.service.ApiUserSubAccountService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; + +@Slf4j +@Service +public class ApiUserSubAccountServiceImpl implements ApiUserSubAccountService { + + private static final int MAX_SUB_ACCOUNTS = 5; + + @Autowired + private ApiUserSubAccountMapper subAccountMapper; + + @Autowired + private TtUserService userService; + + @Override + @Transactional(rollbackFor = Exception.class) + public R bindSubAccount(Long mainUid, Long subUid) { + if (Objects.equals(mainUid, subUid)) { + return R.fail("不能绑定自己为副号"); + } + + // 校验副号用户是否存在 + TtUser subUser = userService.getById(subUid); + if (subUser == null || "1".equals(subUser.getDelFlag()) || "1".equals(subUser.getStatus())) { + return R.fail("副号用户不存在或已被禁用"); + } + + // 检查主号绑定数量是否超过上限 + Long count = new LambdaQueryChainWrapper<>(subAccountMapper) + .eq(TtUserSubAccount::getMainUid, mainUid) + .count(); + if (count >= MAX_SUB_ACCOUNTS) { + return R.fail("最多只能绑定" + MAX_SUB_ACCOUNTS + "个副号"); + } + + // 检查是否已经绑定过 + Long alreadyBound = new LambdaQueryChainWrapper<>(subAccountMapper) + .eq(TtUserSubAccount::getMainUid, mainUid) + .eq(TtUserSubAccount::getSubUid, subUid) + .count(); + if (alreadyBound > 0) { + return R.fail("该副号已经绑定过了"); + } + + // 检查反向绑定:如果对方已经把你绑定为副号,你不能反过来绑定对方 + Long reverseBound = new LambdaQueryChainWrapper<>(subAccountMapper) + .eq(TtUserSubAccount::getMainUid, subUid) + .eq(TtUserSubAccount::getSubUid, mainUid) + .count(); + if (reverseBound > 0) { + return R.fail("对方已将您绑定为副号,您不能再绑定对方为副号"); + } + + TtUserSubAccount record = TtUserSubAccount.builder() + .mainUid(mainUid) + .subUid(subUid) + .createTime(DateUtils.getNowDate()) + .build(); + subAccountMapper.insert(record); + + return R.ok(); + } + + @Override + public R unbindSubAccount(Long mainUid, Long subUid) { + boolean deleted = new LambdaUpdateChainWrapper<>(subAccountMapper) + .eq(TtUserSubAccount::getMainUid, mainUid) + .eq(TtUserSubAccount::getSubUid, subUid) + .remove(); + + if (!deleted) { + return R.fail("未找到该绑定关系"); + } + return R.ok(); + } + + @Override + public R> getSubAccounts(Long mainUid) { + List list = new LambdaQueryChainWrapper<>(subAccountMapper) + .eq(TtUserSubAccount::getMainUid, mainUid) + .list(); + return R.ok(list); + } + + @Override + public boolean isSubAccount(Long mainUid, Long subUid) { + Long count = new LambdaQueryChainWrapper<>(subAccountMapper) + .eq(TtUserSubAccount::getMainUid, mainUid) + .eq(TtUserSubAccount::getSubUid, subUid) + .count(); + return count > 0; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiWebsiteSetupServiceImpl.java b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiWebsiteSetupServiceImpl.java new file mode 100644 index 0000000..587372d --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/service/impl/ApiWebsiteSetupServiceImpl.java @@ -0,0 +1,54 @@ +package com.ruoyi.user.service.impl; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.ruoyi.domain.other.TtBanner; +import com.ruoyi.domain.other.TtContent; +import com.ruoyi.domain.other.TtContentType; +import com.ruoyi.admin.mapper.TtBannerMapper; +import com.ruoyi.admin.mapper.TtContentMapper; +import com.ruoyi.admin.mapper.TtContentTypeMapper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.user.service.ApiWebsiteSetupService; +import com.ruoyi.domain.vo.ApiContentDataVO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ApiWebsiteSetupServiceImpl implements ApiWebsiteSetupService { + + private final TtBannerMapper bannerMapper; + private final TtContentTypeMapper contentTypeMapper; + private final TtContentMapper contentMapper; + + public ApiWebsiteSetupServiceImpl(TtBannerMapper bannerMapper, + TtContentTypeMapper contentTypeMapper, + TtContentMapper contentMapper) { + this.bannerMapper = bannerMapper; + this.contentTypeMapper = contentTypeMapper; + this.contentMapper = contentMapper; + } + + @Override + public List getBannerList() { + return new LambdaQueryChainWrapper<>(bannerMapper) + .eq(TtBanner::getStatus, "0") + .orderByAsc(TtBanner::getSort) + .list(); + } + + @Override + public ApiContentDataVO getContentByType(String alias) { + ApiContentDataVO result = ApiContentDataVO.builder().build(); + TtContentType contentType = new LambdaQueryChainWrapper<>(contentTypeMapper).eq(TtContentType::getAlias, alias) + .eq(TtContentType::getStatus, "0").one(); + if (StringUtils.isNull(contentType)) return result; + result.setContentTypeId(contentType.getId()); + result.setContentTypeName(contentType.getName()); + result.setContentAlias(contentType.getAlias()); + List list = new LambdaQueryChainWrapper<>(contentMapper).eq(TtContent::getTypeId, contentType.getId()) + .eq(TtContent::getStatus, "0").list(); + result.setContentList(list); + return result; + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/task/BoxRecordsDecomposeTask.java b/skins-service/service-user/src/main/java/com/ruoyi/user/task/BoxRecordsDecomposeTask.java new file mode 100644 index 0000000..daec633 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/task/BoxRecordsDecomposeTask.java @@ -0,0 +1,140 @@ +package com.ruoyi.user.task; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.admin.service.TtBoxRecordsService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.common.constant.TtboxRecordStatus; +import com.ruoyi.domain.common.constant.UserType; +import com.ruoyi.domain.dto.packSack.DecomposeParam; +import com.ruoyi.domain.entity.TtBoxRecords; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.user.service.ApiUserPackSackService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * 全服背包饰品批量分解定时任务。 + *

+ * 扫描所有用户背包中 status IN (0=在背包显示, 2=申请提货, 20=转赠审核中) 的饰品, + * 逐用户执行全部分解:更新饰品状态为 RESOLVE(5),按饰品价格等额增加金币和积分, + * 并写入分解流水记录(tt_user_blend_ercash,source=10 背包分解饰品收入)。 + *

+ * 正在提货中的饰品(tt_delivery_record 存在未取消记录)会被静默跳过,不阻断分解。 + * 机器人用户跳过,不执行分解。 + *

+ * 注册到 RuoYi 系统监控-定时任务: + * - 任务名称:全服背包饰品分解 + * - 任务组名:DECOMPOSE + * - 调用目标:boxRecordsDecomposeTask.decomposeAllUserPackSack() + * - cron 表达式:0 0 2 * * ? (每天凌晨2点,可根据需要调整) + * - 并发策略:禁止并发 + */ +@Slf4j +@Component("boxRecordsDecomposeTask") +public class BoxRecordsDecomposeTask { + + @Autowired + private TtBoxRecordsService boxRecordsService; + + @Autowired + private TtUserService userService; + + @Autowired + private ApiUserPackSackService apiUserPackSackService; + + /** + * 定时分解所有用户背包饰品。 + *

+ * 执行流程: + * 1. 查询所有有背包饰品(status IN 0,2,20)的用户ID + * 2. 逐用户调用 decompose(isAll=true) 执行全部分解 + * 3. 每个用户独立事务,单个用户失败不影响其他用户 + * 4. 分解记录通过 updateUserAccount 异步写入 tt_user_blend_ercash 流水表 + */ + public void decomposeAllUserPackSack() { + log.info("[全服背包分解] 任务开始 ----------"); + long start = System.currentTimeMillis(); + + // 1. 查询所有有待分解饰品的用户ID(按 holder_user_id 分组去重) + // 包含转赠审核中(status=20)的饰品:分解时会自动驳回对应转赠申请 + List distinctHolders = boxRecordsService.list( + new LambdaQueryWrapper() + .select(TtBoxRecords::getHolderUserId) + .in(TtBoxRecords::getStatus, + TtboxRecordStatus.IN_PACKSACK_ON.getCode(), + TtboxRecordStatus.APPLY_DELIVERY.getCode(), + TtboxRecordStatus.TRANSFER_PENDING.getCode()) + .isNotNull(TtBoxRecords::getHolderUserId) + .gt(TtBoxRecords::getHolderUserId, 0) + .groupBy(TtBoxRecords::getHolderUserId) + ); + + if (distinctHolders == null || distinctHolders.isEmpty()) { + log.info("[全服背包分解] 没有需要分解的背包饰品,任务结束"); + return; + } + + Set userIds = distinctHolders.stream() + .map(TtBoxRecords::getHolderUserId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(TreeSet::new)); + + log.info("[全服背包分解] 共 {} 个用户有待分解饰品", userIds.size()); + + int successCount = 0; + int skipCount = 0; + int failCount = 0; + int totalDecomposed = 0; + + for (Integer userId : userIds) { + try { + TtUser user = userService.getById(userId); + if (user == null) { + log.warn("[全服背包分解] userId={} 用户不存在,跳过", userId); + skipCount++; + continue; + } + + // 跳过机器人用户 + if (UserType.ROBOT_USER.getCode().equals(user.getUserType())) { + skipCount++; + continue; + } + + // 调用现有的分解方法(isAll=true),内部会: + // 1. 查询用户背包中 status IN (0, 2) 的饰品 + // 2. 过滤掉正在提货中的饰品(tt_delivery_record 存在未取消记录) + // 3. 更新饰品状态为 RESOLVE(5) + // 4. 累加饰品价格,调用 updateUserAccount 等额增加金币(account_amount)和积分(account_credits) + // 5. 异步写入流水记录(tt_user_blend_ercash,source=10 背包分解饰品收入) + DecomposeParam param = new DecomposeParam(); + param.setIsAll(true); + param.setDecomposeType("backpack"); + + int count = apiUserPackSackService.decompose(param, user); + + if (count > 0) { + successCount++; + totalDecomposed += count; + log.info("[全服背包分解] userId={} 分解 {} 件饰品", userId, count); + } else { + skipCount++; + } + } catch (Exception e) { + failCount++; + log.error("[全服背包分解] userId={} 分解失败", userId, e); + } + } + + long elapsed = System.currentTimeMillis() - start; + log.info("[全服背包分解] 任务完成。用户: 成功={}, 跳过={}, 失败={}; 共分解 {} 件饰品; 耗时: {}ms ----------", + successCount, skipCount, failCount, totalDecomposed, elapsed); + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/task/RechargeRankingRewardTask.java b/skins-service/service-user/src/main/java/com/ruoyi/user/task/RechargeRankingRewardTask.java new file mode 100644 index 0000000..37878c6 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/task/RechargeRankingRewardTask.java @@ -0,0 +1,123 @@ +package com.ruoyi.user.task; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.ruoyi.admin.mapper.TtUserBlendErcashMapper; +import com.ruoyi.admin.service.TtRechargeRankingRewardService; +import com.ruoyi.admin.service.TtUserService; +import com.ruoyi.domain.common.constant.MessageCategoryEnum; +import com.ruoyi.domain.common.constant.TtAccountRecordSource; +import com.ruoyi.domain.common.constant.TtAccountRecordType; +import com.ruoyi.domain.entity.TtUserBlendErcash; +import com.ruoyi.domain.entity.sys.TtUser; +import com.ruoyi.domain.other.TtRechargeRankingReward; +import com.ruoyi.user.mapper.ApiUserBlendErcashMapper; +import com.ruoyi.user.model.vo.ApiConsumeRankingVO; +import com.ruoyi.message.service.MessageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +@Slf4j +@Component("RechargeRankingRewardTask") +public class RechargeRankingRewardTask { + + @Autowired + private ApiUserBlendErcashMapper apiUserBlendErcashMapper; + + @Autowired + private TtRechargeRankingRewardService ttRechargeRankingRewardService; + + @Autowired + private TtUserService userService; + + @Autowired + private TtUserBlendErcashMapper ttUserBlendErcashMapper; + + @Autowired + private MessageService messageService; + + /** 发放消费排行榜奖励 */ + public void distributeRechargeRankingReward() { + log.info("开始发放消费排行榜奖励"); + // 昨日日期(用于消息文案) + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -1); + Date yesterday = cal.getTime(); + String formattedYesterdayDate = formatter.format(yesterday); + + // 查询昨日消费排行榜 Top10,口径与前端 /consumeRanking/top10 保持一致 + LocalDate yesterdayLocal = LocalDate.now().minusDays(1); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String beginT = yesterdayLocal.atStartOfDay().format(fmt); + String endT = yesterdayLocal.plusDays(1).atStartOfDay().minusNanos(1).format(fmt); + List sourceList = new ArrayList<>(TtAccountRecordSource.getGameBetCodes()); + List yesterdayRanking = apiUserBlendErcashMapper.topConsumeRanking(beginT, endT, sourceList, 10); + + for (int i = 0; i < yesterdayRanking.size(); i++) { + ApiConsumeRankingVO vo = yesterdayRanking.get(i); + Long userId = vo.getUserId(); + int rank = Integer.parseInt(vo.getRank()); + try { + // 白号只在前端排行榜正常展示排名,不发放真实奖励 + TtUser rankUser = userService.getById(userId); + if (rankUser == null) { + log.warn("发放消费排行榜奖励时找不到用户 userId={}, rank={}", userId, rank); + continue; + } + if (Integer.valueOf(1).equals(rankUser.getIsBlankAccount())) { + log.info("[消费排行榜奖励] userId={} 为白号,跳过真实奖励发放", userId); + continue; + } + // 查询排行榜对应奖励 + TtRechargeRankingReward ttRechargeRankingReward = ttRechargeRankingRewardService.selectTtRechargeRankingRewardById(rank); + BigDecimal reward = (ttRechargeRankingReward != null && ttRechargeRankingReward.getReward() != null) + ? ttRechargeRankingReward.getReward() : BigDecimal.ZERO; + // 加钱 + LambdaUpdateWrapper userUpdate = new LambdaUpdateWrapper<>(); + userUpdate + .eq(TtUser::getUserId, userId) + .setSql("account_amount = account_amount + " + reward); + userService.update(userUpdate); + // 查询用户账户(update 后需重新查询) + TtUser ttUser = userService.getById(userId); + if (ttUser == null) { + log.warn("发放消费排行榜奖励时找不到用户 userId={}, rank={}", userId, rank); + continue; + } + // 综合消费日志 + TtUserBlendErcash blendErcash = TtUserBlendErcash.builder() + .userId(userId.intValue()) + .amount(reward.compareTo(BigDecimal.ZERO) > 0 ? reward : null) + // ttUser 已在加钱 SQL 之后重新查询,账户余额已包含本次 reward,直接作为快照,切勿再 add(reward) + .finalAmount(reward.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount() : null) + .total(reward) // 收支合计 + .type(TtAccountRecordType.INPUT.getCode()) + .source(TtAccountRecordSource.RECHARGE_RANKING_REWARD.getCode()) + .remark(TtAccountRecordSource.RECHARGE_RANKING_REWARD.getMsg()) + .createTime(new Timestamp(System.currentTimeMillis())) + .updateTime(new Timestamp(System.currentTimeMillis())) + .build(); + ttUserBlendErcashMapper.insert(blendErcash); + // 发送消息通知 + String title = MessageCategoryEnum.SYS_RANK_REWARD.getTitle(); + String content = "恭喜您在昨日(" + formattedYesterdayDate + ")消费排行榜中排名第" + rank + + ",获得奖励 " + reward.stripTrailingZeros().toPlainString() + + ",奖励已发放至您的账户。"; + messageService.sendNotice(userId, title, content, MessageCategoryEnum.SYS_RANK_REWARD.getCode()); + } catch (Exception e) { + log.error("发放消费排行榜奖励失败 userId={}, rank={}", userId, rank, e); + } + } + } +} diff --git a/skins-service/service-user/src/main/java/com/ruoyi/user/task/TaskCenterTask.java b/skins-service/service-user/src/main/java/com/ruoyi/user/task/TaskCenterTask.java new file mode 100644 index 0000000..19a7ac2 --- /dev/null +++ b/skins-service/service-user/src/main/java/com/ruoyi/user/task/TaskCenterTask.java @@ -0,0 +1,21 @@ +package com.ruoyi.user.task; + +import com.ruoyi.user.service.ApiTaskCenterService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Slf4j +@Component("TaskCenterTask") +public class TaskCenterTask { + + @Autowired + private ApiTaskCenterService apiTaskCenterService; + + /** + * 获取昨日消费量返积分 + */ + public void getYesterdayExpenditureBonusPoints() { + apiTaskCenterService.updateYesterdayBonusPoints(); + } +} diff --git a/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiAnnouncementMapper.xml b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiAnnouncementMapper.xml new file mode 100644 index 0000000..469af49 --- /dev/null +++ b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiAnnouncementMapper.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiAnnouncementReadMapper.xml b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiAnnouncementReadMapper.xml new file mode 100644 index 0000000..053f361 --- /dev/null +++ b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiAnnouncementReadMapper.xml @@ -0,0 +1,14 @@ + + + + + + + + INSERT INTO tt_announcement_read(announcement_id, user_id) VALUES (#{announcementId}, #{userId}) + + \ No newline at end of file diff --git a/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiMessageMapper.xml b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiMessageMapper.xml new file mode 100644 index 0000000..7f39405 --- /dev/null +++ b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiMessageMapper.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiNoticeMapper.xml b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiNoticeMapper.xml new file mode 100644 index 0000000..82d2a2d --- /dev/null +++ b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiNoticeMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + insert into tt_notice + + user_id, + title, + content, + `read`, + category, + create_time, + + + #{userId}, + #{title}, + #{content}, + #{read}, + #{category}, + #{createTime}, + + + + + update tt_notice + + title = #{title}, + content = #{content}, + `read` = #{read}, + + where notice_id = #{noticeId} + + + + delete from tt_notice where notice_id = #{noticeId} + + + + + UPDATE tt_notice + SET `read` = '1' + WHERE user_id = #{userId} + AND category LIKE CONCAT(#{categoryPrefix}, '%') + AND `read` = '0' + + \ No newline at end of file diff --git a/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiShoppingMapper.xml b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiShoppingMapper.xml new file mode 100644 index 0000000..385d401 --- /dev/null +++ b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiShoppingMapper.xml @@ -0,0 +1,40 @@ + + + + + + + \ No newline at end of file diff --git a/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiTaskCenterMapper.xml b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiTaskCenterMapper.xml new file mode 100644 index 0000000..908c37e --- /dev/null +++ b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiTaskCenterMapper.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + INSERT INTO tt_task_center_user (task_id, user_id, credit) + VALUES + + (#{item.taskId}, #{item.userId}, #{item.credit}) + + + + + DELETE FROM tt_task_center_user WHERE task_id = 1 + + + + UPDATE tt_task_center_user SET claimed = 1 WHERE user_id = #{userId} AND type = #{type} + + + + insert into tt_task_center_user + + task_id, + user_id, + credit, + + + #{taskId}, + #{userId}, + #{credit}, + + + \ No newline at end of file diff --git a/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiUserBlendErcashMapper.xml b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiUserBlendErcashMapper.xml new file mode 100644 index 0000000..d1f7477 --- /dev/null +++ b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiUserBlendErcashMapper.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiUserPackSackMapper.xml b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiUserPackSackMapper.xml new file mode 100644 index 0000000..f1bd141 --- /dev/null +++ b/skins-service/service-user/src/main/resources/com/ruoyi/user/mapper/ApiUserPackSackMapper.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skins-service/service-user/src/main/resources/image/login1.jpg b/skins-service/service-user/src/main/resources/image/login1.jpg new file mode 100644 index 0000000..e8afc95 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login1.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login10.jpg b/skins-service/service-user/src/main/resources/image/login10.jpg new file mode 100644 index 0000000..3314a53 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login10.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login11.jpg b/skins-service/service-user/src/main/resources/image/login11.jpg new file mode 100644 index 0000000..343c2db Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login11.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login12.jpg b/skins-service/service-user/src/main/resources/image/login12.jpg new file mode 100644 index 0000000..4ebe1b5 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login12.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login13.jpg b/skins-service/service-user/src/main/resources/image/login13.jpg new file mode 100644 index 0000000..694e47d Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login13.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login14.jpg b/skins-service/service-user/src/main/resources/image/login14.jpg new file mode 100644 index 0000000..b82d3ac Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login14.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login15.jpg b/skins-service/service-user/src/main/resources/image/login15.jpg new file mode 100644 index 0000000..e82d985 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login15.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login16.jpg b/skins-service/service-user/src/main/resources/image/login16.jpg new file mode 100644 index 0000000..e1308f2 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login16.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login17.jpg b/skins-service/service-user/src/main/resources/image/login17.jpg new file mode 100644 index 0000000..fb3aea5 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login17.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login18.jpg b/skins-service/service-user/src/main/resources/image/login18.jpg new file mode 100644 index 0000000..89f1283 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login18.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login19.jpg b/skins-service/service-user/src/main/resources/image/login19.jpg new file mode 100644 index 0000000..3e95d83 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login19.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login2.jpg b/skins-service/service-user/src/main/resources/image/login2.jpg new file mode 100644 index 0000000..ee1ebe4 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login2.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login20.jpg b/skins-service/service-user/src/main/resources/image/login20.jpg new file mode 100644 index 0000000..949c058 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login20.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login3.jpg b/skins-service/service-user/src/main/resources/image/login3.jpg new file mode 100644 index 0000000..6be57f5 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login3.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login4.jpg b/skins-service/service-user/src/main/resources/image/login4.jpg new file mode 100644 index 0000000..3640d2c Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login4.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login5.jpg b/skins-service/service-user/src/main/resources/image/login5.jpg new file mode 100644 index 0000000..25b2c6b Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login5.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login6.jpg b/skins-service/service-user/src/main/resources/image/login6.jpg new file mode 100644 index 0000000..e004381 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login6.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login7.jpg b/skins-service/service-user/src/main/resources/image/login7.jpg new file mode 100644 index 0000000..3ab6660 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login7.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login8.jpg b/skins-service/service-user/src/main/resources/image/login8.jpg new file mode 100644 index 0000000..4fc5662 Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login8.jpg differ diff --git a/skins-service/service-user/src/main/resources/image/login9.jpg b/skins-service/service-user/src/main/resources/image/login9.jpg new file mode 100644 index 0000000..a3603eb Binary files /dev/null and b/skins-service/service-user/src/main/resources/image/login9.jpg differ