From a2b873a3c2292f5a68fbd24a87c78201356692ce Mon Sep 17 00:00:00 2001 From: stu2not Date: Thu, 25 Jun 2026 18:53:01 +0800 Subject: [PATCH] Add parent WeChat identity mapping --- talkingq-url/banban/dao/parent.py | 318 +++++++++++++++++- talkingq-url/banban/service/parent.py | 11 +- talkingq-url/database/init_db.py | 27 ++ talkingq-url/database/models.py | 19 ++ talkingq-url/mysql/init/02-init.sql | 18 + .../tests/test_parent_wechat_identity.py | 310 +++++++++++++++++ 6 files changed, 697 insertions(+), 6 deletions(-) create mode 100644 talkingq-url/tests/test_parent_wechat_identity.py diff --git a/talkingq-url/banban/dao/parent.py b/talkingq-url/banban/dao/parent.py index 1d71cd1..120c114 100644 --- a/talkingq-url/banban/dao/parent.py +++ b/talkingq-url/banban/dao/parent.py @@ -43,6 +43,222 @@ class ParentDAO(BaseDAO): ) ).mappings().first() + async def get_by_unionid(self, unionid: str) -> Optional[Mapping]: + return ( + await self.execute( + """ + SELECT + p.*, + COALESCE(binding_counts.cnt, 0) AS active_binding_count, + COALESCE(relation_counts.cnt, 0) AS active_relation_count, + COALESCE(family_counts.cnt, 0) AS active_family_count + FROM parents AS p + LEFT JOIN ( + SELECT owner_user_id AS user_id, COUNT(*) AS cnt + FROM device_bindings + WHERE status = 1 + GROUP BY owner_user_id + ) AS binding_counts + ON binding_counts.user_id = p.user_id + LEFT JOIN ( + SELECT user_id, COUNT(*) AS cnt + FROM parent_child_relations + WHERE status = 1 + GROUP BY user_id + ) AS relation_counts + ON relation_counts.user_id = p.user_id + LEFT JOIN ( + SELECT user_id, COUNT(*) AS cnt + FROM device_family_members + WHERE status = 1 + GROUP BY user_id + ) AS family_counts + ON family_counts.user_id = p.user_id + WHERE p.unionid = :unionid + AND p.status = 1 + ORDER BY + ( + COALESCE(binding_counts.cnt, 0) + + COALESCE(relation_counts.cnt, 0) + + COALESCE(family_counts.cnt, 0) + ) DESC, + p.user_id ASC + LIMIT 1 + """, + {"unionid": unionid}, + ) + ).mappings().first() + + async def get_wechat_identity_by_openid( + self, + *, + app_id: str, + account_type: str, + openid: str, + ) -> Optional[Mapping]: + return ( + await self.execute( + """ + SELECT pwi.*, p.status AS parent_status + FROM parent_wechat_identities AS pwi + JOIN parents AS p + ON p.user_id = pwi.user_id + WHERE pwi.app_id = :app_id + AND pwi.account_type = :account_type + AND pwi.openid = :openid + AND p.status = 1 + LIMIT 1 + """, + {"app_id": app_id, "account_type": account_type, "openid": openid}, + ) + ).mappings().first() + + async def get_wechat_identity_by_unionid( + self, + *, + account_type: str, + unionid: str, + ) -> Optional[Mapping]: + return ( + await self.execute( + """ + SELECT + pwi.*, + COALESCE(binding_counts.cnt, 0) AS active_binding_count, + COALESCE(relation_counts.cnt, 0) AS active_relation_count, + COALESCE(family_counts.cnt, 0) AS active_family_count + FROM parent_wechat_identities AS pwi + JOIN parents AS p + ON p.user_id = pwi.user_id + LEFT JOIN ( + SELECT owner_user_id AS user_id, COUNT(*) AS cnt + FROM device_bindings + WHERE status = 1 + GROUP BY owner_user_id + ) AS binding_counts + ON binding_counts.user_id = pwi.user_id + LEFT JOIN ( + SELECT user_id, COUNT(*) AS cnt + FROM parent_child_relations + WHERE status = 1 + GROUP BY user_id + ) AS relation_counts + ON relation_counts.user_id = pwi.user_id + LEFT JOIN ( + SELECT user_id, COUNT(*) AS cnt + FROM device_family_members + WHERE status = 1 + GROUP BY user_id + ) AS family_counts + ON family_counts.user_id = pwi.user_id + WHERE pwi.account_type = :account_type + AND pwi.unionid = :unionid + AND p.status = 1 + ORDER BY + ( + COALESCE(binding_counts.cnt, 0) + + COALESCE(relation_counts.cnt, 0) + + COALESCE(family_counts.cnt, 0) + ) DESC, + pwi.user_id ASC + LIMIT 1 + """, + {"account_type": account_type, "unionid": unionid}, + ) + ).mappings().first() + + async def get_wechat_account_by_unionid(self, unionid: str) -> Optional[Mapping]: + return ( + await self.execute( + """ + SELECT + pwa.*, + COALESCE(binding_counts.cnt, 0) AS active_binding_count, + COALESCE(relation_counts.cnt, 0) AS active_relation_count, + COALESCE(family_counts.cnt, 0) AS active_family_count + FROM parent_wechat_accounts AS pwa + JOIN parents AS p + ON p.user_id = pwa.user_id + LEFT JOIN ( + SELECT owner_user_id AS user_id, COUNT(*) AS cnt + FROM device_bindings + WHERE status = 1 + GROUP BY owner_user_id + ) AS binding_counts + ON binding_counts.user_id = pwa.user_id + LEFT JOIN ( + SELECT user_id, COUNT(*) AS cnt + FROM parent_child_relations + WHERE status = 1 + GROUP BY user_id + ) AS relation_counts + ON relation_counts.user_id = pwa.user_id + LEFT JOIN ( + SELECT user_id, COUNT(*) AS cnt + FROM device_family_members + WHERE status = 1 + GROUP BY user_id + ) AS family_counts + ON family_counts.user_id = pwa.user_id + WHERE pwa.unionid = :unionid + AND p.status = 1 + ORDER BY + ( + COALESCE(binding_counts.cnt, 0) + + COALESCE(relation_counts.cnt, 0) + + COALESCE(family_counts.cnt, 0) + ) DESC, + pwa.user_id ASC + LIMIT 1 + """, + {"unionid": unionid}, + ) + ).mappings().first() + + async def upsert_wechat_identity( + self, + *, + user_id: int, + app_id: str, + account_type: str, + openid: str, + unionid: Optional[str] = None, + ) -> None: + await self.execute( + """ + INSERT INTO parent_wechat_identities ( + user_id, + app_id, + account_type, + openid, + unionid, + created_at, + updated_at + ) VALUES ( + :user_id, + :app_id, + :account_type, + :openid, + :unionid, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) + ON DUPLICATE KEY UPDATE + user_id = VALUES(user_id), + openid = VALUES(openid), + unionid = COALESCE(VALUES(unionid), unionid), + updated_at = CURRENT_TIMESTAMP + """, + { + "user_id": user_id, + "app_id": app_id, + "account_type": account_type, + "openid": openid, + "unionid": unionid, + }, + ) + await self.commit() + async def update( self, user_id: int, @@ -107,19 +323,111 @@ class ParentDAO(BaseDAO): unionid: Optional[str] = None, nickname: Optional[str] = None, avatar_url: Optional[str] = None, + app_id: Optional[str] = None, + account_type: str = "mini_program", ) -> int: + normalized_app_id = (app_id or "").strip() + normalized_account_type = (account_type or "mini_program").strip() or "mini_program" + if normalized_app_id: + existing_identity = await self.get_wechat_identity_by_openid( + app_id=normalized_app_id, + account_type=normalized_account_type, + openid=openid, + ) + if existing_identity: + user_id = int(existing_identity["user_id"]) + await self.update_from_wechat_login(user_id, unionid, nickname, avatar_url) + await self.upsert_wechat_identity( + user_id=user_id, + app_id=normalized_app_id, + account_type=normalized_account_type, + openid=openid, + unionid=unionid, + ) + return user_id + + if unionid: + union_identity = await self.get_wechat_identity_by_unionid( + account_type=normalized_account_type, + unionid=unionid, + ) + if union_identity: + user_id = int(union_identity["user_id"]) + await self.update_from_wechat_login(user_id, unionid, nickname, avatar_url) + await self.upsert_wechat_identity( + user_id=user_id, + app_id=normalized_app_id, + account_type=normalized_account_type, + openid=openid, + unionid=unionid, + ) + return user_id + + union_account = await self.get_wechat_account_by_unionid(unionid) + if union_account: + user_id = int(union_account["user_id"]) + await self.update_from_wechat_login(user_id, unionid, nickname, avatar_url) + await self.upsert_wechat_identity( + user_id=user_id, + app_id=normalized_app_id, + account_type=normalized_account_type, + openid=openid, + unionid=unionid, + ) + return user_id + + union_parent = await self.get_by_unionid(unionid) + if union_parent: + user_id = int(union_parent["user_id"]) + await self.update_from_wechat_login(user_id, unionid, nickname, avatar_url) + await self.upsert_wechat_identity( + user_id=user_id, + app_id=normalized_app_id, + account_type=normalized_account_type, + openid=openid, + unionid=unionid, + ) + return user_id + existing = await self.get_by_openid(openid) if existing: - await self.update_from_wechat_login(existing["user_id"], unionid, nickname, avatar_url) - return int(existing["user_id"]) + user_id = int(existing["user_id"]) + await self.update_from_wechat_login(user_id, unionid, nickname, avatar_url) + if normalized_app_id: + await self.upsert_wechat_identity( + user_id=user_id, + app_id=normalized_app_id, + account_type=normalized_account_type, + openid=openid, + unionid=unionid, + ) + return user_id try: - return await self.create(openid, unionid, nickname, avatar_url) + user_id = await self.create(openid, unionid, nickname, avatar_url) + if normalized_app_id: + await self.upsert_wechat_identity( + user_id=user_id, + app_id=normalized_app_id, + account_type=normalized_account_type, + openid=openid, + unionid=unionid, + ) + return user_id except IntegrityError: await self.db.rollback() existing = await self.get_by_openid(openid) if not existing: raise + user_id = int(existing["user_id"]) if unionid or nickname or avatar_url: - await self.update_from_wechat_login(existing["user_id"], unionid, nickname, avatar_url) - return int(existing["user_id"]) + await self.update_from_wechat_login(user_id, unionid, nickname, avatar_url) + if normalized_app_id: + await self.upsert_wechat_identity( + user_id=user_id, + app_id=normalized_app_id, + account_type=normalized_account_type, + openid=openid, + unionid=unionid, + ) + return user_id diff --git a/talkingq-url/banban/service/parent.py b/talkingq-url/banban/service/parent.py index 235dc12..f8f8d48 100644 --- a/talkingq-url/banban/service/parent.py +++ b/talkingq-url/banban/service/parent.py @@ -19,11 +19,20 @@ class ParentService(DatabaseServiceBase): unionid: Optional[str] = None, nickname: Optional[str] = None, avatar_url: Optional[str] = None, + app_id: Optional[str] = None, + account_type: str = "mini_program", ) -> Mapping: db_session = await self.get_session() try: dao = ParentDAO(db_session) - user_id = await dao.upsert(openid, unionid, nickname, avatar_url) + user_id = await dao.upsert( + openid, + unionid, + nickname, + avatar_url, + app_id=app_id or settings.wechat_app_id, + account_type=account_type, + ) return await self.get(user_id) finally: await db_session.close() diff --git a/talkingq-url/database/init_db.py b/talkingq-url/database/init_db.py index d9101bc..d445634 100644 --- a/talkingq-url/database/init_db.py +++ b/talkingq-url/database/init_db.py @@ -381,6 +381,32 @@ async def _ensure_parent_wechat_accounts_table(conn) -> None: ) +async def _ensure_parent_wechat_identities_table(conn) -> None: + await conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS parent_wechat_identities ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + app_id VARCHAR(64) NOT NULL, + account_type VARCHAR(32) NOT NULL, + openid VARCHAR(64) NOT NULL, + unionid VARCHAR(64) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_parent_wechat_identity_app_openid (app_id, account_type, openid), + KEY idx_parent_wechat_identity_user (user_id), + KEY idx_parent_wechat_identity_unionid (unionid), + CONSTRAINT fk_parent_wechat_identity_user + FOREIGN KEY (user_id) + REFERENCES parents (user_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + """ + ) + ) + + async def _ensure_wechat_mp_bind_states_table(conn) -> None: await conn.execute( text( @@ -428,6 +454,7 @@ async def init_db(): await _ensure_cards_allow_multiple_per_device(conn) await _ensure_device_family_tables(conn) await _ensure_parent_wechat_accounts_table(conn) + await _ensure_parent_wechat_identities_table(conn) await _ensure_wechat_mp_bind_states_table(conn) await engine.dispose() session_logger.info("system", "database", "数据库表已成功创建") diff --git a/talkingq-url/database/models.py b/talkingq-url/database/models.py index 59f811d..407d3b8 100644 --- a/talkingq-url/database/models.py +++ b/talkingq-url/database/models.py @@ -245,6 +245,25 @@ class ParentWechatAccount(Base): updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP")) +class ParentWechatIdentity(Base): + __tablename__ = "parent_wechat_identities" + __table_args__ = ( + UniqueConstraint("app_id", "account_type", "openid", name="uq_parent_wechat_identity_app_openid"), + Index("idx_parent_wechat_identity_user", "user_id"), + Index("idx_parent_wechat_identity_unionid", "unionid"), + {'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'} + ) + + id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("parents.user_id"), nullable=False) + app_id: Mapped[str] = mapped_column(String(64), nullable=False) + account_type: Mapped[str] = mapped_column(String(32), nullable=False) + openid: Mapped[str] = mapped_column(String(64), nullable=False) + unionid: Mapped[Optional[str]] = mapped_column(String(64)) + created_at: Mapped[Optional[datetime]] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP")) + updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP")) + + class WechatMpBindState(Base): __tablename__ = "wechat_mp_bind_states" __table_args__ = ( diff --git a/talkingq-url/mysql/init/02-init.sql b/talkingq-url/mysql/init/02-init.sql index 4b65b40..62c0c11 100644 --- a/talkingq-url/mysql/init/02-init.sql +++ b/talkingq-url/mysql/init/02-init.sql @@ -256,6 +256,24 @@ CREATE TABLE IF NOT EXISTS `parents` ( KEY `idx_parents_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS `parent_wechat_identities` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `app_id` VARCHAR(64) NOT NULL, + `account_type` VARCHAR(32) NOT NULL, + `openid` VARCHAR(64) NOT NULL, + `unionid` VARCHAR(64) NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_parent_wechat_identity_app_openid` (`app_id`, `account_type`, `openid`), + KEY `idx_parent_wechat_identity_user` (`user_id`), + KEY `idx_parent_wechat_identity_unionid` (`unionid`), + CONSTRAINT `fk_parent_wechat_identity_user` + FOREIGN KEY (`user_id`) + REFERENCES `parents` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE IF NOT EXISTS `children` ( `child_id` BIGINT NOT NULL AUTO_INCREMENT, `child_name` VARCHAR(32) NOT NULL, diff --git a/talkingq-url/tests/test_parent_wechat_identity.py b/talkingq-url/tests/test_parent_wechat_identity.py new file mode 100644 index 0000000..dab22b4 --- /dev/null +++ b/talkingq-url/tests/test_parent_wechat_identity.py @@ -0,0 +1,310 @@ +import pytest + +from banban.dao.parent import ParentDAO +from banban.service.parent import ParentService +from database.init_db import _ensure_parent_wechat_identities_table + + +class FakeSession: + async def close(self): + pass + + +class FakeScalarResult: + def scalar(self): + return 0 + + +class FakeConnection: + def __init__(self): + self.sql = [] + + async def execute(self, statement, params=None): + del params + self.sql.append(str(statement)) + return FakeScalarResult() + + +@pytest.mark.asyncio +async def test_ensure_parent_wechat_identities_table_creates_multi_app_mapping_table(): + conn = FakeConnection() + + await _ensure_parent_wechat_identities_table(conn) + + executed = "\n".join(conn.sql) + assert "CREATE TABLE IF NOT EXISTS parent_wechat_identities" in executed + assert "UNIQUE KEY uq_parent_wechat_identity_app_openid (app_id, account_type, openid)" in executed + assert "KEY idx_parent_wechat_identity_unionid (unionid)" in executed + + +@pytest.mark.asyncio +async def test_parent_service_passes_current_wechat_app_id_to_identity_upsert(monkeypatch): + service = ParentService() + calls = [] + + async def fake_get_session(): + return FakeSession() + + async def fake_upsert(self, openid, unionid=None, nickname=None, avatar_url=None, app_id=None, account_type="mini_program"): + calls.append( + { + "openid": openid, + "unionid": unionid, + "nickname": nickname, + "avatar_url": avatar_url, + "app_id": app_id, + "account_type": account_type, + } + ) + return 42 + + async def fake_get(user_id): + return { + "user_id": user_id, + "openid": "openid-new", + "unionid": "union-1", + "nickname": "家长", + "avatar_url": None, + "phone": None, + "status": 1, + } + + monkeypatch.setattr(service, "get_session", fake_get_session) + monkeypatch.setattr("banban.service.parent.settings.wechat_app_id", "wx-new") + monkeypatch.setattr("banban.service.parent.ParentDAO.upsert", fake_upsert) + monkeypatch.setattr(service, "get", fake_get) + + parent = await service.create( + openid="openid-new", + unionid="union-1", + nickname="家长", + avatar_url=None, + ) + + assert parent["user_id"] == 42 + assert calls == [ + { + "openid": "openid-new", + "unionid": "union-1", + "nickname": "家长", + "avatar_url": None, + "app_id": "wx-new", + "account_type": "mini_program", + } + ] + + +@pytest.mark.asyncio +async def test_parent_dao_upsert_reuses_existing_identity(monkeypatch): + dao = ParentDAO(FakeSession()) + calls = [] + + async def fake_get_identity_by_openid(**kwargs): + calls.append(("identity_openid", kwargs)) + return {"user_id": 7} + + async def fake_update(user_id, unionid=None, nickname=None, avatar_url=None): + calls.append(("update", user_id, unionid, nickname, avatar_url)) + + async def fake_upsert_identity(**kwargs): + calls.append(("upsert_identity", kwargs)) + + monkeypatch.setattr(dao, "get_wechat_identity_by_openid", fake_get_identity_by_openid) + monkeypatch.setattr(dao, "update_from_wechat_login", fake_update) + monkeypatch.setattr(dao, "upsert_wechat_identity", fake_upsert_identity) + + user_id = await dao.upsert( + "openid-new", + "union-1", + "新昵称", + None, + app_id="wx-new", + ) + + assert user_id == 7 + assert calls == [ + ( + "identity_openid", + { + "app_id": "wx-new", + "account_type": "mini_program", + "openid": "openid-new", + }, + ), + ("update", 7, "union-1", "新昵称", None), + ( + "upsert_identity", + { + "user_id": 7, + "app_id": "wx-new", + "account_type": "mini_program", + "openid": "openid-new", + "unionid": "union-1", + }, + ), + ] + + +@pytest.mark.asyncio +async def test_parent_dao_upsert_links_new_openid_to_existing_union_parent(monkeypatch): + dao = ParentDAO(FakeSession()) + calls = [] + + async def fake_get_identity_by_openid(**kwargs): + calls.append(("identity_openid", kwargs)) + return None + + async def fake_get_identity_by_unionid(**kwargs): + calls.append(("identity_unionid", kwargs)) + return None + + async def fake_get_wechat_account_by_unionid(unionid): + calls.append(("mp_account_unionid", unionid)) + return None + + async def fake_get_by_unionid(unionid): + calls.append(("parent_unionid", unionid)) + return {"user_id": 4} + + async def fake_update(user_id, unionid=None, nickname=None, avatar_url=None): + calls.append(("update", user_id, unionid, nickname, avatar_url)) + + async def fake_upsert_identity(**kwargs): + calls.append(("upsert_identity", kwargs)) + + monkeypatch.setattr(dao, "get_wechat_identity_by_openid", fake_get_identity_by_openid) + monkeypatch.setattr(dao, "get_wechat_identity_by_unionid", fake_get_identity_by_unionid) + monkeypatch.setattr(dao, "get_wechat_account_by_unionid", fake_get_wechat_account_by_unionid) + monkeypatch.setattr(dao, "get_by_unionid", fake_get_by_unionid) + monkeypatch.setattr(dao, "update_from_wechat_login", fake_update) + monkeypatch.setattr(dao, "upsert_wechat_identity", fake_upsert_identity) + + user_id = await dao.upsert( + "openid-new", + "union-1", + "新昵称", + None, + app_id="wx-new", + ) + + assert user_id == 4 + assert ("parent_unionid", "union-1") in calls + assert calls[-1] == ( + "upsert_identity", + { + "user_id": 4, + "app_id": "wx-new", + "account_type": "mini_program", + "openid": "openid-new", + "unionid": "union-1", + }, + ) + + +@pytest.mark.asyncio +async def test_parent_dao_upsert_links_new_openid_to_existing_mp_union_account(monkeypatch): + dao = ParentDAO(FakeSession()) + calls = [] + + async def fake_get_identity_by_openid(**kwargs): + return None + + async def fake_get_identity_by_unionid(**kwargs): + calls.append(("identity_unionid", kwargs)) + return None + + async def fake_get_wechat_account_by_unionid(unionid): + calls.append(("mp_account_unionid", unionid)) + return {"user_id": 4} + + async def fake_update(user_id, unionid=None, nickname=None, avatar_url=None): + calls.append(("update", user_id, unionid, nickname, avatar_url)) + + async def fake_upsert_identity(**kwargs): + calls.append(("upsert_identity", kwargs)) + + monkeypatch.setattr(dao, "get_wechat_identity_by_openid", fake_get_identity_by_openid) + monkeypatch.setattr(dao, "get_wechat_identity_by_unionid", fake_get_identity_by_unionid) + monkeypatch.setattr(dao, "get_wechat_account_by_unionid", fake_get_wechat_account_by_unionid) + monkeypatch.setattr(dao, "update_from_wechat_login", fake_update) + monkeypatch.setattr(dao, "upsert_wechat_identity", fake_upsert_identity) + + user_id = await dao.upsert( + "openid-new", + "union-1", + "新昵称", + None, + app_id="wx-new", + ) + + assert user_id == 4 + assert ("mp_account_unionid", "union-1") in calls + assert calls[-1] == ( + "upsert_identity", + { + "user_id": 4, + "app_id": "wx-new", + "account_type": "mini_program", + "openid": "openid-new", + "unionid": "union-1", + }, + ) + + +@pytest.mark.asyncio +async def test_parent_dao_upsert_creates_new_parent_and_identity_for_new_user(monkeypatch): + dao = ParentDAO(FakeSession()) + calls = [] + + async def fake_get_identity_by_openid(**kwargs): + return None + + async def fake_get_identity_by_unionid(**kwargs): + return None + + async def fake_get_by_unionid(unionid): + return None + + async def fake_get_wechat_account_by_unionid(unionid): + return None + + async def fake_get_by_openid(openid): + return None + + async def fake_create(openid, unionid=None, nickname=None, avatar_url=None): + calls.append(("create", openid, unionid, nickname, avatar_url)) + return 99 + + async def fake_upsert_identity(**kwargs): + calls.append(("upsert_identity", kwargs)) + + monkeypatch.setattr(dao, "get_wechat_identity_by_openid", fake_get_identity_by_openid) + monkeypatch.setattr(dao, "get_wechat_identity_by_unionid", fake_get_identity_by_unionid) + monkeypatch.setattr(dao, "get_by_unionid", fake_get_by_unionid) + monkeypatch.setattr(dao, "get_wechat_account_by_unionid", fake_get_wechat_account_by_unionid) + monkeypatch.setattr(dao, "get_by_openid", fake_get_by_openid) + monkeypatch.setattr(dao, "create", fake_create) + monkeypatch.setattr(dao, "upsert_wechat_identity", fake_upsert_identity) + + user_id = await dao.upsert( + "openid-brand-new", + None, + "新用户", + None, + app_id="wx-new", + ) + + assert user_id == 99 + assert calls == [ + ("create", "openid-brand-new", None, "新用户", None), + ( + "upsert_identity", + { + "user_id": 99, + "app_id": "wx-new", + "account_type": "mini_program", + "openid": "openid-brand-new", + "unionid": None, + }, + ), + ]