"""HissPay signing/verification example. Default action is READ-ONLY balance.""" import os,json,time,uuid,hashlib,hmac,re,urllib.request,urllib.error GATEWAY=os.environ.get('HISSPAY_GATEWAY','') # 网关地址见商户后台「应用密钥」页面 if not GATEWAY:raise SystemExit('Set HISSPAY_GATEWAY (see merchant portal > App Credentials: appId)') PATHS={'balance':'/transaction/payout/v1/balance','payin_query':'/transaction/payin/v1/query','payout_query':'/transaction/payout/v1/query','payin':'/transaction/payin/v1/apply','payout':'/transaction/payout/v1/apply'} def canonical(body,headers): if set(body)&{'appId','timestamp','nonce','sign'}:raise ValueError('Reserved signature fields must be headers only') values={k:v for k,v in body.items() if v is not None} values.update({k:headers[k] for k in ('appId','timestamp','nonce')}) if any(isinstance(v,(dict,list,bool)) for v in values.values()):raise ValueError('Flat scalar fields required; send amounts as decimal strings') return '&'.join(k+'='+str(values[k]) for k in sorted(values)) def sign(body,app_id,app_secret,timestamp=None,nonce=None): headers={'appId':app_id,'timestamp':str(timestamp if timestamp is not None else int(time.time()*1000)),'nonce':nonce or uuid.uuid4().hex} headers['sign']=hashlib.sha256((canonical(body,headers)+app_secret).encode('utf-8')).hexdigest() return headers def verify_callback(body,headers,app_id,app_secret,now_ms=None,max_age_ms=300000): try: low={k.lower():v for k,v in headers.items()} h={'appId':low['appid'],'timestamp':low['timestamp'],'nonce':low['nonce']} if h['appId']!=app_id or len(h['nonce'])<10:return False now=int(time.time()*1000) if now_ms is None else now_ms if abs(now-int(h['timestamp']))>max_age_ms:return False actual=low['sign'] if not re.fullmatch('[0-9a-f]{64}',actual):return False expected=hashlib.sha256((canonical(body,h)+app_secret).encode('utf-8')).hexdigest() return hmac.compare_digest(expected,actual) except (KeyError,ValueError,TypeError):return False class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self,*args,**kwargs):return None def request(action,body,app_id,app_secret): if action not in PATHS:raise ValueError('Unknown action') if any(v is None for v in body.values()):raise ValueError('Omit optional null fields before signing') headers={**sign(body,app_id,app_secret),'Content-Type':'application/json'} req=urllib.request.Request(GATEWAY+PATHS[action],data=json.dumps(body,separators=(',',':'),ensure_ascii=False).encode(),headers=headers) # No automatic retry/redirect. A timeout does NOT mean a payment failed. try: with urllib.request.build_opener(NoRedirect).open(req,timeout=30) as response:return response.status,json.load(response) except urllib.error.HTTPError as error: try:body=json.load(error) except (ValueError,UnicodeError):body={'error':'Non-JSON gateway response'} return error.code,body def credentials(env=None): """(appId, appSecret) from HISSPAY_APP_ID / HISSPAY_APP_SECRET. The old PAYPLAT_* names still work but are deprecated.""" env=os.environ if env is None else env app_id,secret=env.get('HISSPAY_APP_ID'),env.get('HISSPAY_APP_SECRET') if app_id and secret:return app_id,secret old_id,old_secret=env.get('PAYPLAT_APP_ID'),env.get('PAYPLAT_APP_SECRET') if not app_id and not secret and old_id and old_secret: import sys print('WARNING: PAYPLAT_APP_ID/PAYPLAT_APP_SECRET are deprecated; rename them to HISSPAY_APP_ID/HISSPAY_APP_SECRET.',file=sys.stderr) return old_id,old_secret raise SystemExit('Set HISSPAY_APP_ID and HISSPAY_APP_SECRET in a private server environment.') if __name__=='__main__': app_id,secret=credentials() status,response=request('balance',{'currency':'INR'},app_id,secret) print(json.dumps({'http':status,'response':response},ensure_ascii=False)) # Callback handler: verify_callback(...) -> validate order/amount -> DB transaction -> plain SUCCESS. # This file intentionally contains no automatic payment submission in its main entry point.