SpringCloud microservice combat - building an enterprise-level development framework (fifty-three): WeChat applet authorization login increases multi-tenant configurable interface

  The GitEgg framework integrates the weixin-java-miniapp toolkit to implement WeChat applet-related interface calling functions. The underlying layer of weixin-java-miniapp supports multi-tenant expansion. Each mini program has a unique appid. The multi-tenant implementation of weixin-java-miniapp is not distinguished by the tenant ID TenantId. Instead, when the interface is called, the appid is passed in and the appid of ThreadLocal is dynamically switched to achieve multi-tenancy. . Moreover, the configurations of its multiple WeChat mini programs are all in the configuration yml file. In the actual business operation process, it is obviously inappropriate to modify the configuration file if you need to add multi-tenant mini programs.
  Now we need to integrate the multi-tenant implementation of weixin-java-miniapp into our framework so that multi-tenants can add multi-tenant applets through the system configuration interface. Earlier we talked about how to integrate and how to use weixin-java-miniapp to implement WeChat authorized login and account binding. Now we only need to add data configuration storage on the original basis, and load the corresponding data from the original read configuration file when the service starts. The WeChat applet interface instance is modified to generate the corresponding WeChat applet interface instance by reading the configuration file and reading the cache configuration.

1. Added WeChat applet configuration interface

1. WeChat applet configuration database design

  When designing the database, we need to know which fields need to be configured when authorizing the WeChat applet, whether they are optional fields or required fields. Here we know from the springboot project configuration file of weixin-java-miniapp that the required fields are:

# 公众号配置(必填)
wx.miniapp.appid = appId
wx.miniapp.secret = @secret
wx.miniapp.token = @token
wx.miniapp.aesKey = @aesKey
wx.miniapp.msgDataFormat = @msgDataFormat                  # 消息格式,XML或者JSON.
# 存储配置redis(可选)
# 注意: 指定redis.host值后不会使用容器注入的redis连接(JedisPool)
wx.miniapp.config-storage.type = Jedis                     # 配置类型: Memory(默认), Jedis, RedisTemplate
wx.miniapp.config-storage.key-prefix = wa                  # 相关redis前缀配置: wa(默认)
wx.miniapp.config-storage.redis.host = 127.0.0.1
wx.miniapp.config-storage.redis.port = 6379
# http客户端配置
wx.miniapp.config-storage.http-client-type=HttpClient      # http客户端类型: HttpClient(默认), OkHttp, JoddHttp
wx.miniapp.config-storage.http-proxy-host=
wx.miniapp.config-storage.http-proxy-port=
wx.miniapp.config-storage.http-proxy-username=
wx.miniapp.config-storage.http-proxy-password=

  According to our design, the tenant field needs to be added to the configuration file. We need to be compatible with the configuration file to configure the WeChat applet, and the configuration interface to configure the WeChat applet configuration information into the database. At the same time, add the md5 field configuration, using Used to compare whether the configuration information has changed when reading the configuration. Therefore, the database design to save the WeChat applet configuration is as follows:

CREATE TABLE `t_wechat_miniapp`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户id',
  `miniapp_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序名称',
  `appid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序appid',
  `secret` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序secret',
  `token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序token',
  `aes_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序aesKey',
  `msg_data_format` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '消息格式,XML或者JSON',
  `storage_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '配置类型: Memory(默认), Jedis, RedisTemplate',
  `key_prefix` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '相关redis前缀配置: wa(默认)',
  `redis_host` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Redis服务器地址',
  `redis_port` int(11) NULL DEFAULT NULL COMMENT 'Redis服务器端口',
  `http_client_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http客户端类型: HttpClient(默认), OkHttp, JoddHttp',
  `http_proxy_host` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http_proxy_host',
  `http_proxy_port` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http_proxy_port',
  `http_proxy_username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http_proxy_username',
  `http_proxy_password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http_proxy_password',
  `status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1' COMMENT '状态 1有效 0禁用',
  `md5` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'MD5',
  `comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信小程序配置' ROW_FORMAT = DYNAMIC;

2. Use the code generator to generate the addition, deletion, and modification code for the WeChat applet configuration

  Use the code generator to generate CRUD code based on the newly designed table. The detailed steps will not be repeated. We have previously explained in detail how to generate front-end and back-end code based on the database table design. It's just that you need to add business logic processing and update the cache configuration.

  • When adding, the configuration information is added to Redis, and the cache key needs to be generated based on whether the system has multi-tenancy enabled.
    /**
    * 创建微信小程序配置
    * @param miniapp
    * @return
    */
    @Override
    public boolean createMiniapp(CreateMiniappDTO miniapp) {
        Miniapp miniappEntity = BeanCopierUtils.copyByClass(miniapp, Miniapp.class);
        try {
            String miniappEntityStr = JsonUtils.objToJson(miniappEntity);
            miniappEntity.setMd5(SecureUtil.md5(miniappEntityStr));
        } catch (Exception e) {
            log.error("创建微信小程序配置时,md5加密失败:{}", e);
            throw new BusinessException("创建微信小程序配置时,md5加密失败:" + e);
        }
        boolean result = this.save(miniappEntity);
        if (result)
        {
            // 更新到缓存
            Miniapp miniappEntityLocal = this.getById(miniappEntity.getId());
            MiniappDTO miniappDTO = BeanCopierUtils.copyByClass(miniappEntityLocal, MiniappDTO.class);
            this.addOrUpdateMiniappCache(miniappDTO);
        }
        return result;
    }
  • When editing, you need to update the Redis configuration information because the key may also be modified at the same time. Therefore, you need to delete the old configuration information first and then add new configuration information.
    /**
    * 更新微信小程序配置
    * @param miniapp
    * @return
    */
    @Override
    public boolean updateMiniapp(UpdateMiniappDTO miniapp) {
        Miniapp miniappEntity = BeanCopierUtils.copyByClass(miniapp, Miniapp.class);
        Miniapp miniappEntityOld = this.getById(miniappEntity.getId());
        try {
            String miniappEntityStr = JsonUtils.objToJson(miniappEntity);
            miniappEntity.setMd5(SecureUtil.md5(miniappEntityStr));
        } catch (Exception e) {
            log.error("创建微信小程序配置时,md5加密失败:{}", e);
            throw new BusinessException("创建微信小程序配置时,md5加密失败:" + e);
        }
        boolean result = this.updateById(miniappEntity);
        if (result)
        {
            // 把旧的删掉
            MiniappDTO miniappDTOOld = BeanCopierUtils.copyByClass(miniappEntityOld, MiniappDTO.class);
            this.deleteMiniappCache(miniappDTOOld);
            // 更新到缓存
            Miniapp miniappEntityLocal = this.getById(miniappEntity.getId());
            MiniappDTO miniappDTO = BeanCopierUtils.copyByClass(miniappEntityLocal, MiniappDTO.class);
            this.addOrUpdateMiniappCache(miniappDTO);
        }
        return result;
    }
  • When deleting, directly generate the cache key according to the conditions, and then delete it.
    /**
    * 删除微信小程序配置
    * @param miniappId
    * @return
    */
    @Override
    public boolean deleteMiniapp(Long miniappId) {
        // 从缓存删除
        Miniapp miniappEntity = this.getById(miniappId);
        MiniappDTO miniappDTO = BeanCopierUtils.copyByClass(miniappEntity, MiniappDTO.class);
        this.deleteMiniappCache(miniappDTO);
        // 从数据库中删除
        boolean result = this.removeById(miniappId);
        return result;
    }
  • Public methods to add/update cache
    private void addOrUpdateMiniappCache(MiniappDTO miniappDTO) {
        try {

            String redisKey = MiniappConstant.WX_MINIAPP_CONFIG_KEY;
            if (enable) {
                redisKey = MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + miniappDTO.getAppid();
            }
            redisTemplate.opsForHash().put(redisKey, miniappDTO.getTenantId().toString(), JsonUtils.objToJson(miniappDTO));

            // wxMaService增加config
            this.addConfig(miniappDTO);

        } catch (Exception e) {
            log.error("初始化微信小程序配置失败:{}" , e);
        }
    }
  • Public method to delete cache
    private void deleteMiniappCache(MiniappDTO miniappDTO) {
        try {

            String redisKey = MiniappConstant.WX_MINIAPP_CONFIG_KEY;
            if (enable) {
                redisKey = MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + miniappDTO.getAppid();
            }
            redisTemplate.opsForHash().delete(redisKey, miniappDTO.getTenantId().toString(), JsonUtils.objToJson(miniappDTO));
            // wxMaService删除config
            this.removeConfig(miniappDTO);
        } catch (Exception e) {
            log.error("初始化微信小程序配置失败:{}" , e);
        }
    }
3. Modify the relevant configuration files so that the WeChat applet configuration supports both configuration files and database configuration.
  • The new GitEggWxMaRedissonConfigImpl class inherits from WxMaRedissonConfigImpl and adds the tenant configuration fields we need: configKey, tenantId, md5.
/**
 * @author GitEgg
 * @date 2023/7/21
 */
public class GitEggWxMaRedissonConfigImpl extends WxMaRedissonConfigImpl {

    protected String configKey;

    protected String tenantId;

    protected String md5;

    public GitEggWxMaRedissonConfigImpl(@NonNull RedissonClient redissonClient, String keyPrefix) {
        super(redissonClient, keyPrefix);
    }

    public GitEggWxMaRedissonConfigImpl(@NonNull RedissonClient redissonClient) {
        super(redissonClient);
    }
......
}
  • Modify the WxMaProperties class and add the field tenantId we need. Because configKey and md5 are generated by the system, these fields will only be used for judgment when they are dynamically configurable. If it is a configuration file configuration, the system must be restarted after the modification takes effect. So these fields are not needed here.
@Data
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxMaProperties {

    private List<Config> configs;

    @Data
    public static class Config {

        /**
         * 租户
         */
        private Long tenantId;

        /**
         * 设置微信小程序的appid
         */
        private String appid;

        /**
         * 设置微信小程序的Secret
         */
        private String secret;

        /**
         * 设置微信小程序消息服务器配置的token
         */
        private String token;

        /**
         * 设置微信小程序消息服务器配置的EncodingAESKey
         */
        private String aesKey;

        /**
         * 消息格式,XML或者JSON
         */
        private String msgDataFormat;
    }

}
  • Modify the WxMaConfiguration class. We use our own defined caching method and Config class to perform related operations. We use Redisson for caching here.
    @Bean
    public WxMaService wxMaService() {
        List<WxMaProperties.Config> configs = this.properties.getConfigs();
        //已添加缓存配置,如果配置文件没有,那么在缓存新增时,仍然可以setConfigs
//        if (configs == null) {
//            throw new WxRuntimeException("大哥,拜托先看下项目首页的说明(readme文件),添加下相关配置,注意别配错了!");
//        }
        WxMaService maService = new WxMaServiceImpl();
        if (null != configs)
        {
            maService.setMultiConfigs(
                    configs.stream()
                            .map(a -> {
//                    WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
//                    WxMaDefaultConfigImpl config = new WxMaRedisConfigImpl(new JedisPool());
                                GitEggWxMaRedissonConfigImpl config = new GitEggWxMaRedissonConfigImpl(redissonClient);
                                // 使用上面的配置时,需要同时引入jedis-lock的依赖,否则会报类无法找到的异常
                                config.setTenantId(null != a.getTenantId() ? a.getTenantId().toString() : AuthConstant.DEFAULT_TENANT_ID.toString());
                                config.setConfigKey(config.getTenantId() + StrPool.UNDERLINE + a.getAppid());
                                config.setAppid(a.getAppid());
                                config.setSecret(a.getSecret());
                                config.setToken(a.getToken());
                                config.setAesKey(a.getAesKey());
                                config.setMsgDataFormat(a.getMsgDataFormat());
                                return config;
                            }).collect(Collectors.toMap( GitEggWxMaRedissonConfigImpl::getConfigKey, a -> a, (o, n) -> o)));
        }
        return maService;
    }

2. Load the WeChat applet configuration information when the service starts

1. Added a new loading method. To load configuration data when the service starts, you need to exclude the multi-tenant plug-in. The reason why the configuration information is loaded into the cache at startup is because the configured microservice and the service related to the applet are not the same service, so the The cache acts as a store for related configuration.
  • There is a new initMiniappList database query method in MiniappMapper. Be sure to add the @InterceptorIgnore(tenantLine = “true”) annotation, indicating that this query does not require multi-tenant control and loads all configurations without distinguishing tenants when the service is started.
    /**
     * 排除多租户插件查询微信配置列表
     * @param miniappDTO
     * @return
     */
    @InterceptorIgnore(tenantLine = "true")
    List<MiniappDTO> initMiniappList(@Param("miniapp") QueryMiniappDTO miniappDTO);
    <!-- 不区分租户查询微信小程序配置信息 -->
    <select id="getMiniapp" resultType="com.gitegg.boot.extension.wx.miniapp.dto.MiniappDTO" parameterType="com.gitegg.boot.extension.wx.miniapp.dto.QueryMiniappDTO">
        SELECT
        <include refid="Base_Column_List"/>
        FROM t_wechat_miniapp
        WHERE del_flag = 0
        <if test="miniapp.miniappName != null and miniapp.miniappName != ''">
            AND miniapp_name = #{miniapp.miniappName}
        </if>
        <if test="miniapp.appid != null and miniapp.appid != ''">
            AND appid = #{miniapp.appid}
        </if>
        <if test="miniapp.secret != null and miniapp.secret != ''">
            AND secret = #{miniapp.secret}
        </if>
        <if test="miniapp.status != null and miniapp.status != ''">
            AND status = #{miniapp.status}
        </if>
        ORDER BY id DESC
    </select>
  • The implementation method of the initMiniappList interface is newly added to MiniappServiceImpl. The method of adding, deleting, checking and modifying the cache configuration is also implemented in this class. I will not go into details here. For more information, you can view the framework code.
    /**
     * 初始化微信小程序配置表列表
     * @return
     */
    @Override
    public void initMiniappList() {
        QueryMiniappDTO miniappDTO = new QueryMiniappDTO();
        miniappDTO.setStatus(String.valueOf(GitEggConstant.ENABLE));
        // 这里初始化所有的配置,不再只初始化已启用的配置
        List<MiniappDTO> miniappInfoList = miniappMapper.initMiniappList(miniappDTO);

        // 判断是否开启了租户模式,如果开启了,那么需要按租户进行分类存储
        if (enable) {
            Map<String, List<MiniappDTO>> miniappListMap =
                    miniappInfoList.stream().collect(Collectors.groupingBy(MiniappDTO::getAppid));
            miniappListMap.forEach((key, value) -> {
                String redisKey = MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + key;
                redisTemplate.delete(redisKey);
                addMiniapp(redisKey, value);
            });
        } else {
            redisTemplate.delete(MiniappConstant.WX_MINIAPP_CONFIG_KEY);
            addMiniapp(MiniappConstant.WX_MINIAPP_CONFIG_KEY, miniappInfoList);
        }
    }
  • Add a new call to initMiniappList in the InitExtensionCacheRunner system configuration loading class
/**
 * 容器启动完成加载扩展信息数据到缓存
 * @author GitEgg
 */
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Component
public class InitExtensionCacheRunner implements CommandLineRunner {
    
    private final IJustAuthConfigService justAuthConfigService;
    
    private final IJustAuthSourceService justAuthSourceService;

    private final IMailChannelService mailChannelService;

    private final IMiniappService miniappService;

    @Override
    public void run(String... args) {

        log.info("InitExtensionCacheRunner running");
    
    
        // 初始化第三方登录主配置
        justAuthConfigService.initJustAuthConfigList();

        // 初始化第三方登录 第三方配置
        justAuthSourceService.initJustAuthSourceList();

        // 初始化邮件配置信息
        mailChannelService.initMailChannelList();

        // 初始化微信配置信息
        miniappService.initMiniappList();

    }
}
2. To implement the method of dynamically selecting a tenant's WeChat applet interface, we need to extend the reading of database cache configuration information on the basis of ensuring that the original method of reading configuration files is still available. Therefore, the original configuration needs to be fully considered when implementing the interface. methods are available.
  • Obtain the assembled key value through appid. WxMaService stores the configuration configMap with appid as the key value by default. Here we modify the key value to the format of tenantId_appid as an extension of multi-tenancy. At the same time, when obtaining configuration information, we also need Pass in multi-tenant information.
  • If the tenant is passed to the front end, the tenant of the front end is used first. If the tenant is not passed, the tenant is queried from the system.
  • Get the configuration object from the cache. If the md5 configuration is different from the system configuration, you need to add it again.
  • If there is no cache configuration, it needs to be returned directly, because it may be configured in the configuration file.
  • Get the configuration tenants of all appids in the cache. If there are multiple tenants, an error will be prompted and let the front end select the tenant; if there is only one tenant, then return
    /**
     * 通过appid获取appid,忽略租户插件
     * @param miniappId
     * @return
     */
    @Override
    public String getMiniappId(String miniappId) {
        if (enable) {
            // 如果前端传了租户,那么先使用前端的租户,如果没有传租户,那么从系统中查询租户
            String tenantId = GitEggAuthUtils.getTenantId();
            if (!StringUtils.isEmpty(tenantId))
            {
                String miniappStr = (String) redisTemplate.opsForHash().get(MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + miniappId, tenantId);
                if (!StringUtils.isEmpty(miniappStr))
                {
                    // 转为系统配置对象
                    try {
                        // 从缓存获取配置对象,如果md5配置和系统配置不一样,那么需要重新add
                        MiniappDTO miniappDTO = JsonUtils.jsonToPojo(miniappStr, MiniappDTO.class);
                        return this.ifConfig(miniappDTO);
                    } catch (Exception e) {
                        log.error("获取微信小程序配置时发生异常:{}", e);
                        throw new BusinessException("获取微信小程序配置时发生异常。");
                    }
                }
                // 缓存配置中没有也需要直接返回,因为有可能是配置文件配置的
                return tenantId + StrPool.UNDERLINE + miniappId;
            } else {
                String redisKey = MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + miniappId;
                // 取缓存中所有appid的配置租户,如果存在多个租户,那么提示错误,让前端选择租户;如果只有一个租户,那么返回
                List<Object> values = redisTemplate.opsForHash().values(redisKey);
                if (!CollectionUtils.isEmpty(values))
                {
                    if (values.size() > GitEggConstant.Number.ONE)
                    {
                        throw new BusinessException("此小程序配置在多个租户下,请选择所需要操作的租户。");
                    }

                    String miniappConfig = (String) values.stream().findFirst().orElse(null);
                    try {
                        MiniappDTO miniappConfigDTO = JsonUtils.jsonToPojo(miniappConfig, MiniappDTO.class);
                        return this.ifConfig(miniappConfigDTO);
                    } catch (Exception e) {
                        log.error("获取缓存小程序配置失败:{}", e);
                        throw new BusinessException("小程序已被禁用,请联系管理员");
                    }
                }
                else
                {
                    return AuthConstant.DEFAULT_TENANT_ID + StrPool.UNDERLINE + miniappId;
                }

            }
        } else {
            return AuthConstant.DEFAULT_TENANT_ID + StrPool.UNDERLINE + miniappId;
        }
    }
3. Modify the original switching tenant method call. In WxMaUserController, change the original wxMaService.switchover(appid) to our own implementation miniappService.switchover(appid). Other calls to switch tenants are changed to this method.
        if (!miniappService.switchover(appid)) {
            throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
        }
4. The front-end code of the WeChat applet does not need to be modified. You can still call it according to the instructions in the previous chapter. Add appid to the request and tenant id to the request header.
import request from '@/common/utils/request'

const wechatLoginApi = {
  Login: '/extension/wx/user/'
}

export default wechatLoginApi

/**
 * 微信登录
 * @param {Object} appId
 * @param {Object} parameter
 */
export function wechatLogin (appId, parameter) {
  return request({
    url: wechatLoginApi.Login + appId + '/login',
    method: 'get',
    params: parameter
  })
}

/**
 * 获取微信信息
 * @param {Object} appId
 * @param {Object} parameter
 */
export function wechatInfo (appId, parameter) {
  return request({
    url: wechatLoginApi.Login + appId + '/info',
    method: 'get',
    params: parameter
  })
}

/**
 * 获取手机号
 * @param {Object} appId
 * @param {Object} parameter
 */
export function wechatPhone (appId, parameter) {
  return request({
    url: wechatLoginApi.Login + appId + '/phone',
    method: 'get',
    params: parameter
  })
}

  When modifying the configuration, you must pay attention to permission issues. Generally, the same WeChat applet is not allowed to be configured under different tenants because the appid is unique. After the WeChat applet is released, the WeChat applet is unique. Of course, there are special situations. For example, if the same applet serves as the same merchant management terminal for multiple tenants, then at this time, the user needs to choose to enter the tenant ID on the front end to determine which tenant the logged-in user belongs to, that is, multiple tenants share the same A WeChat applet.

GitEgg-Cloud is an enterprise-level microservice application development framework based on Spring Cloud integration. The open source project address is:

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg

Guess you like

Origin blog.csdn.net/wmz1932/article/details/131982820