pharmacy-pos-qr-system/backend/utils/qr_token_generator.py
thug0bin db5f6063ec fix: SQLite 싱글톤 연결 I/O 에러 수정 + clawdbot 모델 오버라이드
- dbsetup: get_sqlite_connection()에 SELECT 1 헬스체크 추가 (죽은 연결 자동 재생성)
- pos_sales_gui: 싱글톤 SQLite conn.close() 제거 (I/O closed file 에러 원인)
- qr_token_generator: DatabaseManager() 새 생성 → 전역 db_manager 싱글톤 사용
- clawdbot_client: model 파라미터 추가, 업셀링에 claude-sonnet-4-5 지정

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 01:27:47 +09:00

192 lines
5.6 KiB
Python

"""
QR Claim Token 생성 모듈
후향적 적립을 위한 1회성 토큰 생성
"""
import hashlib
import secrets
from datetime import datetime, timedelta
import sys
import os
# DB 연결
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from db.dbsetup import DatabaseManager
# 설정값
MILEAGE_RATE = 0.03 # 3% 적립
TOKEN_EXPIRY_DAYS = 30 # 30일 유효기간
QR_BASE_URL = "https://mile.0bin.in/claim"
def generate_claim_token(transaction_id, total_amount, pharmacy_id="YANGGU001"):
"""
Claim Token 생성 (SHA256 해시 기반)
Args:
transaction_id (str): POS 거래 ID (SALE_MAIN.SL_NO_order)
total_amount (float): 총 판매 금액
pharmacy_id (str): 약국 코드
Returns:
dict: {
'qr_url': QR 코드 URL,
'token_raw': 토큰 원문 (QR에만 포함),
'token_hash': SHA256 해시 (DB 저장용),
'claimable_points': 적립 가능 포인트,
'expires_at': 만료일시
}
보안 정책:
- token_raw는 DB에 저장하지 않음 (QR 코드에만 포함)
- token_hash만 DB 저장 (SHA256, 64자)
- nonce 사용으로 동일 거래도 매번 다른 토큰
URL 최적화:
- nonce: 64자 -> 12자로 단축 (6바이트 = 충분한 보안)
- 타임스탬프: 제거 (DB에만 저장)
- 결과: 약 80자 -> 빠른 QR 인식
"""
# 1. 랜덤 nonce 생성 (6바이트 = 12자 hex) - 대폭 단축!
nonce = secrets.token_hex(6)
# 2. 타임스탬프 (DB 저장용)
timestamp = datetime.now().isoformat()
# 3. 토큰 원문 생성 (검증용 - DB 저장 X)
token_raw = f"{transaction_id}:{nonce}:{timestamp}"
# 4. SHA256 해시 생성 (DB 저장용)
token_hash = hashlib.sha256(token_raw.encode('utf-8')).hexdigest()
# 5. QR URL 생성 (짧게!) - 타임스탬프 제외
qr_token_short = f"{transaction_id}:{nonce}"
qr_url = f"{QR_BASE_URL}?t={qr_token_short}"
# 6. 적립 포인트 계산
claimable_points = calculate_claimable_points(total_amount)
# 7. 만료일 계산 (30일 후)
expires_at = datetime.now() + timedelta(days=TOKEN_EXPIRY_DAYS)
return {
'qr_url': qr_url,
'token_raw': token_raw, # 전체 토큰 (해시 생성용)
'token_hash': token_hash,
'claimable_points': claimable_points,
'expires_at': expires_at,
'pharmacy_id': pharmacy_id,
'transaction_id': transaction_id,
'total_amount': total_amount
}
def calculate_claimable_points(total_amount):
"""
적립 포인트 계산 (3% 정책)
Args:
total_amount (float): 판매 금액
Returns:
int: 적립 포인트 (정수, 소수점 절사)
"""
return int(total_amount * MILEAGE_RATE)
def save_token_to_db(transaction_id, token_hash, total_amount, claimable_points,
expires_at, pharmacy_id):
"""
생성된 토큰을 SQLite DB에 저장
Args:
transaction_id (str): 거래 ID
token_hash (str): SHA256 해시
total_amount (float): 판매 금액
claimable_points (int): 적립 포인트
expires_at (datetime): 만료일시
pharmacy_id (str): 약국 코드
Returns:
tuple: (성공 여부, 에러 메시지 or None)
중복 방지:
- transaction_id가 이미 존재하면 실패
- token_hash가 이미 존재하면 실패 (UNIQUE 제약)
"""
try:
from db.dbsetup import db_manager as _db_manager
conn = _db_manager.get_sqlite_connection()
cursor = conn.cursor()
# 중복 체크 (transaction_id)
cursor.execute("""
SELECT id FROM claim_tokens WHERE transaction_id = ?
""", (transaction_id,))
if cursor.fetchone():
return (False, f"이미 QR이 생성된 거래입니다: {transaction_id}")
# INSERT
cursor.execute("""
INSERT INTO claim_tokens (
transaction_id, pharmacy_id, token_hash,
total_amount, claimable_points, expires_at
) VALUES (?, ?, ?, ?, ?, ?)
""", (
transaction_id,
pharmacy_id,
token_hash,
int(total_amount), # INTEGER 타입
claimable_points,
expires_at.strftime('%Y-%m-%d %H:%M:%S')
))
conn.commit()
return (True, None)
except Exception as e:
return (False, f"DB 저장 실패: {str(e)}")
# 테스트 코드
if __name__ == "__main__":
# 테스트
test_tx_id = "20251024000042"
test_amount = 50000.0
print("=" * 80)
print("Claim Token 생성 테스트")
print("=" * 80)
token_info = generate_claim_token(test_tx_id, test_amount)
print(f"거래 ID: {test_tx_id}")
print(f"판매 금액: {test_amount:,}")
print(f"적립 포인트: {token_info['claimable_points']}P")
print(f"토큰 원문: {token_info['token_raw'][:80]}...")
print(f"토큰 해시: {token_info['token_hash']}")
print(f"QR URL: {token_info['qr_url'][:80]}...")
print(f"만료일: {token_info['expires_at']}")
print("=" * 80)
# DB 저장 테스트
print("\nDB 저장 테스트...")
success, error = save_token_to_db(
test_tx_id,
token_info['token_hash'],
test_amount,
token_info['claimable_points'],
token_info['expires_at'],
token_info['pharmacy_id']
)
if success:
print("[OK] DB 저장 성공")
print(f"\nSQLite DB 경로: backend/db/mileage.db")
print("다음 명령으로 확인:")
print(" sqlite3 backend/db/mileage.db \"SELECT * FROM claim_tokens\"")
else:
print(f"[ERROR] {error}")