# Source: https://github.com/3ixi/CodeScripts/blob/main/lsym.py
# Raw: https://raw.githubusercontent.com/3ixi/CodeScripts/main/lsym.py
# Repo: 3ixi/CodeScripts
# Path: lsym.py
# UploadedAt: 2025-09-23T14:07:43+08:00
# SHA256: 055c5aab9a013320b4421b4b0c1d4ea2893639fbc2eee553a66dfb67bcd7d1e6
# Category: 微信小程序/抓包版
# Evidence: appid/openid/session_key/encryptedData等小程序字段

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
零食有鸣小程序签到脚本
脚本作者：3iXi
创建时间：2025/09/04
！！此脚本需要配合iPad协议服务使用！！
小程序：零食有鸣
"""

import time
import json
import re
from typing import Optional, Dict, List

try:
    import httpx
except ImportError:
    print("错误: 需要安装 httpx[http2] 依赖")
    exit(1)

import getCode

# 可以在这里指定门店ID，如果为空则自动获取注册时的门店ID
STORE_ID = ""


class LsymSignin:
    """零食有鸣小程序签到类"""

    def __init__(self):
        """初始化签到客户端"""
        self.base_url = "https://api-pub.lsym.cn"
        self.app_id = "wx295a967684523cdd"
        
        self.client = httpx.Client(http2=True, timeout=30.0, verify=False)
        
        self.headers = {
            "host": "api-pub.lsym.cn",
            "appid": "wx295a967684523cdd",
            "authorization": "",
            "tenantid": "1001",
            "xweb_xhr": "1",
            "devicelanguage": "zh-cn",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf254101e) XWEB/16389",
            "accept": "*/*",
            "content-type": "application/json",
            "version": "3.1.0",
            "devicemodel": "android",
            "sec-fetch-site": "cross-site",
            "sec-fetch-mode": "cors",
            "sec-fetch-dest": "empty",
            "referer": "https://servicewechat.com/wx295a967684523cdd/73/page-frame.html",
            "accept-encoding": "gzip, deflate, br",
            "accept-language": "zh-CN,zh;q=0.9",
            "priority": "u=1, i"
        }
        
        self.access_token = None
        self.member_id = None
        self.lottery_activity_id = None
        self.store_id = STORE_ID

    def check_response(self, data: dict, operation: str) -> bool:
        """
        检查响应是否成功
        
        Args:
            data (dict): 响应数据
            operation (str): 操作名称
            
        Returns:
            bool: 是否成功
        """
        if data.get("code") != 200:
            print(f"{operation}失败：{data.get('msg', '未知错误')}")
            return False
        return True

    def login(self, code: str) -> bool:
        """
        用户登录
        
        Args:
            code (str): 微信小程序登录code
            
        Returns:
            bool: 登录是否成功
        """
        url = f"{self.base_url}/scrm/member-identity/front/miniapp/silentLogin"
        payload = {"loginType":0,"code":code}
        
        try:
            json_payload = json.dumps(payload, separators=(',', ':'))

            headers = self.headers.copy()
            headers["Content-Length"] = str(len(json_payload))
    
            response = self.client.post(url, content=json_payload, headers=headers)
            response.raise_for_status()
            
            data = response.json()
            if not self.check_response(data, "登录"):
                return False
                
            login_data = data.get("data", {})
            self.access_token = login_data.get("accessToken")
            self.member_id = login_data.get("memberId")
            mobile = login_data.get("mobile", "")
            
            if self.access_token and self.member_id:
                self.headers["authorization"] = self.access_token
                print("登录成功")
                return True
            else:
                print("登录失败：未获取到token或memberId")
                return False
                
        except Exception as e:
            print(f"登录请求失败: {e}")
            return False

    def get_task_list(self) -> Optional[List[Dict]]:
        """
        获取任务列表
        
        Returns:
            Optional[List[Dict]]: 任务列表，None表示获取失败
        """
        url = f"{self.base_url}/scrm/marketing/front/task/batchGetTaskList"
        
        try:
            response = self.client.get(url, headers=self.headers)
            response.raise_for_status()
            
            data = response.json()
            if not self.check_response(data, "获取任务列表"):
                return None
                
            tasks = data.get("data", [])
            filtered_tasks = []
            for task in tasks:
                join_detail = task.get("joinDetailVO", {})
                task_state = join_detail.get("taskState")
                task_type = task.get("taskType")
                
                if task_type == 8 and task_state in [1, 2]:
                    filtered_tasks.append(task)
                    
            return filtered_tasks
            
        except Exception as e:
            print(f"获取任务列表请求失败: {e}")
            return None

    def register_task(self, activity_id: str, activity_name: str) -> bool:
        """
        领取任务
        
        Args:
            activity_id (str): 活动ID
            activity_name (str): 活动名称
            
        Returns:
            bool: 是否成功
        """
        url = f"{self.base_url}/scrm/marketing/front/task/registerJoin"
        params = {"activityId": activity_id}
        
        try:
            response = self.client.get(url, params=params, headers=self.headers)
            response.raise_for_status()
            
            data = response.json()
            if self.check_response(data, f"领取任务【{activity_name}】"):
                print(f"任务【{activity_name}】领取成功")
                return True
            return False
            
        except Exception as e:
            print(f"领取任务【{activity_name}】请求失败: {e}")
            return False

    def visit_page(self, activity_id: str, activity_name: str) -> bool:
        """
        提交任务浏览记录
        
        Args:
            activity_id (str): 活动ID
            activity_name (str): 活动名称
            
        Returns:
            bool: 是否成功
        """
        url = f"{self.base_url}/scrm/marketing/front/task/visitPage"
        payload = {
            "activityId": activity_id,
            "taskType": 8
        }
        
        try:
            json_payload = json.dumps(payload, separators=(',', ':'))
                
            headers = self.headers.copy()
            headers["Content-Length"] = str(len(json_payload))
    
            response = self.client.post(url, content=json_payload, headers=headers)
            response.raise_for_status()
            
            data = response.json()
            if self.check_response(data, f"浏览任务【{activity_name}】"):
                print(f"任务【{activity_name}】浏览成功")
                return True
            return False
            
        except Exception as e:
            print(f"浏览任务【{activity_name}】请求失败: {e}")
            return False

    def receive_prize(self, activity_id: str, activity_name: str) -> bool:
        """
        领取任务奖励
        
        Args:
            activity_id (str): 活动ID
            activity_name (str): 活动名称
            
        Returns:
            bool: 是否成功
        """
        url = f"{self.base_url}/scrm/marketing/front/task/receivePrize"
        params = {"activityId": activity_id}
        
        try:
            response = self.client.get(url, params=params, headers=self.headers)
            response.raise_for_status()
            
            data = response.json()
            if self.check_response(data, f"领取奖励【{activity_name}】"):
                print(f"任务【{activity_name}】奖励领取成功")
                return True
            return False
            
        except Exception as e:
            print(f"领取奖励【{activity_name}】请求失败: {e}")
            return False

    def get_lottery_activity_id(self) -> Optional[str]:
        """
        获取抽奖活动ID
        
        Returns:
            Optional[str]: 抽奖活动ID，None表示获取失败
        """
        if self.lottery_activity_id:
            return self.lottery_activity_id
            
        url = f"{self.base_url}/scrm/cms/front/page/redirect/all"
        payload = {}
        
        try:
            json_payload = json.dumps(payload, separators=(',', ':'))
                
            headers = self.headers.copy()
            headers["Content-Length"] = str(len(json_payload))
    
            response = self.client.post(url, content=json_payload, headers=headers)
            response.raise_for_status()
            
            data = response.json()
            if not self.check_response(data, "获取抽奖活动ID"):
                return None
                
            activities = data.get("data", [])
            for activity in activities:
                original_title = activity.get("originalTitle", "")
                if "抽" in original_title and "停用" not in original_title:
                    redirect_url = activity.get("redirectUrl", "")
                    try:
                        redirect_data = json.loads(redirect_url)
                        params = redirect_data.get("params", {})
                        activity_id = params.get("id")
                        if activity_id:
                            self.lottery_activity_id = activity_id
                            return activity_id
                    except json.JSONDecodeError:
                        continue
                        
            print("未找到有效的抽奖活动")
            return None
            
        except Exception as e:
            print(f"获取抽奖活动ID请求失败: {e}")
            return None

    def get_lottery_quota_rules(self, activity_id: str) -> Optional[Dict]:
        """
        获取抽奖任务规则

        Args:
            activity_id (str): 抽奖活动ID

        Returns:
            Optional[Dict]: 抽奖任务规则，None表示获取失败
        """
        url = f"{self.base_url}/scrm/marketing/front/activity/lottery-wheel/get-user-exchange-quota-rule-list"
        params = {"activityId": activity_id}

        try:
            response = self.client.get(url, params=params, headers=self.headers)
            response.raise_for_status()

            data = response.json()
            if not self.check_response(data, "获取抽奖任务规则"):
                return None

            return data.get("data", {})

        except Exception as e:
            print(f"获取抽奖任务规则请求失败: {e}")
            return None

    def add_browse_record(self, activity_id: str, link_path: str, second: int, title: str) -> bool:
        """
        添加浏览记录

        Args:
            activity_id (str): 抽奖活动ID
            link_path (str): 链接路径
            second (int): 浏览秒数
            title (str): 页面标题

        Returns:
            bool: 是否成功
        """
        url = f"{self.base_url}/scrm/marketing/front/activity/lottery-wheel/add-browse-record"
        payload = {
            "activityId": activity_id,
            "memberId": self.member_id,
            "linkPath": link_path,
            "second": second
        }

        try:
            json_payload = json.dumps(payload, separators=(',', ':'))
                
            headers = self.headers.copy()
            headers["Content-Length"] = str(len(json_payload))
    
            response = self.client.post(url, content=json_payload, headers=headers)
            response.raise_for_status()

            data = response.json()
            if self.check_response(data, f"浏览【{title}】{second}秒"):
                print("任务完成")
                return True
            return False

        except Exception as e:
            print(f"添加浏览记录请求失败: {e}")
            return False

    def submit_share_task(self, activity_id: str) -> bool:
        """
        提交分享任务

        Args:
            activity_id (str): 抽奖活动ID

        Returns:
            bool: 是否成功
        """
        url = f"{self.base_url}/scrm/marketing/front/activity/lottery-wheel/user-exchange-quota-operate"
        payload = {
            "activityId": activity_id,
            "type": 2,
            "versionId": "7"
        }

        try:
            json_payload = json.dumps(payload, separators=(',', ':'))
                
            headers = self.headers.copy()
            headers["Content-Length"] = str(len(json_payload))
    
            response = self.client.post(url, content=json_payload, headers=headers)
            response.raise_for_status()

            data = response.json()
            if self.check_response(data, "分享任务"):
                print("分享任务完成")
                return True
            return False

        except Exception as e:
            print(f"提交分享任务请求失败: {e}")
            return False

    def get_lottery_quota(self, activity_id: str) -> Optional[int]:
        """
        查询抽奖次数

        Args:
            activity_id (str): 抽奖活动ID

        Returns:
            Optional[int]: 抽奖次数，None表示获取失败
        """
        url = f"{self.base_url}/scrm/marketing/front/activity/lottery-wheel/get-user-lottery-quota"
        params = {"activityId": activity_id}

        try:
            response = self.client.get(url, params=params, headers=self.headers)
            response.raise_for_status()

            data = response.json()
            if not self.check_response(data, "查询抽奖次数"):
                return None

            quota = data.get("data", 0)
            print(f"可用抽奖次数{quota}次")
            return quota

        except Exception as e:
            print(f"查询抽奖次数请求失败: {e}")
            return None

    def ensure_store_id(self) -> bool:
        """
        确保门店ID已设置，如未设置则从会员信息中获取
        
        Returns:
            bool: 是否成功获取门店ID
        """
        if self.store_id:
            return True
            
        member_info = self.get_member_info()
        if not member_info:
            print("获取会员信息失败，无法获取门店ID")
            return False
            
        register_store_id = member_info.get("registerStoreId")
        register_store_name = member_info.get("registerStoreName", "")
        
        if not register_store_id:
            print("会员信息中未找到注册门店ID")
            return False
            
        self.store_id = register_store_id
        print(f"获取到注册门店信息：{register_store_name}({register_store_id})")
        return True

    def draw_lottery(self, activity_id: str) -> Optional[Dict]:
        """
        抽奖

        Args:
            activity_id (str): 抽奖活动ID

        Returns:
            Optional[Dict]: 抽奖结果，None表示抽奖失败
        """
        if not self.ensure_store_id():
            print("门店ID获取失败，无法进行抽奖")
            return None
        
        url = f"{self.base_url}/scrm/marketing/front/activity/lottery-wheel/prize-draw"
        payload = {
            "activityId": activity_id,
            "versionId": "7"
        }

        try:
            json_payload = json.dumps(payload, separators=(',', ':'))
                
            headers = self.headers.copy()
            headers["Content-Length"] = str(len(json_payload))
            headers["storeid"] = self.store_id
    
            response = self.client.post(url, content=json_payload, headers=headers)
            response.raise_for_status()

            data = response.json()
            if not self.check_response(data, "抽奖"):
                return None

            return data.get("data", {})

        except Exception as e:
            print(f"抽奖请求失败: {e}")
            return None

    def get_member_info(self) -> Optional[Dict]:
        """
        获取会员信息

        Returns:
            Optional[Dict]: 会员信息，None表示获取失败
        """
        url = f"{self.base_url}/scrm/member/front/getCurrentMember"

        try:
            response = self.client.get(url, headers=self.headers)
            response.raise_for_status()

            data = response.json()
            if not self.check_response(data, "获取会员信息"):
                return None

            return data.get("data", {})

        except Exception as e:
            print(f"获取会员信息请求失败: {e}")
            return None

    def get_coupons(self) -> Optional[List[Dict]]:
        """
        获取未使用的优惠券

        Returns:
            Optional[List[Dict]]: 优惠券列表，None表示获取失败
        """
        url = f"{self.base_url}/scrm/marketing/front/memberCoupon/page"
        params = {
            "currPage": 1,
            "pageSize": 20,
            "couponState": 2
        }

        try:
            response = self.client.get(url, params=params, headers=self.headers)
            response.raise_for_status()

            data = response.json()
            if not self.check_response(data, "获取优惠券"):
                return None

            records = data.get("data", {}).get("records", [])
            return records

        except Exception as e:
            print(f"获取优惠券请求失败: {e}")
            return None

    def process_tasks(self) -> bool:
        """
        处理任务流程

        Returns:
            bool: 是否处理成功
        """
        # 1. 获取任务列表
        tasks = self.get_task_list()
        if not tasks:
            print("未获取到有效任务")
            return False

        # 2. 处理任务
        for task in tasks:
            activity_id = task.get("activityId")
            activity_name = task.get("activityName")
            join_detail = task.get("joinDetailVO", {})
            task_state = join_detail.get("taskState")

            # 检查必要字段
            if not activity_id or not activity_name:
                continue

            if task_state == 1:  # 未领取
                if not self.register_task(activity_id, activity_name):
                    continue

            if task_state in [1, 2]:  # 未领取或待完成
                # 提交浏览记录
                if not self.visit_page(activity_id, activity_name):
                    continue

                # 领取奖励
                if not self.receive_prize(activity_id, activity_name):
                    continue

            elif task_state == 3:  # 已完成，直接领取奖励
                self.receive_prize(activity_id, activity_name)

        return True

    def process_lottery(self) -> bool:
        """
        处理抽奖流程

        Returns:
            bool: 是否处理成功
        """
        # 1. 获取抽奖活动ID
        activity_id = self.get_lottery_activity_id()
        if not activity_id:
            return False

        # 2. 获取抽奖任务规则
        quota_rules = self.get_lottery_quota_rules(activity_id)
        if not quota_rules:
            return False

        # 3. 处理浏览任务
        browse_task = quota_rules.get("browseDesignatedPage", {})
        if browse_task.get("valid") and browse_task.get("state") == 1:
            thresholds = browse_task.get("thresholds", [])
            for threshold in thresholds:
                if threshold.get("valid"):
                    link_url = threshold.get("linkUrl", "")
                    second = threshold.get("second", 10)

                    try:
                        link_data = json.loads(link_url)
                        title = link_data.get("title", "")
                        path = link_data.get("path", "")

                        print(f"可做任务：浏览【{title}】{second}秒")
                        self.add_browse_record(activity_id, path, second, title)
                    except json.JSONDecodeError:
                        continue

        # 4. 处理分享任务
        share_task = quota_rules.get("share", {})
        if share_task.get("valid") and share_task.get("state") == 1:
            self.submit_share_task(activity_id)

        # 5. 查询抽奖次数并抽奖
        time.sleep(2)
        quota = self.get_lottery_quota(activity_id)
        if quota and quota > 0:
            for i in range(quota):
                draw_result = self.draw_lottery(activity_id)
                if draw_result:
                    state = draw_result.get("state", 1)
                    if state == 0:  # 中奖
                        prize = draw_result.get("prize", {})
                        prize_name = prize.get("prizeName", "")
                        prize_object = prize.get("prizeObject", {})
                        coupons = prize_object.get("coupons", [])
                        if coupons:
                            count = coupons[0].get("count", 1)
                            print(f"第{i+1}次抽奖抽中了{prize_name}x{count}")
                        else:
                            print(f"第{i+1}次抽奖抽中了{prize_name}")
                    if i < quota - 1:
                        time.sleep(1)

        return True

    def process_account(self, nick_name: str, code: str) -> bool:
        """
        处理单个账号的签到流程

        Args:
            nick_name (str): 账号昵称
            code (str): 微信小程序登录code

        Returns:
            bool: 是否处理成功
        """
        print(f"\n开始处理账号: {nick_name}")

        # 1. 登录
        if not self.login(code):
            print(f"账号 {nick_name} 登录失败")
            return False

        # 2. 处理任务
        self.process_tasks()

        # 3. 处理抽奖
        self.process_lottery()

        # 4. 获取会员信息和积分
        member_info = self.get_member_info()
        if member_info:
            mobile = member_info.get("mobile", "")
            balance = member_info.get("balance", "0")

            # 手机号打码
            if len(mobile) >= 7:
                masked_mobile = mobile[:3] + "****" + mobile[7:]
            else:
                masked_mobile = mobile

            print(f"{masked_mobile}当前有{balance}积分")

        # 5. 获取优惠券信息
        coupons = self.get_coupons()
        if coupons:
            print("未使用优惠券：")
            for coupon in coupons:
                coupon_name = coupon.get("couponName", "")
                effective_time_end = coupon.get("effectiveTimeEnd", "")
                print(f"{coupon_name}({effective_time_end}过期)")
        else:
            print("暂无未使用优惠券")

        return True

    def close(self):
        """关闭HTTP客户端"""
        self.client.close()

    def reset_session(self):
        """
        重置/清理会话数据
        """
        try:
            self.client.close()
        except Exception:
            pass

        self.client = httpx.Client(http2=True, timeout=30.0, verify=False)

        self.access_token = None
        self.member_id = None
        self.headers["authorization"] = ""
        self.store_id = STORE_ID

        print("已清理会话数据，准备下一个账号...")


def main():
    """主函数"""
    try:
        print("正在获取登录Code...")
        app_id = "wx295a967684523cdd"
        codes = getCode.get_wechat_codes(app_id)

        if not codes:
            print("未获取到任何在线账号的Code")
            return

        print(f"获取到 {len(codes)} 个账号的Code")

        signin = LsymSignin()

        try:
            for i, (nick_name, code) in enumerate(codes.items(), 1):
                print(f"\n{'='*50}")
                print(f"处理第 {i}/{len(codes)} 个账号")
                print(f"{'='*50}")

                try:
                    signin.process_account(nick_name, code)
                except Exception as e:
                    print(f"处理账号 {nick_name} 时发生错误: {e}")

                signin.reset_session()

                if i < len(codes):
                    print(f"等待2秒后处理下一个账号...")
                    time.sleep(2)

        finally:
            signin.close()

        print("\n所有账号处理完成")

    except Exception as e:
        print(f"脚本执行失败: {e}")


if __name__ == "__main__":
    main()
