diff --git a/src/main/java/Sdf1_login/SnManager.java b/src/main/java/Sdf1_login/SnManager.java
index 5f79202..61cc110 100644
--- a/src/main/java/Sdf1_login/SnManager.java
+++ b/src/main/java/Sdf1_login/SnManager.java
@@ -1847,6 +1847,8 @@ public class SnManager implements Listener {
result = "已永久绑定";
} else if ("report_check".equalsIgnoreCase(cmd)) {
result = doReportCheck(sn, player, force);
+ } else if ("issue".equalsIgnoreCase(cmd)) {
+ result = doIssue(sn, player, itemType, reason, force);
} else {
result = "未知命令: " + cmd;
}
@@ -2030,6 +2032,63 @@ public class SnManager implements Listener {
return "补发成功,新SN=" + newSn;
}
+ /**
+ * 办理签发:管理员在后台为玩家签发新设备(新 SN)。
+ *
+ *
前置校验在 PHP 端完成(旧 SN 已注销解绑 + 注销冷静期已结束,
+ * 未达标分别提示是哪一条),Java 侧做兜底复核,防止绕过接口直接入队。
+ *
+ *
与「强制补发」(doReissue) 的区别:issue 不作废旧 SN
+ * (旧 SN 本就已注销/销毁/补发了结),直接按类型登记新 SN 并发放;
+ * 因此也不需要 removeSnFromWorld 回收旧实物。
+ *
+ * @param force 管理员强制操作(PHP 侧 issue 固定传 0):仅跳过冷静期兜底校验
+ */
+ private String doIssue(String sn, String player, String itemType,
+ String reason, boolean force) {
+ Map row = getSn(sn);
+ if (row == null) return "SN不存在";
+ String owner = (player != null && !player.isEmpty())
+ ? player : str(row.get("owner"));
+ if (itemType == null || itemType.isEmpty())
+ itemType = str(row.get("item_type"));
+
+ // ★ 兜底复核①:旧 SN 必须已了结(PHP 已校验,此处防绕过)
+ String st = str(row.get("status"));
+ if (!ST_CANCELLED.equals(st) && !ST_DESTROYED.equals(st)
+ && !ST_REISSUED.equals(st)) {
+ return "旧SN尚未注销解绑,不能办理签发";
+ }
+ // ★ 兜底复核②:本地冷静期未结束不办理(权威值在本服 sn_cooldown)
+ if (!force) {
+ long cd = getCooldownUntil(owner);
+ long now = System.currentTimeMillis();
+ if (cd > now) {
+ long mins = (cd - now + 59999) / 60000;
+ return "注销冷静期未结束,还剩约 " + mins + " 分钟";
+ }
+ }
+
+ Player online = Bukkit.getPlayerExact(owner);
+ if (online == null) return "玩家不在线,待其上线后再办理";
+
+ // 登记新 SN(同种类仅 1 个 仍然生效;旧 SN 已了结不会占名额)
+ String newSn = force
+ ? applySnForce(online, itemType, typeName(itemType))
+ : applySn(online, itemType, typeName(itemType));
+ if (newSn == null) return "办理申领被拒(见玩家提示)";
+
+ ItemStack fresh = buildSnItem(online, itemType, newSn);
+ if (fresh == null) {
+ // 未知类型:回滚刚登记的新 SN,别白占"同种类仅1个"名额
+ destroySn(newSn, "办理签发物品构建失败回滚");
+ return "办理登记成功但物品构建失败";
+ }
+ giveTo(online, fresh);
+ logSn(newSn, itemType, "issue_admin", owner, reason);
+ return "办理成功,新SN=" + newSn;
+ }
+
/**
* 报失核查:按统一三条件门槛给结论。
* force=1(管理员手动核查,回执只进 web_sn_commands,管理端可见)时