05-使用Redis缓存数据,管理员相关数据表

使用Redis缓存数据

使用Redis可以提高查询效率,一定程度上可以减轻数据库服务器的压力,从而保护了数据库。

通常,应用Redis的场景有:

  • 高频查询,例如:热搜列表、秒杀
  • 改变频率低的数据,例如:商品类别

一旦使用Redis,就会导致Redis和数据库中都存在同样的数据,当数据发生变化时,可能出现不一致的问题!

所以,还有某些数据在特定的场景中不能使用Redis:

  • 要求数据必须是准确的:下单购买时要求库存是准确的
    • 如果每次库存发生变化时都更新了Redis中的库存值,保证了Redis中的数据是准确的,也可以使用
  • 数据的修改频率很高,且对数据准确性有一定要求

需要学会评估是否要求数据一定保持一致!

要使用Redis缓存数据,至少需要:

  • 开发新的组件,实现对Redis中的数据访问
    • 此组件并不是必须的,因为访问Redis数据的API都非常简单,自定义组件时,组件中的每个方法可能也只有少量代码,甚至只有1行代码
    • 如果直接将访问Redis的代码写在Service中,首次开发时会更省事,但不利于维护
    • 【推荐】如果将访问Redis的代码写的新的组件中,首次开发时会更麻烦,但利于维护
  • 在Service中调用新的组件,在Service中决定何时访问MySQL,何时访问Redis

在使用Redis之前,还必须明确一些问题:

  • 哪些查询功能改为从Redis中获取数据
  • Redis中的数据从哪里来

暂定目标:

  • 根据类别的id查询类别详情,改为从Redis中获取数据
  • 优先从Redis中获取数据,如果Redis中没有,则从MySQL中获取,且获取到数据后,将数据存入到Redis中,所以,经过首次查询后,Redis中将存在此数据,后续每一次都可以直接从Redis中拿到必要的数据

cn.tedu.csmall.product.webapi.repository创建ICategoryRedisRepository接口,并在接口中添加抽象方法:

public interface ICategoryRedisRepository {

    String KEY_CATEGORY_ITEM_PREFIX = "categories:item:";
    
    // 将类别详情存入到Redis中
    void save(CategoryDetailsVO category);
    
    // 根据类别id获取类别详情
    CategoryDetailsVO getDetailsById(Long id);
    
}

然后在cn.tedu.csmall.product.webapi.repository.impl创建CategoryRedisRepositoryImpl(接口的实现类),实现以上接口:

@Repository
public class CategoryRedisRepositoryImpl implements ICategoryRedisRepository {
    
    @Autowired
    private RedisTemplate<String, Serilizalbe> redisTemplate;
    
    @Override
    public void save(CategoryDetailsVO category) {
        String key = KEY_CATEGORY_ITEM_PREFIX + category.getId();
        redisTemplate.opsForValue().set(key, category);
    }
    
    @Override
    public CategoryDetailsVO getDetailsById(Long id) {
        String key = KEY_CATEGORY_ITEM_PREFIX + id;
        Serializable result = redisTemplate.opsForValue().get(key);
        if (result == null) {
            return null;
        } else {
            CategoryDetailsVO category = (CategoryDetailsVO) result;
            return category;
        }
    }
}

完成后,测试:

package cn.tedu.csmall.product.webapi.repository;

import cn.tedu.csmall.pojo.vo.CategoryDetailsVO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class CategoryRedisRepositoryTests {

    @Autowired
    ICategoryRedisRepository repository;

    @Test
    void testGetDetailsByIdSuccessfully() {
        testSave();

        Long id = 10L;
        CategoryDetailsVO category = repository.getDetailsById(id);
        Assertions.assertNotNull(category);
    }

    @Test
    void testGetDetailsByIdReturnNull() {
        Long id = -1L;
        CategoryDetailsVO category = repository.getDetailsById(id);
        Assertions.assertNull(category);
    }

    private void testSave() {
        CategoryDetailsVO category = new CategoryDetailsVO();
        category.setId(10L);
        category.setName("家用电器");

        Assertions.assertDoesNotThrow(() -> {
            repository.save(category);
        });
    }
}

然后,需要修改CategoryServiceImpl中的实现:

@Autowired
private ICategoryRedisRepository categoryRedisRepository;

@Override
public CategoryDetailsVO getDetailsById(Long id) {
    // ===== 以下是原有代码,只从数据库中获取数据 =====
    // CategoryDetailsVO category = categoryMapper.getDetailsById(id);
    // if (category == null) {
    //     throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND,
    //             "获取类别详情失败,尝试访问的数据不存在!");
    // }
    // return category;
    
    // ===== 以下是新的业务,将从Redis中获取数据 =====
    // 从repsotiroy中调用方法,根据id获取缓存的数据
    // 判断缓存中是否存在与此id对应的key
    // 有:表示明确的存入过某数据,此数据可能是有效数据,也可能是null
    // -- 判断此key对应的数据是否为null
    // -- 是:表示明确的存入了null值,则此id对应的数据确实不存在,则抛出异常
    // -- 否:表示明确的存入了有效数据,则返回此数据即可
    
    // 无:表示从未向缓存中写入此id对应的数据,在数据库中,此id可能存在数据,也可能不存在
    // 从mapper中调用方法,根据id获取数据库的数据
    // 判断从数据库中获取的结果是否为null
    // 是:数据库也没有此数据,先向缓存中写入错误数据(null),再抛出异常
    
    // 将从数据库中查询到的结果存入到缓存中
    // 返回查询结果
}

为了避免缓存穿透,需要在ICategoryRedisRepository中添加2个抽象方法:

/**
 * 判断是否存在id对应的缓存数据
 *
 * @param id 类别id
 * @return 存在则返回true,否则返回false
 */
boolean exists(Long id);

/**
 * 向缓存中写入某id对应的空数据(null),此方法主要用于解决缓存穿透问题
 *
 * @param id 类别id
 */
void saveEmptyValue(Long id);

并在CategoryRedisRepositoryImpl中补充实现:

@Override
public boolean exists(Long id) {
    String key = KEY_CATEGORY_ITEM_PREFIX + id;
    return redisTemplate.hasKey(key);
}

@Override
public void saveEmptyValue(Long id) {
    String key = KEY_CATEGORY_ITEM_PREFIX + id;
    redisTemplate.opsForValue().set(key, null);
}

业务中的具体实现为:

@Override
public CategoryDetailsVO getDetailsById(Long id) {
    // ===== 以下是原有代码,只从数据库中获取数据 =====
    // CategoryDetailsVO category = categoryMapper.getDetailsById(id);
    // if (category == null) {
    //     throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND,
    //             "获取类别详情失败,尝试访问的数据不存在!");
    // }
    // return category;

    // ===== 以下是新的业务,将从Redis中获取数据 =====
    log.debug("根据id({})获取类别详情……", id);
    // 从repository中调用方法,根据id获取缓存的数据
    // 判断缓存中是否存在与此id对应的key
    boolean exists = categoryRedisRepository.exists(id);
    if (exists) {
        // 有:表示明确的存入过某数据,此数据可能是有效数据,也可能是null
        // -- 判断此key对应的数据是否为null
        CategoryDetailsVO cacheResult = categoryRedisRepository.getDetailsById(id);
        if (cacheResult == null) {
            // -- 是:表示明确的存入了null值,则此id对应的数据确实不存在,则抛出异常
            log.warn("在缓存中存在此id()对应的Key,却是null值,则抛出异常", id);
            throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND,
                    "获取类别详情失败,尝试访问的数据不存在!");
        } else {
            // -- 否:表示明确的存入了有效数据,则返回此数据即可
            return cacheResult;
        }
    }

    // 缓存中没有此id匹配的数据
    // 从mapper中调用方法,根据id获取数据库的数据
    log.debug("没有命中缓存,则从数据库查询数据……");
    CategoryDetailsVO dbResult = categoryMapper.getDetailsById(id);
    // 判断从数据库中获取的结果是否为null
    if (dbResult == null) {
        // 是:数据库也没有此数据,先向缓存中写入错误数据,再抛出异常
        log.warn("数据库中也无此数据(id={}),先向缓存中写入错误数据", id);
        categoryRedisRepository.saveEmptyValue(id);
        log.warn("抛出异常");
        throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND,
                "获取类别详情失败,尝试访问的数据不存在!");
    }

    // 将从数据库中查询到的结果存入到缓存中
    log.debug("已经从数据库查询到匹配的数据,将数据存入缓存……");
    categoryRedisRepository.save(dbResult);
    // 返回查询结果
    log.debug("返回查询到数据:{}", dbResult);
    return dbResult;
}

许多缓存数据应该是服务器刚刚启动就直接写入到Redis中的,当后续客户端访问时,缓存中已经存在的数据可以直接响应,避免获取数据时缓存中还没有对应的数据,还需要从数据库中查询。

在服务器刚刚启动时就加载需要缓存的数据并写入到Redis中,这种做法称之为缓存预热。

需要解决的问题有:

  • 需要实现开机启动时自动执行某个任务
  • 哪些数据需要写入到缓存中,例如全部“类别”数据

在Spring Boot中,可以自定义某个组件类,实现ApplicationRunner即可,例如:

package cn.tedu.csmall.product.webapi.app;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class CachePreLoad implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("CachePreLoad.run()");
    }

}

为了将全部“类别”写入到缓存中,首先,需要能够从数据库中查询到全部数据,则需要:

  • CategoryMapper接口中添加:List<CategoryDetailsVO> list();
  • CategoryMapper.xml中配置以上抽象方法映射的SQL语句

然后,还需要实现将查询到的List<CategoryDetailsVO>写入到Redis中,则需要:

  • ICategoryRedisRepository接口中添加:void save(List<CategoryDetailsVO> categories);
  • CategoryRedisRepositoryImpl中实现以上方法
    • 存入时,Key值可以是:categories:list

由于向Redis中存入列表数据始终是“追加”的,且Redis中的数据并不会因为项目重启而消失,所以,如果反复启动项目,会在Redis的列表中反复追加重复的数据!为了避免此问题,应该在每次缓存预热之间先删除现有数据,所以,还需要:

  • ICategoryRedisRepository接口中添加:Boolean deleteList();
  • CategoryRedisRepositoryImpl中实现以上方法

从设计的角度,Service是可以调用数据访问层的组件的,即可以调用Mapper或其它Repository组件,换言之,Mapper和其它Repository组件应该只被Service调用

所以,应该在ICategoryService中定义“预热类别数据的缓存”的抽象方法:

void preloadCache();

另外,在Redis中存入了整个“类别”的列表后,也只能一次性拿到整个列表,不便于根据“类别”的id获取指定的数据,反之,如果每个“类别”数据都独立的存入到Redis中,当需要获取整个列表时,也只能把每个数据都找出来,然后再在Java程序中存入到List集合中,操作也是不方便的,所以,当需要更加关注效率时,应该将类别数据存2份到Redis中,一份是整个列表,另一份是若干个独立的类别数据。

目前,在缓存中存入独立的各个类别数据,在预热时并没有清除这些数据,如果在数据库中删除了数据,但缓存中的数据仍存在,为了避免这样的错误,应该在预热时,补充“删除所有类别”的功能!

则在ICategoryRedisRepository中添加void deleteAllItem();方法,用于删除所有独立的类别数据。

相关代码:ICategoryRedisRepository

package cn.tedu.csmall.product.webapi.repository;

import cn.tedu.csmall.pojo.vo.CategoryDetailsVO;

import java.util.List;

public interface ICategoryRedisRepository {

    /**
     * 类别数据的KEY的前缀
     */
    String KEY_CATEGORY_ITEM_PREFIX = "categories:item:";
    /**
     * 类别列表的KEY
     */
    String KEY_CATEGORY_LIST = "categories:list";

    /**
     * 判断是否存在id对应的缓存数据
     *
     * @param id 类别id
     * @return 存在则返回true,否则返回false
     */
    Boolean exists(Long id);

    /**
     * 向缓存中写入某id对应的空数据(null),此方法主要用于解决缓存穿透问题
     *
     * @param id 类别id
     */
    void saveEmptyValue(Long id);

    /**
     * 将类别详情存入到Redis中
     *
     * @param category 类别详情
     */
    void save(CategoryDetailsVO category);

    /**
     * 将类别的列表存入到Redis中
     *
     * @param categories 类别列表
     */
    void save(List<CategoryDetailsVO> categories);

    /**
     * 删除Redis中各独立存储的类别数据
     */
    void deleteAllItem();

    /**
     * 删除Redis中的类别列表
     * @return 如果成功删除,则返回true,否则返回false
     */
    Boolean deleteList();

    /**
     * 根据类别id获取类别详情
     *
     * @param id 类别id
     * @return 匹配的类别详情,如果没有匹配的数据,则返回null
     */
    CategoryDetailsVO getDetailsById(Long id);

}

相关代码:CategoryRedisRepositoryImpl

package cn.tedu.csmall.product.webapi.repository.impl;

import cn.tedu.csmall.pojo.vo.CategoryDetailsVO;
import cn.tedu.csmall.product.webapi.repository.ICategoryRedisRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Repository
public class CategoryRedisRepositoryImpl implements ICategoryRedisRepository {

    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    @Override
    public Boolean exists(Long id) {
        String key = KEY_CATEGORY_ITEM_PREFIX + id;
        return redisTemplate.hasKey(key);
    }

    @Override
    public void saveEmptyValue(Long id) {
        String key = KEY_CATEGORY_ITEM_PREFIX + id;
        redisTemplate.opsForValue().set(key, null, 30, TimeUnit.SECONDS);
    }

    @Override
    public void save(CategoryDetailsVO category) {
        String key = KEY_CATEGORY_ITEM_PREFIX + category.getId();
        redisTemplate.opsForValue().set(key, category);
    }

    @Override
    public void save(List<CategoryDetailsVO> categories) {
        for (CategoryDetailsVO category : categories) {
            redisTemplate.opsForList().rightPush(KEY_CATEGORY_LIST, category);
        }
    }

    @Override
    public void deleteAllItem() {
        Set<String> keys = redisTemplate.keys(KEY_CATEGORY_ITEM_PREFIX + "*");
        redisTemplate.delete(keys);
    }

    @Override
    public Boolean deleteList() {
        return redisTemplate.delete(KEY_CATEGORY_LIST);
    }

    @Override
    public CategoryDetailsVO getDetailsById(Long id) {
        String key = KEY_CATEGORY_ITEM_PREFIX + id;
        Serializable result = redisTemplate.opsForValue().get(key);
        if (result == null) {
            return null;
        } else {
            CategoryDetailsVO category = (CategoryDetailsVO) result;
            return category;
        }
    }
}

相关代码:缓存预热的业务代码(以下方法的声明在ICategoryService接口中,以下代码是CategoryServiceImpl中重写的方法):

@Override
public void preloadCache() {
    log.debug("删除缓存中的类别列表……");
    categoryRedisRepository.deleteList();
    log.debug("删除缓存中的各独立的类别数据……");
    categoryRedisRepository.deleteAllItem();

    log.debug("从数据库查询类别列表……");
    List<CategoryDetailsVO> list = categoryMapper.list();

    for (CategoryDetailsVO category : list) {
        log.debug("查询结果:{}", category);
        log.debug("将当前类别存入到Redis:{}", category);
        categoryRedisRepository.save(category);
    }

    log.debug("将类别列表写入到Redis……");
    categoryRedisRepository.save(list);
    log.debug("将类别列表写入到Redis完成!");
}

相关代码:缓存预热类(CachePreLoad):

package cn.tedu.csmall.product.webapi.app;

import cn.tedu.csmall.product.service.ICategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class CachePreLoad implements ApplicationRunner {

    @Autowired
    private ICategoryService categoryService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("CachePreLoad.run()");
        log.debug("准备执行缓存预热……");

        categoryService.preloadCache();

        log.debug("缓存预热完成!");
    }

}

管理员相关数据表

管理员及权限的管理,涉及的数据表有:

-- 数据库:mall_ams

-- 权限表:创建数据表
drop table if exists ams_permission;
create table ams_permission (
    id bigint unsigned auto_increment,
    name varchar(50) default null comment '名称',
    value varchar(255) default null comment '值',
    description varchar(255) default null comment '描述',
    sort tinyint unsigned default 0 comment '自定义排序序号',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '权限表' charset utf8mb4;

-- 权限表:插入测试数据
insert into ams_permission (name, value, description) values
('商品-商品管理-读取', '/pms/product/read', '读取商品数据,含列表、详情、查询等'),
('商品-商品管理-编辑', '/pms/product/update', '修改商品数据'),
('商品-商品管理-删除', '/pms/product/delete', '删除商品数据'),
('后台管理-管理员-读取', '/ams/admin/read', '读取管理员数据,含列表、详情、查询等'),
('后台管理-管理员-编辑', '/ams/admin/update', '编辑管理员数据'),
('后台管理-管理员-删除', '/ams/admin/delete', '删除管理员数据');

-- 角色表:创建数据表
drop table if exists ams_role;
create table ams_role (
    id bigint unsigned auto_increment,
    name varchar(50) default null comment '名称',
    description varchar(255) default null comment '描述',
    sort tinyint unsigned default 0 comment '自定义排序序号',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '角色表' charset utf8mb4;

-- 角色表:插入测试数据
insert into ams_role (name) values
    ('超级管理员'), ('系统管理员'), ('商品管理员'), ('订单管理员');

-- 角色权限关联表:创建数据表
drop table if exists ams_role_permission;
create table ams_role_permission (
    id bigint unsigned auto_increment,
    role_id bigint unsigned default null comment '角色id',
    permission_id bigint unsigned default null comment '权限id',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '角色权限关联表' charset utf8mb4;

-- 角色权限关联表:插入测试数据
insert into ams_role_permission (role_id, permission_id) values
    (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6),
    (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6),
    (3, 1), (3, 2), (3, 3);

-- 管理员表:创建数据表
drop table if exists ams_admin;
create table ams_admin (
    id bigint unsigned auto_increment,
    username varchar(50) default null unique comment '用户名',
    password char(64) default null comment '密码(密文)',
    nickname varchar(50) default null comment '昵称',
    avatar varchar(255) default null comment '头像URL',
    phone varchar(50) default null unique comment '手机号码',
    email varchar(50) default null unique comment '电子邮箱',
    description varchar(255) default null comment '描述',
    is_enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用',
    last_login_ip varchar(50) default null comment '最后登录IP地址(冗余)',
    login_count int unsigned default 0 comment '累计登录次数(冗余)',
    gmt_last_login datetime default null comment '最后登录时间(冗余)',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '管理员表' charset utf8mb4;

-- 管理员表:插入测试数据
insert into ams_admin (username, password, nickname, email, description, is_enable) values
    ('root', '1234', 'root', 'root@tedu.cn', '最高管理员', 1),
    ('super_admin', '1234', 'administrator', 'admin@tedu.cn', '超级管理员', 1),
    ('nobody', '1234', '无名', 'liucs@tedu.cn', null, 0);

-- 管理员角色关联表:创建数据表
drop table if exists ams_admin_role;
create table ams_admin_role (
    id bigint unsigned auto_increment,
    admin_id bigint unsigned default null comment '管理员id',
    role_id bigint unsigned default null comment '角色id',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '管理员角色关联表' charset utf8mb4;

-- 管理员角色关联表:插入测试数据
insert into ams_admin_role (admin_id, role_id) values
    (1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (2, 4), (3, 3);

-- 查询示例:查询id=1的管理员的权限
select distinct ams_permission.value from ams_permission
left join ams_role_permission on ams_role_permission.permission_id=ams_permission.id
left join ams_role on ams_role_permission.role_id=ams_role.id
left join ams_admin_role on ams_admin_role.role_id=ams_role.id
left join ams_admin on ams_admin_role.admin_id=ams_admin.id
where ams_admin.id=1
order by ams_permission.value;

-- 管理员登录日志表:创建数据表
drop table if exists ams_login_log;
create table ams_login_log (
    id bigint unsigned auto_increment,
    admin_id bigint unsigned default null comment '管理员id',
    username varchar(50) default null comment '管理员用户名(冗余)',
    nickname varchar(50) default null comment '管理员昵称(冗余)',
    ip varchar(50) default null comment '登录IP地址',
    user_agent varchar(255) default null comment '浏览器内核',
    gmt_login datetime default null comment '登录时间',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '管理员登录日志表' charset utf8mb4;

-- 管理员登录日志表:插入测试数据
insert into ams_login_log (admin_id, username, nickname, ip, user_agent, gmt_login) values
    (1, 'root', 'root', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15', DATE_SUB(NOW(), interval 1 day)),
    (2, 'root', 'root', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15', DATE_SUB(NOW(), interval 12 hour)),
    (3, 'root', 'root', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15', NOW());

-- 查看数据表结构
desc ams_permission; desc ams_role; desc ams_role_permission; desc ams_admin; desc ams_admin_role; desc ams_login_log;

当某个管理员尝试登录时,必须实现”根据用户名查询此管理员的信息,至少包括id、密码、权限“,需要执行的SQL语句大致是:

-- 管理员表 admin
-- 角色表 role
-- 管理员与角色关联表 admin_role (admin_id, role_id)
-- 权限表 permission
-- 角色与权限关联表 role_permission (role_id, permission_id)
-- 【根据用户名查询管理员,且必须查出对应的权限】
select
    ams_admin.id,
    ams_admin.username,
    ams_admin.password,
    ams_admin.is_enable,
    ams_permission.value
from ams_admin
left join ams_admin_role on ams_admin.id = ams_admin_role.admin_id
left join ams_role_permission on ams_admin_role.role_id = ams_role_permission.role_id
left join ams_permission on ams_role_permission.permission_id = ams_permission.id
where username='root';

接下来,在根项目中创建csmall-admin模块(与csmall-product类似),并在其下创建csmall-admin-servicecsmall-admin-webapi这2个子模块(与csmall-product的2个子模块类似),然后,尽量在csmall-admin-webapi中实现以上查询功能:

public interface AdminMapper {
    AdminLoginVO findByUsername(String username);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青木编码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值