收银台双模式收款与打包配置全链路实现
- 收银员新增 can_cash 现金收款权限字段(登录/列表/新增/编辑全链路) - 购物车结算支持 pay_mode=cash/bond 双模式,现金模式跳过余额校验与扣债券 - 收银台前端读取可配置 packmoney 实时报价,修复此前硬编码+2元与实际收费不符 - 管理后台新增打包费(packmoney)与环保减免(green_discount)配置端口 - 同步接口新增 get/set_shop_config,Java端定时器拉取配置 - 购物小票书塞入首个潜影盒,免费潜影盒订正为原色(SHULKER_BOX)
This commit is contained in:
@@ -407,6 +407,8 @@ async function loadShop(el) {
|
||||
<p style="color:var(--dim);font-size:13px;margin-bottom:14px">配置购物车结算时「塞背包」折扣与「潜影盒打包」加价系数,系数作用于商品原价合计。</p>
|
||||
<div class="form-row"><label>塞背包折扣系数</label><input id="cfgBackpack" type="number" step="0.01" min="0.01" max="1" oninput="updateCartCfgHint()"></div>
|
||||
<div class="form-row"><label>潜影盒加价系数</label><input id="cfgShulker" type="number" step="0.01" min="1" max="3" oninput="updateCartCfgHint()"></div>
|
||||
<div class="form-row"><label>彩色潜影盒打包费</label><input id="cfgPackMoney" type="number" min="0" max="999" value="5"><span style="font-size:12px;color:var(--dim)">彩色潜影盒加收债券数(原色免费)</span></div>
|
||||
<div class="form-row"><label>环保单减免(%)</label><input id="cfgGreen" type="number" min="0" max="9.99" step="0.01" value="2"><span style="font-size:12px;color:var(--dim)">0 = 不减免;玩家选「不打包」时按比例减免(命令 /sdf1_login shop setgreen 10 亦为不减免)</span></div>
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||||
<button class="btn btn-green" onclick="saveCartConfig()">保存配置</button>
|
||||
<span id="cartCfgHint" style="font-size:12px;color:var(--accent)"></span>
|
||||
@@ -423,8 +425,12 @@ async function loadCartConfig() {
|
||||
if (r && r.success && r.data) {
|
||||
const bp = document.getElementById('cfgBackpack');
|
||||
const sh = document.getElementById('cfgShulker');
|
||||
const pm = document.getElementById('cfgPackMoney');
|
||||
const gd = document.getElementById('cfgGreen');
|
||||
if (bp && r.data.backpack_rate != null) bp.value = r.data.backpack_rate;
|
||||
if (sh && r.data.shulker_rate != null) sh.value = r.data.shulker_rate;
|
||||
if (pm && r.data.packmoney != null) pm.value = r.data.packmoney;
|
||||
if (gd && r.data.green_discount != null) gd.value = r.data.green_discount;
|
||||
updateCartCfgHint();
|
||||
}
|
||||
} catch (e) {}
|
||||
@@ -448,12 +454,19 @@ function updateCartCfgHint() {
|
||||
async function saveCartConfig() {
|
||||
const bpEl = document.getElementById('cfgBackpack');
|
||||
const shEl = document.getElementById('cfgShulker');
|
||||
const pmEl = document.getElementById('cfgPackMoney');
|
||||
const gdEl = document.getElementById('cfgGreen');
|
||||
const bp = parseFloat(bpEl.value);
|
||||
const sh = parseFloat(shEl.value);
|
||||
const pm = parseInt(pmEl.value || '5', 10);
|
||||
let gd = parseFloat(gdEl.value || '0');
|
||||
if (gd >= 10) gd = 0; // 10 视为不减免
|
||||
if (isNaN(bp) || isNaN(sh)) { toast('请输入有效数字', 'err'); return; }
|
||||
if (bp <= 0 || bp > 1) { toast('塞背包折扣系数应在 0.01 ~ 1.00 之间', 'err'); return; }
|
||||
if (sh < 1 || sh > 3) { toast('潜影盒加价系数应在 1.00 ~ 3.00 之间', 'err'); return; }
|
||||
const r = await postApi('save_shop_config', { cart_backpack_rate: bp, cart_shulker_rate: sh });
|
||||
if (isNaN(pm) || pm < 0 || pm > 999) { toast('打包费应在 0 ~ 999 之间', 'err'); return; }
|
||||
if (gd < 0 || gd > 9.99) { toast('环保单减免应在 0 ~ 9.99 之间', 'err'); return; }
|
||||
const r = await postApi('save_shop_config', { cart_backpack_rate: bp, cart_shulker_rate: sh, packmoney: pm, green_discount: gd });
|
||||
toast(r.message || (r.success ? '已保存' : '保存失败'), r.success ? 'ok' : 'err');
|
||||
if (r.success) updateCartCfgHint();
|
||||
}
|
||||
@@ -3177,11 +3190,12 @@ async function loadCashierManage(el) {
|
||||
el.innerHTML = `<div class="card">
|
||||
<h2>🧾 收银员管理</h2>
|
||||
<p style="color:var(--dim);font-size:12px;margin-bottom:12px">收银员可登录独立收银台(cashier.php)代玩家购买商品并手动打折,折扣上限在此设置。管理员亦可登录收银台。</p>
|
||||
<div class="card" style="background:var(--bg)">
|
||||
<div class="card" style="background:var(--bg)">
|
||||
<h2 style="font-size:14px;color:var(--green)">+ 新增收银员</h2>
|
||||
<div class="form-row"><label>账号</label><input id="cmUser" placeholder="3-20位字母/数字/下划线"></div>
|
||||
<div class="form-row"><label>密码</label><input id="cmPass" type="password" placeholder="登录密码"></div>
|
||||
<div class="form-row"><label>折扣上限%</label><input id="cmLimit" type="number" min="0" max="100" value="10"></div>
|
||||
<div class="form-row"><label>现金收款权限</label><select id="cmCash"><option value="0">否(仅债券收款)</option><option value="1">是(可现金收款)</option></select></div>
|
||||
<button class="btn btn-green" onclick="cmAdd()">创建收银员</button>
|
||||
</div>
|
||||
<div id="cmList" style="margin-top:14px">加载中…</div>
|
||||
@@ -3195,11 +3209,12 @@ async function cmAdd() {
|
||||
const limit = parseInt(document.getElementById('cmLimit').value || '0', 10);
|
||||
if (!username || !password) { toast('账号和密码不能为空', 'err'); return; }
|
||||
if (limit < 0 || limit > 100) { toast('折扣上限需在0-100', 'err'); return; }
|
||||
const canCash = parseInt(document.getElementById('cmCash').value || '0', 10);
|
||||
try {
|
||||
const d = await fetch('api/cashier.php?action=add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password) + '&discount_limit_percent=' + limit
|
||||
body: 'username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password) + '&discount_limit_percent=' + limit + '&can_cash=' + canCash
|
||||
}).then(r => r.json());
|
||||
if (d.success) { toast('收银员已创建', 'ok'); document.getElementById('cmUser').value = ''; document.getElementById('cmPass').value = ''; cmLoad(); }
|
||||
else toast(d.message || '创建失败', 'err');
|
||||
@@ -3213,21 +3228,22 @@ async function cmLoad() {
|
||||
const d = await fetch('api/cashier.php?action=cashier_list').then(r => r.json());
|
||||
if (!d.success) { wrap.innerHTML = '<div style="color:var(--red);font-size:13px">' + escAdmHtml(d.message) + '</div>'; return; }
|
||||
if (!d.data.length) { wrap.innerHTML = '<div style="color:var(--dim);font-size:13px">暂无收银员</div>'; return; }
|
||||
wrap.innerHTML = '<table class="table"><tr><th>ID</th><th>账号</th><th>折扣上限</th><th>创建时间</th><th>操作</th></tr>' + d.data.map(c => `
|
||||
wrap.innerHTML = '<table class="table"><tr><th>ID</th><th>账号</th><th>折扣上限</th><th>现金收款</th><th>创建时间</th><th>操作</th></tr>' + d.data.map(c => `
|
||||
<tr>
|
||||
<td>${c.id}</td>
|
||||
<td>${escAdmHtml(c.username)}</td>
|
||||
<td>${c.discount_limit_percent}%</td>
|
||||
<td>${c.can_cash ? '✅ 可现金' : '— 仅债券'}</td>
|
||||
<td style="color:var(--dim)">${c.created_at ? new Date(c.created_at * 1000).toLocaleString('zh-CN') : '-'}</td>
|
||||
<td>
|
||||
<button class="btn btn-yellow" style="padding:4px 10px;font-size:11px" onclick="cmEdit(${c.id},'${escAdmHtml(c.username)}',${c.discount_limit_percent})">编辑</button>
|
||||
<button class="btn btn-yellow" style="padding:4px 10px;font-size:11px" onclick="cmEdit(${c.id},'${escAdmHtml(c.username)}',${c.discount_limit_percent},${c.can_cash ? 1 : 0})">编辑</button>
|
||||
<button class="btn btn-red" style="padding:4px 10px;font-size:11px" onclick="cmDelete(${c.id})">删除</button>
|
||||
</td>
|
||||
</tr>`).join('') + '</table>';
|
||||
} catch (e) { wrap.innerHTML = '<div style="color:var(--red);font-size:13px">' + escAdmHtml(e.message) + '</div>'; }
|
||||
}
|
||||
|
||||
async function cmEdit(id, username, curLimit) {
|
||||
async function cmEdit(id, username, curLimit, curCash) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'glass-alert-overlay show';
|
||||
overlay.innerHTML = `<div class="glass-alert-card">
|
||||
@@ -3237,6 +3253,11 @@ async function cmEdit(id, username, curLimit) {
|
||||
<input class="alert-input" type="password" id="cmNewPw" placeholder="留空=不修改">
|
||||
<div class="alert-label">折扣上限(0-100,当前 ${curLimit}%)</div>
|
||||
<input class="alert-input" type="number" id="cmNewLimit" min="0" max="100" value="${curLimit}">
|
||||
<div class="alert-label">现金收款权限(当前 ${curCash ? '可现金' : '仅债券'})</div>
|
||||
<select class="alert-input" id="cmNewCash">
|
||||
<option value="0"${curCash ? '' : ' selected'}>否(仅债券收款)</option>
|
||||
<option value="1"${curCash ? ' selected' : ''}>是(可现金收款)</option>
|
||||
</select>
|
||||
<div class="alert-btns">
|
||||
<button class="ag-cancel" onclick="this.closest('.glass-alert-overlay').remove()">取消</button>
|
||||
<button class="ag-ok" onclick="cmEditSave(${id}, this)">保存</button>
|
||||
@@ -3250,10 +3271,12 @@ async function cmEditSave(id, btn) {
|
||||
const pw = document.getElementById('cmNewPw').value;
|
||||
const limit = parseInt(document.getElementById('cmNewLimit').value || '0', 10);
|
||||
if (limit < 0 || limit > 100) { toast('折扣上限需在0-100', 'err'); return; }
|
||||
const cash = parseInt(document.getElementById('cmNewCash').value || '0', 10);
|
||||
const body = new URLSearchParams();
|
||||
body.set('id', String(id));
|
||||
if (pw) body.set('password', pw);
|
||||
body.set('discount_limit_percent', String(limit));
|
||||
body.set('can_cash', String(cash));
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const d = await fetch('api/cashier.php?action=edit', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() }).then(r => r.json());
|
||||
|
||||
+47
-21
@@ -290,36 +290,62 @@ function adminShopRemove() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 购物车结算折扣/加价配置 =====
|
||||
// ===== 商店配置(折扣/加价系数 + 打包费 + 环保减免)=====
|
||||
function adminSaveShopConfig() {
|
||||
requireAdminSession();
|
||||
$db = getDB();
|
||||
try {
|
||||
$bp = getParam('cart_backpack_rate');
|
||||
$sh = getParam('cart_shulker_rate');
|
||||
if ($bp === null || $sh === null) error('缺少配置参数');
|
||||
$bp = (float)$bp;
|
||||
$sh = (float)$sh;
|
||||
// 校验范围:塞背包折扣系数 0.01~1.00;潜影盒加价系数 1.00~3.00
|
||||
if ($bp <= 0 || $bp > 1.0) error('塞背包折扣系数必须介于 0.01 ~ 1.00 之间');
|
||||
if ($sh < 1.0 || $sh > 3.0) error('潜影盒加价系数必须介于 1.00 ~ 3.00 之间');
|
||||
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS shop_config (cfg_key TEXT PRIMARY KEY, cfg_value TEXT NOT NULL)");
|
||||
$upsert = $db->prepare("INSERT OR REPLACE INTO shop_config (cfg_key, cfg_value) VALUES (:k, :v)");
|
||||
$upsert->bindValue(':k', 'cart_backpack_rate', SQLITE3_TEXT);
|
||||
$upsert->bindValue(':v', number_format($bp, 2, '.', ''), SQLITE3_TEXT);
|
||||
$upsert->execute();
|
||||
$upsert->bindValue(':k', 'cart_shulker_rate', SQLITE3_TEXT);
|
||||
$upsert->bindValue(':v', number_format($sh, 2, '.', ''), SQLITE3_TEXT);
|
||||
$upsert->execute();
|
||||
$saved = [];
|
||||
|
||||
// 购物车折扣/加价系数(需成对提交)
|
||||
$bp = getParam('cart_backpack_rate');
|
||||
$sh = getParam('cart_shulker_rate');
|
||||
if ($bp !== null || $sh !== null) {
|
||||
if ($bp === null || $sh === null) error('塞背包与潜影盒系数需同时提交');
|
||||
$bp = (float)$bp;
|
||||
$sh = (float)$sh;
|
||||
if ($bp <= 0 || $bp > 1.0) error('塞背包折扣系数必须介于 0.01 ~ 1.00 之间');
|
||||
if ($sh < 1.0 || $sh > 3.0) error('潜影盒加价系数必须介于 1.00 ~ 3.00 之间');
|
||||
$upsert->bindValue(':k', 'cart_backpack_rate', SQLITE3_TEXT);
|
||||
$upsert->bindValue(':v', number_format($bp, 2, '.', ''), SQLITE3_TEXT);
|
||||
$upsert->execute();
|
||||
$upsert->bindValue(':k', 'cart_shulker_rate', SQLITE3_TEXT);
|
||||
$upsert->bindValue(':v', number_format($sh, 2, '.', ''), SQLITE3_TEXT);
|
||||
$upsert->execute();
|
||||
$saved['backpack_rate'] = $bp;
|
||||
$saved['shulker_rate'] = $sh;
|
||||
}
|
||||
|
||||
// 彩色潜影盒打包费
|
||||
$pm = getParam('packmoney');
|
||||
if ($pm !== null) {
|
||||
$pm = (int)$pm;
|
||||
if ($pm < 0 || $pm > 999) error('打包费需介于 0 ~ 999 之间');
|
||||
$upsert->bindValue(':k', 'packmoney', SQLITE3_TEXT);
|
||||
$upsert->bindValue(':v', (string)$pm, SQLITE3_TEXT);
|
||||
$upsert->execute();
|
||||
$saved['packmoney'] = $pm;
|
||||
}
|
||||
|
||||
// 环保单减免百分比(0=不减免;命令 setgreen 10 亦为不减免)
|
||||
$gd = getParam('green_discount');
|
||||
if ($gd !== null) {
|
||||
$gd = (float)$gd;
|
||||
if ($gd < 0 || $gd > 9.99) error('环保单减免需介于 0 ~ 9.99 之间');
|
||||
$upsert->bindValue(':k', 'green_discount', SQLITE3_TEXT);
|
||||
$upsert->bindValue(':v', number_format($gd, 2, '.', ''), SQLITE3_TEXT);
|
||||
$upsert->execute();
|
||||
$saved['green_discount'] = $gd;
|
||||
}
|
||||
|
||||
if (empty($saved)) error('没有可保存的配置项');
|
||||
|
||||
exit(json_encode([
|
||||
'success' => true,
|
||||
'message' => '购物车结算配置已保存',
|
||||
'data' => [
|
||||
'backpack_rate' => $bp,
|
||||
'shulker_rate' => $sh
|
||||
]
|
||||
'message' => '商店配置已保存',
|
||||
'data' => $saved
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
} catch (\Throwable $e) {
|
||||
@error_log('[save_shop_config] Error: ' . $e->getMessage());
|
||||
|
||||
+19
-4
@@ -73,7 +73,8 @@ function cashierDoLogin() {
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'username' => $c['username'],
|
||||
'discount_limit_percent' => $c['discount_limit_percent']
|
||||
'discount_limit_percent' => $c['discount_limit_percent'],
|
||||
'can_cash' => (int)($c['can_cash'] ?? 0)
|
||||
]
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
@@ -105,7 +106,8 @@ function cashierSession() {
|
||||
'data' => [
|
||||
'role' => 'cashier',
|
||||
'username' => $c['username'],
|
||||
'discount_limit_percent' => $c['discount_limit_percent']
|
||||
'discount_limit_percent' => $c['discount_limit_percent'],
|
||||
'can_cash' => (int)($c['can_cash'] ?? 0)
|
||||
]
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
@@ -181,7 +183,7 @@ function cashierOrderList() {
|
||||
function cashierList() {
|
||||
requireAdminSession();
|
||||
$db = getDB();
|
||||
$result = $db->prepare("SELECT id, username, discount_limit_percent, created_at, created_by FROM cashiers ORDER BY id ASC")->execute();
|
||||
$result = $db->prepare("SELECT id, username, discount_limit_percent, can_cash, created_at, created_by FROM cashiers ORDER BY id ASC")->execute();
|
||||
$rows = [];
|
||||
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
$rows[] = $r;
|
||||
@@ -195,6 +197,7 @@ function cashierAdd() {
|
||||
$username = trim(getParam('username', ''));
|
||||
$password = getParam('password', '');
|
||||
$discountLimit = (int)getParam('discount_limit_percent', 0);
|
||||
$canCash = (int)getParam('can_cash', 0);
|
||||
if ($username === '' || $password === '') {
|
||||
exit(json_encode(['success' => false, 'message' => '账号和密码不能为空'], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
@@ -204,6 +207,9 @@ function cashierAdd() {
|
||||
if ($discountLimit < 0 || $discountLimit > 100) {
|
||||
exit(json_encode(['success' => false, 'message' => '折扣上限需在0-100之间'], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
if ($canCash < 0 || $canCash > 1) {
|
||||
exit(json_encode(['success' => false, 'message' => '现金收款权限需为0或1'], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
$db = getDB();
|
||||
// 查重
|
||||
$chk = $db->prepare("SELECT id FROM cashiers WHERE username = :u");
|
||||
@@ -213,11 +219,12 @@ function cashierAdd() {
|
||||
}
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$salt = bin2hex(random_bytes(8));
|
||||
$stmt = $db->prepare("INSERT INTO cashiers (username, password_hash, salt, discount_limit_percent, created_at, created_by) VALUES (:u,:h,:s,:d,:t,'admin')");
|
||||
$stmt = $db->prepare("INSERT INTO cashiers (username, password_hash, salt, discount_limit_percent, can_cash, created_at, created_by) VALUES (:u,:h,:s,:d,:c,:t,'admin')");
|
||||
$stmt->bindValue(':u', $username, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':h', $hash, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':s', $salt, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':d', $discountLimit, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':c', $canCash, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':t', time(), SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
exit(json_encode(['success' => true, 'message' => '收银员创建成功'], JSON_UNESCAPED_UNICODE));
|
||||
@@ -251,6 +258,14 @@ function cashierEdit() {
|
||||
$sets[] = "discount_limit_percent = :d";
|
||||
$params[':d'] = $dl;
|
||||
}
|
||||
if (getParam('can_cash') !== null && getParam('can_cash') !== '') {
|
||||
$cc = (int)getParam('can_cash', 0);
|
||||
if ($cc < 0 || $cc > 1) {
|
||||
exit(json_encode(['success' => false, 'message' => '现金收款权限需为0或1'], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
$sets[] = "can_cash = :c";
|
||||
$params[':c'] = $cc;
|
||||
}
|
||||
if (empty($sets)) {
|
||||
exit(json_encode(['success' => false, 'message' => '没有可更新的字段'], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
+22
-8
@@ -268,7 +268,9 @@ function getShopConfig($key, $default = null) {
|
||||
function cartConfig() {
|
||||
success([
|
||||
'backpack_rate' => (float)getShopConfig('cart_backpack_rate', '0.98'),
|
||||
'shulker_rate' => (float)getShopConfig('cart_shulker_rate', '1.00')
|
||||
'shulker_rate' => (float)getShopConfig('cart_shulker_rate', '1.00'),
|
||||
'packmoney' => (int)getShopConfig('packmoney', '5'),
|
||||
'green_discount' => (float)getShopConfig('green_discount', '2')
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -276,7 +278,8 @@ function cartConfig() {
|
||||
function shopBuyCart($token) {
|
||||
$rawItems = getParam('items');
|
||||
$settlement = getParam('settlement', 'backpack'); // backpack=塞背包, shulker=潜影盒打包
|
||||
$shulkerColor = getParam('shulker_color', 'purple'); // 潜影盒颜色(purple免费,其它+2元)
|
||||
$shulkerColor = getParam('shulker_color', 'default'); // 潜影盒颜色(default原色免费,其它+2元)
|
||||
$payMode = getParam('pay_mode', 'bond'); // bond=债券扣款, cash=现金仅记账
|
||||
$player = getParam('player');
|
||||
$password = getParam('password');
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
@@ -346,10 +349,11 @@ function shopBuyCart($token) {
|
||||
if ($settlement === 'shulker') {
|
||||
$rate = (float)getShopConfig('cart_shulker_rate', '1.00');
|
||||
$modeName = '潜影盒打包';
|
||||
// 潜影盒颜色额外收费(紫色免费,其它+2元)
|
||||
$colorFee = ($shulkerColor !== 'purple') ? 2 : 0;
|
||||
// 潜影盒颜色额外收费(原色/紫色免费,其它加收打包费,打包费来自配置 packmoney)
|
||||
$packMoney = (int)getShopConfig('packmoney', '5');
|
||||
$colorFee = ($shulkerColor !== 'default' && $shulkerColor !== 'purple') ? $packMoney : 0;
|
||||
// 颜色名称映射
|
||||
$colorNames = ['purple'=>'紫色','white'=>'白色','black'=>'黑色','red'=>'红色','blue'=>'蓝色','green'=>'绿色','yellow'=>'黄色','orange'=>'橙色'];
|
||||
$colorNames = ['default'=>'原色','purple'=>'原色','white'=>'白色','black'=>'黑色','red'=>'红色','blue'=>'蓝色','green'=>'绿色','yellow'=>'黄色','orange'=>'橙色'];
|
||||
$colorName = $colorNames[$shulkerColor] ?? $shulkerColor;
|
||||
} else {
|
||||
$settlement = 'backpack';
|
||||
@@ -426,8 +430,16 @@ function shopBuyCart($token) {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查余额
|
||||
if (!$isPreview && $finalPrice > 0) {
|
||||
// ★ 现金收款权限校验:收银员需管理员授予 can_cash 权限
|
||||
if ($payMode === 'cash') {
|
||||
if ($isCashier && empty($cashierRow['can_cash'])) {
|
||||
error('该收银员无现金收款权限(需管理员在收银员管理中开启)', 403);
|
||||
}
|
||||
// 现金模式:仅记账,不扣玩家债券(跳过余额校验与扣款)
|
||||
}
|
||||
|
||||
// 检查余额(现金模式跳过:仅记账)
|
||||
if (!$isPreview && $finalPrice > 0 && $payMode !== 'cash') {
|
||||
$balanceStmt = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :player");
|
||||
$balanceStmt->bindValue(':player', $player, SQLITE3_TEXT);
|
||||
$balanceResult = $balanceStmt->execute();
|
||||
@@ -485,6 +497,7 @@ function shopBuyCart($token) {
|
||||
'total_price' => $finalPrice,
|
||||
'discount_percent' => (int)$discountPercent,
|
||||
'discount_amount' => (int)$discountAmount,
|
||||
'pay_mode' => $payMode,
|
||||
'items' => $lines
|
||||
];
|
||||
if ($settlement === 'shulker') {
|
||||
@@ -509,7 +522,8 @@ function shopBuyCart($token) {
|
||||
'total_price' => $finalPrice,
|
||||
'discount_percent' => (int)$discountPercent,
|
||||
'discount_amount' => (int)$discountAmount,
|
||||
'settlement' => $settlement
|
||||
'settlement' => $settlement,
|
||||
'payment_method' => $payMode
|
||||
], $db);
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,12 @@ switch ($action) {
|
||||
case 'pull_shop_prices':
|
||||
pullShopPrices();
|
||||
break;
|
||||
case 'get_shop_config':
|
||||
getShopConfigSync();
|
||||
break;
|
||||
case 'set_shop_config':
|
||||
setShopConfigSync();
|
||||
break;
|
||||
case 'clear_admin_prices':
|
||||
clearAdminPrices();
|
||||
break;
|
||||
@@ -2789,6 +2795,45 @@ function pullShopPrices() {
|
||||
success(['items' => $items, 'count' => count($items)]);
|
||||
}
|
||||
|
||||
// ===== 插件拉取商店打包配置(打包费 / 环保单减免)=====
|
||||
function getShopConfigSync() {
|
||||
$secret = getParam('secret');
|
||||
if (!$secret || $secret !== SECRET_KEY) error('认证失败');
|
||||
|
||||
success([
|
||||
'packmoney' => (int)getShopConfig('packmoney', '5'),
|
||||
'green_discount' => (float)getShopConfig('green_discount', '2')
|
||||
]);
|
||||
}
|
||||
|
||||
// ===== 插件推送商店配置(Java命令 /sdf1_login set packmoney、shop setgreen 写入)=====
|
||||
function setShopConfigSync() {
|
||||
$secret = getParam('secret');
|
||||
if (!$secret || $secret !== SECRET_KEY) error('认证失败');
|
||||
|
||||
$key = getParam('key');
|
||||
$value = getParam('value');
|
||||
$allowed = ['packmoney', 'green_discount'];
|
||||
if (!in_array($key, $allowed, true)) error('不支持的配置项');
|
||||
if ($value === null) error('缺少value');
|
||||
|
||||
if ($key === 'packmoney') {
|
||||
$v = (int)$value;
|
||||
if ($v < 0 || $v > 999) error('打包费需介于 0 ~ 999');
|
||||
} else {
|
||||
$v = (float)$value;
|
||||
if ($v < 0 || $v > 9.99) error('环保单减免需介于 0 ~ 9.99');
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS shop_config (cfg_key TEXT PRIMARY KEY, cfg_value TEXT NOT NULL)");
|
||||
$stmt = $db->prepare("INSERT OR REPLACE INTO shop_config (cfg_key, cfg_value) VALUES (:k, :v)");
|
||||
$stmt->bindValue(':k', $key, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':v', (string)$v, SQLITE3_TEXT);
|
||||
$stmt->execute();
|
||||
success([], '配置已保存: ' . $key . '=' . $v);
|
||||
}
|
||||
|
||||
// ===== 插件确认已应用管理员价格改动 =====
|
||||
function clearAdminPrices() {
|
||||
$secret = getParam('secret');
|
||||
|
||||
+76
-7
@@ -7,6 +7,21 @@ require_once __DIR__ . '/core.php';
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
|
||||
// 读取打包费配置(彩色潜影盒加收,须与后端 shopBuyCart 保持一致)
|
||||
function cashierGetPackMoney() {
|
||||
try {
|
||||
$db = getDB();
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS shop_config (cfg_key TEXT PRIMARY KEY, cfg_value TEXT NOT NULL)");
|
||||
$stmt = $db->prepare("SELECT cfg_value FROM shop_config WHERE cfg_key = 'packmoney'");
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(SQLITE3_ASSOC);
|
||||
return $row ? (int)$row['cfg_value'] : 5;
|
||||
} catch (Exception $e) {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
$packMoney = cashierGetPackMoney();
|
||||
|
||||
$loggedIn = isAdminLoggedIn() || isCashierLoggedIn();
|
||||
$initialRole = 'guest';
|
||||
$initialName = '';
|
||||
@@ -15,17 +30,21 @@ if (isAdminLoggedIn()) {
|
||||
$initialRole = 'admin';
|
||||
$initialName = 'admin';
|
||||
$initialLimit = 100;
|
||||
$initialCanCash = 1; // 管理员始终可进行现金收款
|
||||
} elseif (isCashierLoggedIn()) {
|
||||
$c = getCurrentCashier();
|
||||
$initialRole = 'cashier';
|
||||
$initialName = $c['username'];
|
||||
$initialLimit = $c['discount_limit_percent'];
|
||||
$initialCanCash = (int)($c['can_cash'] ?? 0);
|
||||
}
|
||||
$initialState = json_encode([
|
||||
'logged_in' => $loggedIn,
|
||||
'role' => $initialRole,
|
||||
'username' => $initialName,
|
||||
'discount_limit' => $initialLimit
|
||||
'discount_limit' => $initialLimit,
|
||||
'cashier_can_cash' => $initialCanCash,
|
||||
'packmoney' => $packMoney
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
@@ -222,12 +241,21 @@ $initialState = json_encode([
|
||||
<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:6px">结算方式</label>
|
||||
<div class="settlement-row">
|
||||
<div class="seg active" data-set="backpack" onclick="setSettlement('backpack')">🎒 塞背包 (98折)</div>
|
||||
<div class="seg" data-set="shulker" onclick="setSettlement('shulker')">📦 潜影盒 (+2元)</div>
|
||||
<div class="seg" data-set="shulker" onclick="setSettlement('shulker')">📦 潜影盒(原色免费)</div>
|
||||
</div>
|
||||
|
||||
<!-- ★ 收款模式切换(管理员可用 / 收银员看权限) -->
|
||||
<div id="payModeWrap" style="margin-top:10px">
|
||||
<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:6px">收款模式</label>
|
||||
<div class="settlement-row" id="payModeRow">
|
||||
<div class="seg active" data-pm="bond" onclick="setPayMode('bond')">💰 债券收款(扣余额)</div>
|
||||
<div class="seg" data-pm="cash" onclick="setPayMode('cash')" id="cashModeBtn">💵 现金收款(仅记账)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="shulkerColors" class="hidden" style="margin-bottom:10px">
|
||||
<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:6px">潜影盒颜色(紫色免费,其它+2元)</label>
|
||||
<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:6px">潜影盒颜色(原色免费,其它颜色+2元)</label>
|
||||
<select id="shulkerColor" onchange="recalc()">
|
||||
<option value="purple">紫色(免费)</option>
|
||||
<option value="default">原色(免费)</option>
|
||||
<option value="white">白色</option>
|
||||
<option value="black">黑色</option>
|
||||
<option value="red">红色</option>
|
||||
@@ -276,10 +304,16 @@ $initialState = json_encode([
|
||||
|
||||
<script>
|
||||
const STATE = <?php echo $initialState; ?>;
|
||||
// 动态更新打包费提示(与后端 packmoney 配置一致,避免硬编码 +2 元)
|
||||
(function() {
|
||||
const lbl = document.querySelector('#shulkerColors label');
|
||||
if (lbl) lbl.textContent = '潜影盒颜色(原色免费,其它颜色+ ' + (STATE.packmoney || 2) + ' 元打包费)';
|
||||
})();
|
||||
let products = [];
|
||||
let categories = [];
|
||||
let cart = {}; // id -> qty
|
||||
let settlement = 'backpack';
|
||||
let payMode = 'bond'; // bond=债券收款(扣余额), cash=现金收款(仅记账)
|
||||
let currentPlayer = null;
|
||||
let playerBalance = 0;
|
||||
|
||||
@@ -365,6 +399,22 @@ async function initApp() {
|
||||
// 管理员需密码确认大额
|
||||
if (STATE.role === 'admin') document.getElementById('adminPwWrap').classList.remove('hidden');
|
||||
|
||||
// ★ 收款模式权限控制
|
||||
const cashModeBtn = document.getElementById('cashModeBtn');
|
||||
if (STATE.role === 'admin') {
|
||||
// 管理员始终可用现金模式
|
||||
cashModeBtn.style.display = '';
|
||||
document.getElementById('payModeWrap').style.display = '';
|
||||
} else if (STATE.cashier_can_cash) {
|
||||
// 收银员:管理员授权了现金收款权限
|
||||
cashModeBtn.style.display = '';
|
||||
document.getElementById('payModeWrap').style.display = '';
|
||||
} else {
|
||||
// 收银员无现金权限 → 隐藏现金按钮,锁定债券模式
|
||||
cashModeBtn.style.display = 'none';
|
||||
// 不隐藏整个 payModeWrap,但只显示债券模式
|
||||
}
|
||||
|
||||
await Promise.all([loadProducts(), loadOrders()]);
|
||||
}
|
||||
|
||||
@@ -448,6 +498,18 @@ function setSettlement(s) {
|
||||
recalc();
|
||||
}
|
||||
|
||||
// ★ 收款模式切换
|
||||
function setPayMode(pm) {
|
||||
payMode = pm;
|
||||
document.querySelectorAll('#payModeRow .seg').forEach(x => x.classList.toggle('active', x.dataset.pm === pm));
|
||||
|
||||
// 现金模式下提示"仅记账不扣债券"
|
||||
if (pm === 'cash') {
|
||||
toast('现金收款模式:仅记账,不扣除玩家债券', 'ok');
|
||||
}
|
||||
recalc();
|
||||
}
|
||||
|
||||
// ===== 计算 + 预览 =====
|
||||
const RATES = { backpack: 0.98, shulker: 1.00 };
|
||||
async function recalc() {
|
||||
@@ -466,7 +528,8 @@ async function recalc() {
|
||||
const rate = RATES[settlement];
|
||||
let total = Math.round(subtotal * rate);
|
||||
let colorFee = 0;
|
||||
if (settlement === 'shulker' && document.getElementById('shulkerColor').value !== 'purple') colorFee = 2;
|
||||
const sc = document.getElementById('shulkerColor').value;
|
||||
if (settlement === 'shulker' && sc !== 'default' && sc !== 'purple') colorFee = STATE.packmoney || 2;
|
||||
total += colorFee;
|
||||
|
||||
let discount = parseFloat(document.getElementById('discount').value) || 0;
|
||||
@@ -481,7 +544,8 @@ async function recalc() {
|
||||
<div class="summary-line"><span>${settlement==='shulker'?'潜影盒费率':'背包费率'} (${rate*100}%)</span><span>${colorFee?('+'+colorFee):''}</span></div>
|
||||
<div class="summary-line"><span>应收原价</span><span>${total} 债券</span></div>
|
||||
<div class="summary-line"><span class="disc">手动折扣 ${discount}%</span><span class="disc">-${discAmt} 债券</span></div>
|
||||
<div class="summary-line total"><span>实收</span><span class="v">${final} 债券</span></div>`;
|
||||
${payMode === 'cash' ? '<div class="summary-line" style="border-color:var(--orange);color:var(--orange)"><span>💵 现金收款模式</span><span>仅记账,不扣债券</span></div>' : ''}
|
||||
<div class="summary-line total"><span>实收${payMode==='cash'?'(记账)':''}</span><span class="v">${final} ${payMode==='cash'?'债券':''}</span></div>`;
|
||||
payBtn.disabled = false;
|
||||
}
|
||||
|
||||
@@ -515,6 +579,7 @@ async function confirmPay() {
|
||||
const body = new URLSearchParams();
|
||||
body.set('items', JSON.stringify(items));
|
||||
body.set('settlement', settlement);
|
||||
body.set('pay_mode', payMode); // ★ 新增:收款模式(bond/cash)
|
||||
body.set('shulker_color', document.getElementById('shulkerColor').value);
|
||||
body.set('player', currentPlayer);
|
||||
body.set('discount_percent', String(discount));
|
||||
@@ -540,7 +605,11 @@ async function confirmPay() {
|
||||
function finishPay(d) {
|
||||
const btn = document.getElementById('payBtn');
|
||||
if (d.success) {
|
||||
toast('收款成功,实收 ' + (d.data.total_price) + ' 债券', 'ok');
|
||||
if (payMode === 'cash') {
|
||||
toast('✅ 现金收款记账成功(订单 #' + (d.data.order_id || '') + ',未扣玩家债券)', 'ok');
|
||||
} else {
|
||||
toast('收款成功,实收 ' + (d.data.total_price) + ' 债券', 'ok');
|
||||
}
|
||||
cart = {}; renderCart();
|
||||
document.getElementById('summary').innerHTML = '';
|
||||
document.getElementById('discount').value = 0;
|
||||
|
||||
@@ -609,6 +609,8 @@ function initTables(SQLite3 $db) {
|
||||
created_at INTEGER DEFAULT 0,
|
||||
created_by TEXT DEFAULT ''
|
||||
)");
|
||||
// 收银员现金收款权限字段迁移(已存在则静默失败)
|
||||
@$db->exec("ALTER TABLE cashiers ADD COLUMN can_cash INTEGER DEFAULT 0");
|
||||
|
||||
// ===== 收银台订单表(代购/收银员操作记录)=====
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS cashier_orders (
|
||||
@@ -623,9 +625,12 @@ function initTables(SQLite3 $db) {
|
||||
discount_percent INTEGER DEFAULT 0,
|
||||
discount_amount INTEGER DEFAULT 0,
|
||||
settlement TEXT DEFAULT '',
|
||||
payment_method TEXT DEFAULT 'bond',
|
||||
status TEXT DEFAULT 'completed',
|
||||
created_at INTEGER DEFAULT 0
|
||||
)");
|
||||
// 收款模式字段迁移(现金/债券)
|
||||
@$db->exec("ALTER TABLE cashier_orders ADD COLUMN payment_method TEXT DEFAULT 'bond'");
|
||||
|
||||
$db->exec('COMMIT');
|
||||
} catch (Exception $e) {
|
||||
@@ -939,6 +944,7 @@ function cashierLogin($username, $password) {
|
||||
$_SESSION['cashier_id'] = (int)$row['id'];
|
||||
$_SESSION['cashier_name'] = $row['username'];
|
||||
$_SESSION['cashier_discount_limit'] = (int)$row['discount_limit_percent'];
|
||||
$_SESSION['cashier_can_cash'] = (int)($row['can_cash'] ?? 0);
|
||||
$_SESSION['cashier_login_time'] = time();
|
||||
return true;
|
||||
}
|
||||
@@ -955,7 +961,8 @@ function getCurrentCashier() {
|
||||
return [
|
||||
'id' => $_SESSION['cashier_id'] ?? 0,
|
||||
'username' => $_SESSION['cashier_name'] ?? '',
|
||||
'discount_limit_percent' => (int)($_SESSION['cashier_discount_limit'] ?? 0)
|
||||
'discount_limit_percent' => (int)($_SESSION['cashier_discount_limit'] ?? 0),
|
||||
'can_cash' => (int)($_SESSION['cashier_can_cash'] ?? 0)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -975,7 +982,7 @@ function cashierLogout() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
unset($_SESSION['cashier_auth'], $_SESSION['cashier_id'], $_SESSION['cashier_name'], $_SESSION['cashier_discount_limit'], $_SESSION['cashier_login_time']);
|
||||
unset($_SESSION['cashier_auth'], $_SESSION['cashier_id'], $_SESSION['cashier_name'], $_SESSION['cashier_discount_limit'], $_SESSION['cashier_can_cash'], $_SESSION['cashier_login_time']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -986,7 +993,7 @@ function cashierLogout() {
|
||||
function recordCashierOrder($data, $db = null) {
|
||||
if ($db === null) $db = getDB();
|
||||
$orderNo = 'C' . date('YmdHis') . str_pad(mt_rand(0, 999), 3, '0', STR_PAD_LEFT);
|
||||
$stmt = $db->prepare("INSERT INTO cashier_orders (order_no, operator_type, operator_name, player_name, items_detail, subtotal, total_price, discount_percent, discount_amount, settlement, status, created_at) VALUES (:no,:ot,:on,:pn,:det,:sub,:tp,:dp,:da,:st,'completed',:time)");
|
||||
$stmt = $db->prepare("INSERT INTO cashier_orders (order_no, operator_type, operator_name, player_name, items_detail, subtotal, total_price, discount_percent, discount_amount, settlement, payment_method, status, created_at) VALUES (:no,:ot,:on,:pn,:det,:sub,:tp,:dp,:da,:st,:pm,'completed',:time)");
|
||||
$stmt->bindValue(':no', $orderNo, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':ot', $data['operator_type'] ?? 'cashier', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':on', $data['operator_name'] ?? '', SQLITE3_TEXT);
|
||||
@@ -997,6 +1004,7 @@ function recordCashierOrder($data, $db = null) {
|
||||
$stmt->bindValue(':dp', (int)($data['discount_percent'] ?? 0), SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':da', (int)($data['discount_amount'] ?? 0), SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':st', $data['settlement'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':pm', $data['payment_method'] ?? 'bond', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':time', time(), SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
return $orderNo;
|
||||
|
||||
Reference in New Issue
Block a user