CREATE TABLE AWARD_INFO (
ID NUMBER(11) NOT NULL PRIMARY KEY,
ACT_ID NUMBER(11), -- 活動 ID
NUM NUMBER(11), -- 獎品總量(0 表示不限量)
REST NUMBER(11), -- 剩餘量
ODDS NUMBER(11) DEFAULT 0, -- 中獎權重/概率
START_DATE DATE, -- 開始時間
END_DATE DATE, -- 結束時間
PRODUCT_ID NUMBER(11), -- 獎品 ID
STATE NUMBER(5) DEFAULT 0, -- 狀態:0 有效 1 失效
INFO_TYPE NUMBER(5) DEFAULT 0 -- 類型:0 普通
);
CREATE TABLE AWARD_LOG (
ID NUMBER(11) PRIMARY KEY,
ACT_ID NUMBER(11),
GET_TIME DATE,
PRODUCT_ID NUMBER(11),
NUM NUMBER(11) DEFAULT 1,
PERSON VARCHAR2(50),
INFO_ID NUMBER(11),
STATE NUMBER(5)
);
public static class AwardResult {
public int ret; // 返回碼
public int logId; // 記錄 ID
}
/**
* 統一抽獎/搶購入口
* @param actId 活動 ID
* @param person 參與者
* @param productId 指定獎品 ID;-1 表示活動下所有獎品
* @param excludeIds 排除的獎品 ID 陣列
* @param checkDate 是否校驗時間窗口
* @return AwardResult
*/
public static AwardResult getAwardFull(int actId, String person, int productId,
int[] excludeIds, boolean checkDate) throws SQLException {
AwardResult result = new AwardResult();
Connection conn = JDBC.getConnection();
conn.setAutoCommit(false);
try {
// 1. 查詢可用獎品(加鎖)
String baseSql = "SELECT t.id, t.product_id, t.num, t.rest, t.odds, t.info_type " +
"FROM award_info t WHERE t.act_id = ? AND t.state = 0 ";
String checkDateSql = checkDate ? " AND t.start_date <= SYSDATE AND t.end_date >= SYSDATE " : "";
String sql;
Object[] params;
if (productId > 0) { // 搶購模式
sql = baseSql + " AND t.product_id = ? " + checkDateSql + " FOR UPDATE";
params = new Object[]{actId, productId};
} else { // 抽獎模式
sql = baseSql + checkDateSql + " FOR UPDATE";
params = new Object[]{actId};
}
List<Map<String, Object>> rows = JDBC.getRows(sql, params, conn);
if (rows.isEmpty()) {
result.ret = -1; // 無獎品資料
conn.commit();
return result;
}
// 2. 確定中獎獎品
int infoId = -1, getProductId = -1, num = -1, rest = -1;
if (rows.size() == 1) { // 單獎品:搶購
Map<String, Object> row = rows.get(0);
infoId = ((Number) row.get("ID")).intValue();
getProductId = ((Number) row.get("PRODUCT_ID")).intValue();
num = ((Number) row.get("NUM")).intValue();
rest = ((Number) row.get("REST")).intValue();
} else { // 多獎品:按權重抽獎
int[][] range = new int[rows.size()][3];
int sum = 0, idx = 0;
for (int k = 0; k < rows.size(); k++) {
int odds = ((BigDecimal) rows.get(k).get("ODDS")).intValue();
range[idx][0] = sum; // 區間起始
sum += odds;
range[idx][1] = sum; // 區間結束
range[idx][2] = k; // 對應 rows 索引
idx++;
}
int r = new Random().nextInt(sum + 1);
int hitIdx = 0;
for (int k = 0; k < idx; k++) {
if (r >= range[k][0] && r <= range[k][1]) {
hitIdx = k;
break;
}
}
Map<String, Object> hitRow = rows.get(range[hitIdx][2]);
infoId = ((BigDecimal) hitRow.get("ID")).intValue();
getProductId = ((BigDecimal) hitRow.get("PRODUCT_ID")).intValue();
num = ((Number) hitRow.get("NUM")).intValue();
rest = ((Number) hitRow.get("REST")).intValue();
}
// 3. 排除檢查
if (ArrayUtils.contains(excludeIds, getProductId)) {
result.ret = -4;
conn.commit();
return result;
}
// 4. 庫存檢查
if (num > 0 && rest <= 0) {
result.ret = -2; // 售罄
conn.commit();
return result;
}
// 5. 扣減庫存
if (num > 0) {
JDBC.update("UPDATE award_info SET rest = rest - 1 WHERE id = ?", new Object[]{infoId}, conn);
}
// 6. 記錄中獎
AwardLog log = new AwardLog();
log.setActId(actId);
log.setNum(1);
log.setPerson(person);
log.setProductId(getProductId);
log.setInfoId(infoId);
Number logId = log.save(conn);
if (logId == null) throw new SQLException("save award_log error");
result.logId = logId.intValue();
conn.commit();
result.ret = getProductId;
return result;
} catch (SQLException e) {
conn.rollback();
result.ret = -3;
return result;
} finally {
JDBC.close(conn);
}
}
FOR UPDATE 確保高併發下庫存扣減原子性。本文提供了一個生產級的抽獎/搶購核心邏輯,涵蓋資料庫設計、併發控制、概率算法與事務管理,可直接應用於營銷活動系統。