Browse Source

建立商家分管账号数据模型

qmj 1 day ago
parent
commit
5b910adb8c

+ 36 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/InfoUser.java

@@ -72,6 +72,15 @@ public class InfoUser
     @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
     private String status;
 
+    /** 分管账号所属商家主账号ID */
+    private Long merchantOwnerId;
+
+    /** 主账号控制状态(0正常 1停用) */
+    private String subaccountStatus;
+
+    /** 最近一次商家端成功登录时间 */
+    private Date lastLoginAt;
+
     /** 删除标志(0代表存在 1代表删除) */
     @Excel(name = "删除标志", readConverterExp = "0=代表存在,1=代表删除")
     private String delFlag;
@@ -334,6 +343,30 @@ public class InfoUser
     {
         return status;
     }
+
+    public Long getMerchantOwnerId() {
+        return merchantOwnerId;
+    }
+
+    public void setMerchantOwnerId(Long merchantOwnerId) {
+        this.merchantOwnerId = merchantOwnerId;
+    }
+
+    public String getSubaccountStatus() {
+        return subaccountStatus;
+    }
+
+    public void setSubaccountStatus(String subaccountStatus) {
+        this.subaccountStatus = subaccountStatus;
+    }
+
+    public Date getLastLoginAt() {
+        return lastLoginAt;
+    }
+
+    public void setLastLoginAt(Date lastLoginAt) {
+        this.lastLoginAt = lastLoginAt;
+    }
     public void setDelFlag(String delFlag)
     {
         this.delFlag = delFlag;
@@ -610,6 +643,9 @@ public class InfoUser
             .append("avatar", getAvatar())
             .append("password", getPassword())
             .append("status", getStatus())
+            .append("merchantOwnerId", getMerchantOwnerId())
+            .append("subaccountStatus", getSubaccountStatus())
+            .append("lastLoginAt", getLastLoginAt())
             .append("delFlag", getDelFlag())
             .append("cid", getCid())
             .append("mycode", getMycode())

+ 33 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/MerchantSubaccountStore.java

@@ -0,0 +1,33 @@
+package com.ruoyi.system.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 商家分管账号与门店授权关系。
+ */
+@Data
+@TableName("merchant_subaccount_store")
+@EqualsAndHashCode(callSuper = false)
+public class MerchantSubaccountStore {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long subaccountUserId;
+    private Long storeId;
+    private Date createTime;
+    private Date updateTime;
+
+    public MerchantSubaccountStore() {
+    }
+
+    public MerchantSubaccountStore(Long subaccountUserId, Long storeId) {
+        this.subaccountUserId = subaccountUserId;
+        this.storeId = storeId;
+    }
+}

+ 16 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/constants/MerchantAccountConstants.java

@@ -0,0 +1,16 @@
+package com.ruoyi.system.domain.constants;
+
+/**
+ * 商家主账号与分管账号常量。
+ */
+public final class MerchantAccountConstants {
+
+    public static final String OWNER_USER_TYPE = "1";
+    public static final String SUBACCOUNT_USER_TYPE = "5";
+    public static final String STATUS_ENABLED = "0";
+    public static final String STATUS_DISABLED = "1";
+    public static final String NOT_DELETED = "0";
+
+    private MerchantAccountConstants() {
+    }
+}

+ 21 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/MerchantSubaccountStoreMapper.java

@@ -0,0 +1,21 @@
+package com.ruoyi.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.MerchantSubaccountStore;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface MerchantSubaccountStoreMapper extends BaseMapper<MerchantSubaccountStore> {
+
+    List<Long> selectStoreIdsBySubaccountUserId(Long subaccountUserId);
+
+    List<InfoUser> selectEligibleSubaccountsByStoreId(Long storeId);
+
+    int deleteBySubaccountUserId(Long subaccountUserId);
+
+    int deleteByStoreId(Long storeId);
+
+    int insertBatch(@Param("relations") List<MerchantSubaccountStore> relations);
+}

+ 19 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/IMerchantSubaccountStoreService.java

@@ -0,0 +1,19 @@
+package com.ruoyi.system.service;
+
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.MerchantSubaccountStore;
+
+import java.util.List;
+
+public interface IMerchantSubaccountStoreService {
+
+    List<Long> selectStoreIdsBySubaccountUserId(Long subaccountUserId);
+
+    List<InfoUser> selectEligibleSubaccountsByStoreId(Long storeId);
+
+    int replaceRelations(Long subaccountUserId, List<Long> storeIds);
+
+    int deleteByStoreId(Long storeId);
+
+    int insertBatch(List<MerchantSubaccountStore> relations);
+}

+ 60 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/MerchantSubaccountStoreServiceImpl.java

@@ -0,0 +1,60 @@
+package com.ruoyi.system.service.impl;
+
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.MerchantSubaccountStore;
+import com.ruoyi.system.mapper.MerchantSubaccountStoreMapper;
+import com.ruoyi.system.service.IMerchantSubaccountStoreService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+import java.util.List;
+
+@Service
+public class MerchantSubaccountStoreServiceImpl implements IMerchantSubaccountStoreService {
+
+    private final MerchantSubaccountStoreMapper mapper;
+
+    public MerchantSubaccountStoreServiceImpl(MerchantSubaccountStoreMapper mapper) {
+        this.mapper = mapper;
+    }
+
+    @Override
+    public List<Long> selectStoreIdsBySubaccountUserId(Long subaccountUserId) {
+        List<Long> storeIds = mapper.selectStoreIdsBySubaccountUserId(subaccountUserId);
+        return storeIds == null ? Collections.emptyList() : storeIds;
+    }
+
+    @Override
+    public List<InfoUser> selectEligibleSubaccountsByStoreId(Long storeId) {
+        List<InfoUser> users = mapper.selectEligibleSubaccountsByStoreId(storeId);
+        return users == null ? Collections.emptyList() : users;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int replaceRelations(Long subaccountUserId, List<Long> storeIds) {
+        int affected = mapper.deleteBySubaccountUserId(subaccountUserId);
+        if (storeIds == null || storeIds.isEmpty()) {
+            return affected;
+        }
+        List<MerchantSubaccountStore> relations = storeIds.stream()
+                .distinct()
+                .map(storeId -> new MerchantSubaccountStore(subaccountUserId, storeId))
+                .toList();
+        return affected + mapper.insertBatch(relations);
+    }
+
+    @Override
+    public int deleteByStoreId(Long storeId) {
+        return mapper.deleteByStoreId(storeId);
+    }
+
+    @Override
+    public int insertBatch(List<MerchantSubaccountStore> relations) {
+        if (relations == null || relations.isEmpty()) {
+            return 0;
+        }
+        return mapper.insertBatch(relations);
+    }
+}

+ 12 - 0
ruoyi-system/src/main/resources/mapper/infouser/InfoUserMapper.xml

@@ -15,6 +15,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="avatar"    column="avatar"    />
         <result property="password"    column="password"    />
         <result property="status"    column="status"    />
+        <result property="merchantOwnerId"    column="merchant_owner_id"    />
+        <result property="subaccountStatus"    column="subaccount_status"    />
+        <result property="lastLoginAt"    column="last_login_at"    />
         <result property="delFlag"    column="del_flag"    />
         <result property="cid"    column="cid"    />
         <result property="cidType"    column="cid_type"    />
@@ -105,6 +108,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="avatar != null and avatar != ''">avatar,</if>
             <if test="password != null">password,</if>
             <if test="status != null and status != ''">status,</if>
+            <if test="merchantOwnerId != null">merchant_owner_id,</if>
+            <if test="subaccountStatus != null and subaccountStatus != ''">subaccount_status,</if>
+            <if test="lastLoginAt != null">last_login_at,</if>
             <if test="delFlag != null">del_flag,</if>
             <if test="cid != null">cid,</if>
             <if test="cidType != null">cid_type,</if>
@@ -150,6 +156,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="avatar != null and avatar != ''">#{avatar},</if>
             <if test="password != null">#{password},</if>
             <if test="status != null and status != ''">#{status},</if>
+            <if test="merchantOwnerId != null">#{merchantOwnerId},</if>
+            <if test="subaccountStatus != null and subaccountStatus != ''">#{subaccountStatus},</if>
+            <if test="lastLoginAt != null">#{lastLoginAt},</if>
             <if test="delFlag != null">#{delFlag},</if>
             <if test="cid != null">#{cid},</if>
             <if test="cidType != null">#{cidType},</if>
@@ -199,6 +208,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="avatar != null and avatar != ''">avatar = #{avatar},</if>
             <if test="password != null">password = #{password},</if>
             <if test="status != null and status != ''">status = #{status},</if>
+            <if test="merchantOwnerId != null">merchant_owner_id = #{merchantOwnerId},</if>
+            <if test="subaccountStatus != null and subaccountStatus != ''">subaccount_status = #{subaccountStatus},</if>
+            <if test="lastLoginAt != null">last_login_at = #{lastLoginAt},</if>
             <if test="delFlag != null">del_flag = #{delFlag},</if>
             <if test="cid != null">cid = #{cid},</if>
             <if test="cidType != null">cid_type = #{cidType},</if>

+ 57 - 0
ruoyi-system/src/main/resources/mapper/infouser/MerchantSubaccountStoreMapper.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.MerchantSubaccountStoreMapper">
+
+    <resultMap id="MerchantSubaccountStoreResult" type="MerchantSubaccountStore">
+        <id property="id" column="id" />
+        <result property="subaccountUserId" column="subaccount_user_id" />
+        <result property="storeId" column="store_id" />
+        <result property="createTime" column="create_time" />
+        <result property="updateTime" column="update_time" />
+    </resultMap>
+
+    <select id="selectStoreIdsBySubaccountUserId" resultType="long">
+        SELECT store_id
+        FROM merchant_subaccount_store
+        WHERE subaccount_user_id = #{subaccountUserId}
+        ORDER BY store_id
+    </select>
+
+    <select id="selectEligibleSubaccountsByStoreId" resultMap="com.ruoyi.system.mapper.InfoUserMapper.InfoUserResult">
+        SELECT subaccount.*
+        FROM merchant_subaccount_store relation
+        INNER JOIN info_user subaccount ON subaccount.user_id = relation.subaccount_user_id
+        INNER JOIN info_user owner ON owner.user_id = subaccount.merchant_owner_id
+        INNER JOIN pos_store store ON store.id = relation.store_id AND store.user_id = owner.user_id
+        WHERE relation.store_id = #{storeId}
+          AND subaccount.user_type = '5'
+          AND subaccount.status = '0'
+          AND subaccount.subaccount_status = '0'
+          AND subaccount.del_flag = '0'
+          AND owner.user_type = '1'
+          AND owner.status = '0'
+          AND owner.del_flag = '0'
+        ORDER BY subaccount.user_id
+    </select>
+
+    <delete id="deleteBySubaccountUserId">
+        DELETE FROM merchant_subaccount_store
+        WHERE subaccount_user_id = #{subaccountUserId}
+    </delete>
+
+    <delete id="deleteByStoreId">
+        DELETE FROM merchant_subaccount_store
+        WHERE store_id = #{storeId}
+    </delete>
+
+    <insert id="insertBatch">
+        INSERT INTO merchant_subaccount_store
+            (subaccount_user_id, store_id, create_time, update_time)
+        VALUES
+        <foreach collection="relations" item="relation" separator=",">
+            (#{relation.subaccountUserId}, #{relation.storeId}, NOW(), NOW())
+        </foreach>
+    </insert>
+</mapper>

+ 69 - 0
ruoyi-system/src/test/java/com/ruoyi/system/mapper/MerchantSubaccountStoreMapperXmlTest.java

@@ -0,0 +1,69 @@
+package com.ruoyi.system.mapper;
+
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.MerchantSubaccountStore;
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.mapping.ResultMapping;
+import org.apache.ibatis.session.Configuration;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MerchantSubaccountStoreMapperXmlTest {
+
+    @Test
+    void infoUserMapsMerchantSubaccountFields() throws IOException {
+        Configuration configuration = mapperConfiguration(
+                "InfoUser", InfoUser.class, "mapper/infouser/InfoUserMapper.xml");
+
+        assertColumn(configuration, "com.ruoyi.system.mapper.InfoUserMapper.InfoUserResult",
+                "merchantOwnerId", "merchant_owner_id");
+        assertColumn(configuration, "com.ruoyi.system.mapper.InfoUserMapper.InfoUserResult",
+                "subaccountStatus", "subaccount_status");
+        assertColumn(configuration, "com.ruoyi.system.mapper.InfoUserMapper.InfoUserResult",
+                "lastLoginAt", "last_login_at");
+    }
+
+    @Test
+    void relationMapperUsesBoundParametersAndUniqueRelationColumns() throws IOException {
+        String resource = "mapper/infouser/MerchantSubaccountStoreMapper.xml";
+        try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource)) {
+            assertNotNull(input);
+            String xml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
+            assertTrue(xml.contains("#{subaccountUserId}"));
+            assertTrue(xml.contains("#{relation.subaccountUserId}"));
+            assertTrue(xml.contains("#{relation.storeId}"));
+            assertTrue(xml.contains("subaccount_user_id"));
+            assertTrue(xml.contains("store_id"));
+        }
+
+        mapperConfiguration("MerchantSubaccountStore", MerchantSubaccountStore.class, resource);
+    }
+
+    private void assertColumn(Configuration configuration, String resultMapId,
+                              String property, String column) {
+        ResultMapping mapping = configuration.getResultMap(resultMapId)
+                .getResultMappings().stream()
+                .filter(item -> property.equals(item.getProperty()))
+                .findFirst()
+                .orElseThrow();
+        assertEquals(column, mapping.getColumn());
+    }
+
+    private Configuration mapperConfiguration(String alias, Class<?> type, String resource)
+            throws IOException {
+        Configuration configuration = new Configuration();
+        configuration.getTypeAliasRegistry().registerAlias(alias, type);
+        try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource)) {
+            assertNotNull(input);
+            new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
+        }
+        return configuration;
+    }
+}

+ 22 - 0
updatesql/sql.md

@@ -925,3 +925,25 @@ WHERE NOT EXISTS (
 );
 ```
 
+## 2026-08-28 商家门店分管账号
+
+```sql
+-- 仅记录迁移脚本,由开发者确认环境后手动执行;Codex 不直接执行数据库变更。
+ALTER TABLE info_user
+  ADD COLUMN merchant_owner_id BIGINT NULL COMMENT '分管账号所属商家主账号ID' AFTER user_type,
+  ADD COLUMN subaccount_status CHAR(1) NULL COMMENT '主账号控制状态:0启用 1停用' AFTER status,
+  ADD COLUMN last_login_at DATETIME NULL COMMENT '最近一次商家端成功登录时间' AFTER subaccount_status,
+  ADD KEY idx_info_user_merchant_owner (merchant_owner_id, user_type, del_flag);
+
+CREATE TABLE merchant_subaccount_store (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  subaccount_user_id BIGINT NOT NULL,
+  store_id BIGINT NOT NULL,
+  create_time DATETIME NOT NULL,
+  update_time DATETIME NOT NULL,
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_subaccount_store (subaccount_user_id, store_id),
+  KEY idx_subaccount_store_store (store_id, subaccount_user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商家分管账号门店授权';
+```
+