当前位置: 首页 > article >正文

【Go】十六、protobuf构建基础服务信息、grpc服务启动的基础信息

商品服务

服务结构

创建 goods 服务,将之前 user 服务的基本结构迁移到 goods 服务上,完整目录是:

mxshop_srvs

user_srv

tmp

goods_srv

config

config.go 配置的读取表

global

global.go 数据库、日志初始化、全局变量定义

handler

xxx.go 拦截器

initialize

xxx.go 初始化信息

model

xxx.go 数据库表、数据库对象

proto proto 相关信息

xxx.proto

xxx.pb.go

xxx_grpc.pb.go

tests 测试信息

utils

xxx.go 工具

config-debug.yaml 配置信息

main.go 启动类

数据表结构

在 model 目录中创建需要的表结构对应的数据对象:

  • 创建基础数据类:base.go:
package model

import (
	"gorm.io/gorm"
	"time"
)

type BaseModel struct {
	ID        int32     `gorm:"primarykey";type:int` // 注意这里对应数据库的int,我们进行统一定义,避免出现问题,若数据量过大也可以采用bigint
	CreatedAt time.Time `gorm:"column:add_time"`
	UpdatedAt time.Time `gorm:"column:update_time"`
	DeletedAt gorm.DeletedAt
	IsDeleted bool
}

  • 创建商品表
package model

// 商品分类数据对象:一级分类、二级分类...
type Category struct {
	BaseModel
	Name             string    `gorm:"type:varchar(20);not null;"` // 分类名
	ParentCategoryID int32     // 父分类ID
	ParentCategory   *Category // 父分类对象	此处因为是自己指向自己,必须使用指针
	Level            int32     `gorm:"type:int;not null;default:1"` // 分类级别
	IsTab            bool      `gorm:"default:false;not null"`      // 是否显示在 Tab 栏
}

// 品牌数据对象
type Brands struct {
	BaseModel
	Name string `gorm:"type:varchar(20);not null"`
	Logo string `gorm:"type:varchar(200);default:'';not null"`
}

// 品牌 / 类型 对应表
type GoodsCategoryBrand struct {
	BaseModel
	CategoryID int32 `gorm:"type:int;index:idx_category_brand,unique"`
	Category   Category
	BrandsID   int32 `gorm:"type:int;index:idx_category_brand,unique"`
	Brands     Brands
}

// 轮播图数据对象
type Banner struct {
	BaseModel
	Image string `gorm:"type:varchar(200);not null"`
	Url   string `gorm:"type:varchar(200);not null"`
	Index int32  `gorm:"type:int;default:1;not null"`
}

  • 商品对象的创建,注意:商品对象的创建较为复杂,单独拎出来处理:
// 商品表
type Goods struct {
	BaseModel

	CategoryID int32 `gorm:"type:int;not null"`
	Category   Category
	BrandsID   int32 `gorm:"type:int;not null"`
	Brands     Brands

	OnSale   bool `gorm:"default:false;not null"` // 是否上架
	ShipFree bool `gorm:"default:false;not null"` // 是否xxx
	ISNew    bool `gorm:"default:false;not null""`
	IsHot    bool `gorm:"default:false;not null"`

	Name            string   `gorm:"type:varchar(50);not null"`
	GoodsSn         string   `gorm:"type:varchar(50);not null"`
	ClickNum        int32    `gorm:"type:int;default:0;not null"`
	SoldNum         int32    `gorm:"type:int;default:0;not null"`
	FavNum          int32    `gorm:"type:int;default:0;not null"`
	MarketPrice     float32  `gorm:"not null"`
	ShopPrice       float32  `gorm:"not null"`
	GoodsBrief      string   `gorm:"type:varchar(100);not null"`
	Images          GormList `gorm:"type:varchar(1000);not null"`
	DescImages      GormList `gorm:"type:varchar(1000);not null"`
	GoodsFrontImage string   `gorm:"type:varchar(200);not null"`
}

这里需要注意的是 对于 图片列表的处理,我们单独存储一个图片是没问题的,但是如果需要存储多个图片的话,我们就有两种方式选择了:

  1. 建立一个图片表,表里是所有的图片,每个图片存储一个归属商品,但这样的缺陷是无法避免连表操作,到后期数据量极大的时候,这种程度的连表能够造成极大的性能隐患。
  2. 直接将图片路径形成 json 格式的字符串,存储在表中,在 代码中通过 marshal 和 unmarshal 进行编码和解码,再进行图片的存取,这种方式有效规避了连表带来的性能损耗。

故而这里选用第二种方式。

这里就需要我们在 model/Base.go 中添加编解码的工具代码

type GormList []string

// 设定这种变量在插入数据库的时候怎么插入:
// 这里是 将json格式的内容转换为 字符串再进行插入
func (g GormList) Value() (driver.Value, error) {
	return json.Marshal(g)
}

// 设定在从数据库中取数据时,自动将数据转换为 []string 的列表
// 从数据库中取出来的时候是 GormList 数据类型,并将它的地址传入这个方法,直接修改其地址中的内容,将其修改为 []string
func (g *GormList) Scan(value interface{}) error {
	return json.Unmarshal(value.([]byte), &g)
}

type BaseModel struct {
	ID        int32     `gorm:"primarykey";type:int` // 注意这里对应数据库的int,我们进行统一定义,避免出现问题,若数据量过大也可以采用bigint
	CreatedAt time.Time `gorm:"column:add_time"`
	UpdatedAt time.Time `gorm:"column:update_time"`
	DeletedAt gorm.DeletedAt
	IsDeleted bool
}

这样我们存入的数据或者取出的数据只要是 定义为了 GormList 格式,就会在存入时自动转为字符串,取出是自动转为 json

之后进行数据库创建操作

这里我们默认数据库自行创建完成了,利用 gorm 来建表:

goods_srv/model/main/main.go

package main

import (
	"log"
	"os"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"

	"mxshop_srvs/goods_srv/model"
)

// 这里的代码是用来在数据库中建表的
func main() {
	dsn := "root:123456@tcp(192.168.202.140:3306)/mxshop_goods_srv?charset=utf8mb4&parseTime=True&loc=Local"

	// 添加日志信息
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold:             time.Second, // Slow SQL threshold
			LogLevel:                  logger.Info, // Log level
			IgnoreRecordNotFoundError: true,        // Ignore ErrRecordNotFound error for logger
			ParameterizedQueries:      true,        // Don't include params in the SQL log
			Colorful:                  true,        // Disable color,true is colorful, false to black and white
		},
	)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		// 阻止向创建的数据库表后添加复数
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
		// 将日添加到配置中
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}

	// 建表
	_ = db.AutoMigrate(&model.Category{}, &model.Brands{}, &model.GoodsCategoryBrand{}, &model.Banner{}, &model.Goods{})
}

这里相当于是一个商品的单元测试,用来做一些一次性的事情

protobuf 数据定义、定义所有的接口和请求和返回信息

注意这里,一定是先确定好需要的所有的需要的接口信息,再进行后续的接口定义操作

下面是全量的详细proto信息:

syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";

// 在这里定义一个个的接口
service Goods {
  // 商品部分
  // 获取商品的接口,包括条件获取
  rpc GoodsList(GoodsFilterRequest) returns(GoodsListResponse);
  // 批量查询商品信息的接口,避免查商品时发生一个一个调用服务、一条一条查的低效情况
  rpc BatchGetGoods(BatchGoodsIdInfo) returns(GoodsListResponse);
  // 添加商品
  rpc CreateGoods(CreateGoodsInfo) returns(GoodsInfoResponse);
  // 删除商品,没有明确需要返回的信息,返回一个占位符
  rpc DeleteGoods(DeleteGoodsInfo) returns(google.protobuf.Empty);
  // 更新商品信息
  rpc UpdateGoods(CreateGoodsInfo) returns(google.protobuf.Empty);
  // 获取商品信息(单独获取)
  rpc GetGoodsDetail(GoodInfoRequest) returns(GoodsInfoResponse);

  // 分类部分
  // 获取所有商品分类
  rpc GetAllCategorysList(google.protobuf.Empty) returns(CategoryListResponse);
  // 获取子分类 todo 把这个补齐,文件在桌面上,视频已经看完了
  rpc GetSubCategory(CategoryListRequest) returns(SubCategoryListResponse);
  rpc CreateCategory(CategoryInfoRequest) returns(CategoryInfoResponse);
  rpc DeleteCategory(DeleteCategoryRequest) returns(google.protobuf.Empty);
  rpc UpdateCategory(CategoryInfoRequest) returns(google.protobuf.Empty);

  // 品牌部分
  rpc BrandList(BrandFilterRequest) returns(BrandListResponse);
  rpc CreateBrand(BrandRequest) returns(BrandInfoResponse);
  rpc DeleteBrand(BrandRequest) returns(google.protobuf.Empty);
  rpc UpdateBrand(BrandRequest) returns(google.protobuf.Empty);

  // 轮播图部分
  rpc BannerList(google.protobuf.Empty) returns(BannerListResponse);
  rpc CreateBanner(BannerRequest) returns(BannerResponse);
  rpc DeleteBranner(BannerRequest) returns(google.protobuf.Empty);
  rpc UpdateBanner(BannerRequest) returns(google.protobuf.Empty);

  // 品牌分类信息
  // 过滤需要的品牌、分类信息
  rpc CategoryBrandList(CategoryBrandFilterRequest) returns(CategoryBrandListResponse);
  // 获取某个分类下所有品牌的接口
  rpc GetCategoryBrandList(CategoryInfoRequest) returns(BrandListResponse);
  rpc CreateCategoryBrand(CategoryBrandRequest) returns(CategoryBrandResponse);
  rpc DeleteCategoryBrand(CategoryBrandRequest) returns(google.protobuf.Empty);
  rpc UpdateCategoryBrand(CategoryBrandRequest) returns(google.protobuf.Empty);
}

// 在过滤商品时传入的条件信息
message GoodsFilterRequest {
  int32 priceMin = 1;
  int32 priceMax = 2;
  bool isHot = 3;
  bool isNew = 4;
  bool isTab = 5;
  int32 topCategory = 6;
  int32 pages = 7;
  int32 pagePerNums = 8;
  string keyWords = 9;
  int32 brand = 10;
}

// 单独的一条关联信息
message CategoryBrandRequest{
  int32 id = 1;
  int32 categoryId = 2;
  int32 brandId = 3;
}

// 返回的品牌、分类信息集合、也就是联系信息
message CategoryBrandListResponse {
  int32 total = 1;
  repeated CategoryBrandResponse data = 2;
}

// 返回一个品牌信息、一个分类信息
message CategoryBrandResponse{
  int32 id = 1;
  BrandInfoResponse brand = 2;
  CategoryInfoResponse category = 3;
}

// 轮播图的返回结果
message BannerListResponse {
  int32 total = 1;
  repeated BannerResponse data = 2;
}

// 过滤品牌、分类信息请求
message CategoryBrandFilterRequest  {
  int32 pages = 1;
  int32 pagePerNums = 2;
}

// 单个轮播图
message BannerResponse {
  int32 id = 1;
  int32 index = 2;
  string image = 3;
  string url = 4;
}

// 单个轮播图的请求
message BannerRequest {
  int32 id = 1;
  int32 index = 2;
  string image = 3;
  string url = 4;
}

// 过滤品牌请求的信息
message BrandFilterRequest {
  int32 pages = 1;
  int32 pagePerNums = 2;
}

// 品牌查询请求
message BrandRequest {
  int32 id = 1;
  string name = 2;
  string logo = 3;
}

// 创建分类的请求信息
message CategoryInfoRequest {
  int32 id = 1;
  string name = 2;
  int32 parentCategory = 3;
  int32 level = 4;
  bool isTab = 5;
}

// 传入删除信息的ID
message DeleteCategoryRequest {
  int32 id = 1;
}

// 商品ID 列表,便于批量查询
message BatchGoodsIdInfo {
  repeated int32 id = 1;
}

// 获取分类信息集合
message CategoryListResponse {
  int32 total = 1;
  repeated CategoryInfoResponse data = 2;
  string jsonData = 3;
}

// 获取子分类集合(需要传入选中分类的id,level选传)
message CategoryListRequest {
  int32 id = 1;
  int32 level = 2;
}

// 子分类的返回
message SubCategoryListResponse {
  int32 total = 1;
  CategoryInfoResponse info = 2;    // 将本分类的所有信息返回
  repeated CategoryInfoResponse subCategorys = 3;  // 将子分类的所有信息返回
}

// 分类信息
message CategoryInfoResponse {
  int32 id = 1;
  string name = 2;
  int32 parentCategory = 3;
  int32 level = 4;
  bool isTab = 5;
}

// 获取单独商品详情
message GoodInfoRequest {
  int32 id = 1;
}

// 商品列表的返回信息
message GoodsListResponse {
  int32 total = 1;
  repeated GoodsInfoResponse data = 2;
}

// 单个商品的信息
message GoodsInfoResponse {
  int32 id = 1;
  int32 categoryId = 2;
  string name = 3;
  string goodsSn = 4;
  int32 clickNum = 5;
  int32 soldNum = 6;
  int32 favNum = 7;
  float marketPrice = 9;
  float shopPrice = 10;
  string goodsBrief = 11;
  string goodsDesc = 12;
  bool shipFree = 13;
  repeated string images = 14;
  repeated string descImages = 15;
  string goodsFrontImage = 16;
  bool isNew = 17;
  bool isHot = 18;
  bool onSale = 19;
  int64 addTime = 20;
  CategoryBriefInfoResponse category = 21;
  BrandInfoResponse brand = 22;
}

// 删除时传入一个ID
message DeleteGoodsInfo {
  int32 id = 1;
}

// 创建商品去要传递的信息
message CreateGoodsInfo {
  int32 id = 1;
  string name = 2;
  string goodsSn = 3;
  int32 stocks = 7;
  float marketPrice = 8;
  float shopPrice = 9;
  string goodsBrief = 10;
  string goodsDesc = 11;
  bool shipFree = 12;
  repeated string images = 13;
  repeated string descImages = 14;
  string goodsFrontImage = 15;
  bool isNew = 16;
  bool isHot = 17;
  bool onSale = 18;
  int32 categoryId = 19;
  int32 brandId = 20;
}

// 商品分类的简要信息
message CategoryBriefInfoResponse {
  int32 id = 1;
  string name = 2;
}

message CategoryFilterRequest {
  int32 id = 1;
  string name = 2;
}

// 品牌单个信息
message BrandInfoResponse {
  int32 id = 1;
  string name = 2;
  string logo = 3;
}

// 品牌列表信息
message BrandListResponse {
  int32 total = 1;
  repeated BrandInfoResponse data = 2;
}

protobuf 文件的生成

在对应的文件夹目录下输入:

// 标准版
protoc --go_out=. xxxx.proto
// gprc 定制版
protoc --go_out=. --go-grpc_out=. *.proto

就可以在当前目录下创建好我们所需要的 xxxx.pb.go 文件,这个文件就是我们的以proto 作为传输协议的正式接口文件。

注意:此命令需要 protoc 环境完善,并配置好完整的环境变量(protoc 的环境变量)

protobuf 的构建

此时,我们发现,我们的接口过于多了,这就需要我们分开进行,我们在 .pb.go 文件中找到: GoodsServer,这里面定义的就是所有的接口,我们在 handler 文件中将所有的接口进行定义:

handler

banner.go

brands.go

goods.go

category.go

category_brand.go

示例定义(goods.go):

测试定义:若我们希望进行快速测试,就可以给 自己的 GoodsServer 添加一个属性,有这个属性存在,就可以进行服务器快速测试。

package handler

import (
	"mxshop_srvs/goods_srv/proto"
)

type GoodsServer struct {
	proto.UnimplementedGoodsServer
}

记得修改 main文件中的 注册:

	proto.RegisterGoodsServer(server, &handler.GoodsServer{})

之后进行Nacos 相关配置:
创建一个新的命名空间、添加文件:goods-srv.json :

{
    "name": "goods-srv",
    "tags": ["imooc", "bobby", "goods", "srv"],
    "web-host": "192.168.10.108",
    "mysql": {
        "host": "192.168.202.140",
        "port": 3306,
        "db": "mxshop_goods_srv",
        "user": "root",
        "password": "123456"
    },
    "consul": {
        "host": "192.168.202.140",
        "port": 8500
    }
}

之后修改启动的配置文件的命名空间的ID

记得同步修改 config.go 中,新添加了一个 Tags 标签

type ServerConfig struct {

	// 这里是为了配置服务端口使用的,后期会移植到
	//Host       string       `mapstruce:"host" json:"host"`
	//Port       int          `mapstruct:"port" json:"port"`
	Name       string       `mapstructure:"name" json:"name"`
	Tags       []string     `mapstructure:"tags" json:"tags"`
	MysqlInfo  MysqlConfig  `mapstructure:"mysql" json:"mysql"`
	ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"`
	WebHost    string       `json:"web-host"`
}

然后在 main 中读取:

registration.Tags = global.ServerConfig.Tags

GRPC服务启动的相关信息

main.go:

package main

import (
	"flag"
	"fmt"
	"go.uber.org/zap"
	"mxshop_srvs/goods_srv/global"
	"mxshop_srvs/goods_srv/initialize"
	"mxshop_srvs/goods_srv/utils"
	"net"
	"os"
	"os/signal"
	"syscall"

	"github.com/hashicorp/consul/api"
	"github.com/satori/go.uuid"
	"google.golang.org/grpc"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/health/grpc_health_v1"

	"mxshop_srvs/goods_srv/handler"
	"mxshop_srvs/goods_srv/proto"
)

func main() {
	// 由于ip和端口号有可能需要用户输入,所以这里摘出来
	// flag 包是一个命令行工具包,允许从命令行中设置参数
	IP := flag.String("ip", "0.0.0.0", "ip地址")
	Port := flag.Int("port", 0, "端口号")

	initialize.InitLogger()
	initialize.InitConfig()

	flag.Parse()
	fmt.Println("ip: ", *IP)

	// 设置端口号自动获取
	if *Port == 0 {
		*Port, _ = utils.GetFreePort()
	}

	fmt.Println("port: ", *Port)

	// 创建新服务器
	server := grpc.NewServer()
	// 注册自己的已实现的方法进来
	proto.RegisterGoodsServer(server, &handler.GoodsServer{})

	//lis, err := net.Listen("tcp", fmt.Sprintf("192.168.202.140:8021"))
	lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
	if err != nil {
		panic("failed to listen" + err.Error())

	}

	// 绑定服务健康检查
	grpc_health_v1.RegisterHealthServer(server, health.NewServer())

	// 服务注册
	cfg := api.DefaultConfig()
	cfg.Address = fmt.Sprintf("%s:%d", global.ServerConfig.ConsulInfo.Host, global.ServerConfig.ConsulInfo.Port)

	client, err := api.NewClient(cfg)
	if err != nil {
		panic(err)
	}

	check := &api.AgentServiceCheck{
		GRPC:     fmt.Sprintf("%s:%d", global.ServerConfig.Host, *Port),
		Interval: "5s",
		//Timeout:                        "10s",
		DeregisterCriticalServiceAfter: "30s",
	}

	registration := new(api.AgentServiceRegistration)
	registration.Address = global.ServerConfig.Host
	//registration.Address = "127.0.0.1"
	//registration.ID = global.ServerConfig.Name		// 此处修改为使用 UUID 生成
	serviceID := fmt.Sprintf("%s", uuid.NewV4()) // 此处修改为使用 UUID 生成
	registration.ID = serviceID
	registration.Port = *Port
	registration.Tags = global.ServerConfig.Tags
	registration.Name = global.ServerConfig.Name
	registration.Check = check

	err = client.Agent().ServiceRegister(registration)
	if err != nil {
		panic(err)
	}

	//err = server.Serve(lis)

	// 注意此处是阻塞式的所以需要一个 goroutine 来进行异步操作
	// 将自己的服务绑定端口
	go func() {
		err = server.Serve(lis)
		if err != nil {
			panic("fail to start grpc" + err.Error())
		}
	}()

	// 创建一个通道
	quit := make(chan os.Signal)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	// 阻塞住,若接到请求则放通,直接将服务注销
	<-quit
	if err = client.Agent().ServiceDeregister(serviceID); err != nil {
		zap.S().Info("注销失败...")
	}
	zap.S().Info("注销成功")
}

InitConfig:(配置文件的相关信息)

intialize/config.go:

package initialize

import (
	"encoding/json"
	"fmt"
	"github.com/nacos-group/nacos-sdk-go/clients"
	"github.com/nacos-group/nacos-sdk-go/vo"

	"github.com/nacos-group/nacos-sdk-go/common/constant"
	"github.com/spf13/viper"
	"go.uber.org/zap"

	"mxshop_srvs/goods_srv/global"
)

func GetEnvInfo(env string) bool {
	viper.AutomaticEnv()
	var rs bool
	rs = viper.GetBool(env)
	return rs
	return true
}

func InitConfig() {
	debug := GetEnvInfo("MXSHOP-DEBUG")
	zap.S().Info(fmt.Sprintf("------------", debug))
	configFileNamePrefix := "config"
	configFileName := fmt.Sprintf("goods_srv/%s-pro.yaml", configFileNamePrefix)
	if debug {
		configFileName = fmt.Sprintf("goods_srv/%s-debug.yaml", configFileNamePrefix)
	}
	v := viper.New()
	v.SetConfigFile(configFileName)
	if err := v.ReadInConfig(); err != nil {
		panic(err)
	}

	// 将配置文件进行解析
	if err := v.Unmarshal(&global.NacosConfig); err != nil {
		panic(err)
	}

	sc := []constant.ServerConfig{
		{
			IpAddr: global.NacosConfig.Host,
			Port:   global.NacosConfig.Port,
		},
	}

	cc := constant.ClientConfig{
		TimeoutMs:           5000,
		NamespaceId:         global.NacosConfig.Namespace,
		CacheDir:            "tmp/nacos/cache",
		NotLoadCacheAtStart: true,
		LogDir:              "tmp/nacos/log",
		LogLevel:            "debug",
	}

	configClient, err := clients.CreateConfigClient(map[string]interface{}{
		"serverConfigs": sc,
		"clientConfig":  cc,
	})
	if err != nil {
		zap.S().Fatalf("%s", err.Error())
	}

	content, err := configClient.GetConfig(vo.ConfigParam{
		DataId: global.NacosConfig.Dataid,
		Group:  global.NacosConfig.Group,
	})
	if err != nil {
		zap.S().Fatalf("%s", err.Error())
	}

	err = configClient.ListenConfig(vo.ConfigParam{
		DataId: global.NacosConfig.Dataid,
		Group:  global.NacosConfig.Group,
		OnChange: func(namespace, group, dataId, data string) {
			fmt.Println("配置文件发生变化")
			fmt.Println("namespace: " + namespace)
			fmt.Println("group: " + group)
			fmt.Println("dataId: " + dataId)
			fmt.Println("data: " + data)
		},
	})
	if err != nil {
		zap.S().Fatalf("%s", err.Error())
	}

	err = json.Unmarshal([]byte(content), &global.ServerConfig)
	if err != nil {
		zap.S().Fatalf("%s", err.Error())
	}
	zap.S().Info(global.ServerConfig)
}

此处还需要注意配置文件和 Nacos 的配置文件:

config-debug.yml

host: '192.168.202.140'
port: 8848
namespace: '043d2547-bd1e-44df-b097-75f649848099'
user: 'nacos'
password: 'nacos'
dataid: 'goods-srv.json'
group: 'dev'

Nacos配置:

{
    "name": "goods-srv",
    "host": "192.168.10.107",
    "mysql": {
        "host": "192.168.202.140",
        "port": 3306,
        "db": "mxshop_user_srv",
        "user": "root",
        "password": "123456"
    },
    "consul": {
        "host": "192.168.202.140",
        "port": 8500
    }
}

http://www.kler.cn/a/565822.html

相关文章:

  • Flutter系列教程之(4)——自定义Widget控件及相关知识
  • LeetCode 2656 K个元素的最大和
  • 力扣——不同路径
  • 算法日记31:leetcode341整数拆分(DFS->记忆化->DP)
  • deepseek 和chatgpt的论文降重方法有哪些?
  • 大模型最新面试题系列:训练篇之分布式训练
  • go设计模式
  • 【压力测试】
  • 自然语言处理NLP入门 -- 第五节词向量与嵌入
  • Ollama download DeepSeek Local Install
  • 如意玲珑应用构建指南(一):规范体系与配置文件全解析
  • 二、IDE集成DeepSeek保姆级教学(使用篇)
  • 2-1文件描述符
  • DeepSeek如何快速开发PDF转Word软件
  • DeepSeek 开源周(2025/0224-0228)进度全分析:技术亮点、调用与编程及潜在影响
  • WPF高级 | WPF 3D 图形编程基础:创建立体的用户界面元素
  • Uniapp开发微信小程序插件的一些心得
  • CSS—背景属性与盒子模型(border、padding、margin)
  • ipywidgets深度探索:从交互原理到企业级应用
  • /ɪ/音的字母或字母组合的单词